Spring Security之RememberMe

news/2024/12/21 20:30:15/

前言

今天我们来聊聊RemenberMe功能,他的实现或许跟你的最初的想法不一样哦。

什么是RememberMe

其实就是“记住我”功能。在我们工作/生活中,总会存在被打断的情况,临时需要去做其他事情。而当我们想回来继续处理的时候,通常都会发现,网页已经退出登录态了。也就是开发同学常说的,session超时了。而“记住我”则可以完美解决该问题。

除此之外,对于移动端的APP而言,也有同样的妙用。可以让用户长时间保持登录态。

“记住我”意味着,只要用户不是主动退出的,都应该认为用户还处于登录态。

如何实现RememberMe

我们来看看实现RememberMe需要做什么?一个完整流程是怎么样的?

  1. 登录认证成功后,我们需要生成一个RememberMe的凭证,然后返回给浏览器。这里通常是一个长期有效的Cookie,有效期与你预期的RememberMe的时间一样。
  2. 检测用户是否处于登录态。当用户没有处于登录态,且cookie中存在RememberMe凭证,在确认凭证有效之后,自动恢复登录态。
  3. 用户主动注销登录态,我们就要清理cookie,销毁凭证。
  • 对于步骤一,SpringSecurity提供了一个新的组件:RememberMeServices。在我们专栏的Spring Security之认证过滤器的UsernamePasswordAuthenticationFilter中我们也看到了源码在认证完成后调用该组件完成RememberMe的凭证处理。

    PS: 可能有同学会问为什么不用AuthenticationSuccessHandler。如果你看过他的类注释,你就能找到答案了。这个组件的设计本意是完成登录后给用户呈现的内容。而我们这里要做的与之无关。

  • 对于步骤二,我们则需要一个新的过滤器,用来检查每个请求的登录态,以及处理登录态恢复。这便是RememberMeAuthenticationFilter。

  • 而步骤三,通过LogoutHandler就行。实际上,RememberMeServices也是他的子类。这一点体现了功能的高内聚,将与RememberMe相关的内容都放在一起了。

PS:题外话,对于软件而言,当功能足够小的时候,可以放在同一个类中。可当功能随着发展,细节就会增加,类就会显得臃肿。我们应当在嗅到代码的坏味道时,重新对功能进行审视,进行必要的新的抽象,大胆定义新的组件,以满足新的业务诉求。

Spring Security的设计

按照“高内聚低耦合”原则,我们应当把RememberMe相关的功能都放在同一个组件里。SpringSecurity则设计了两层结构。首先是RememberMeServices接口:

java">public interface RememberMeServices {// 自动登录。这自然是与session超时之后,从cookie中读取凭证自动恢复登录态有关Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);// 登录失败的处理。要是自动登录失败了,那必须把cookie清理了哇void loginFail(HttpServletRequest request, HttpServletResponse response);// 登录成功。那就是要生成RememberMe凭证并丢到cookie了。void loginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication);
}

除了自动登录(基于RememberMe的凭证),还有与认证过滤器配合的登录成功与登录失败的处理(涉及凭证的生成与清理)。

这第二层便是AbstractRememberMeServices

java">public abstract class AbstractRememberMeServicesimplements RememberMeServices, InitializingBean, LogoutHandler, MessageSourceAware {@Overridepublic Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {// 寻找目标cookieString rememberMeCookie = extractRememberMeCookie(request);if (rememberMeCookie == null) {return null;}if (rememberMeCookie.length() == 0) {// 清理重置cookiecancelCookie(request, response);return null;}try {// 解析凭证String[] cookieTokens = decodeCookie(rememberMeCookie);// 通过凭证处理自动登录-这是抽象方法,由子类实现UserDetails user = processAutoLoginCookie(cookieTokens, request, response);// 检查用户状态this.userDetailsChecker.check(user);// 创建登录成功的认证信息return createSuccessfulAuthentication(request, user);}catch (CookieTheftException ex) {// 被攻击了,清理cookiecancelCookie(request, response);throw ex;}catch (UsernameNotFoundException ex) {// 没解析到用户}catch (InvalidCookieException ex) {// cookie已失效}catch (AccountStatusException ex) {// 用户状态异常}catch (RememberMeAuthenticationException ex) {// 登录异常}// 清理cookiecancelCookie(request, response);// 返回空,意味着通过rememberMe登录失败了。交由原来的登录过滤器处理。return null;}@Overridepublic void loginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication) {// 检查是否勾选了rememberMeif (!rememberMeRequested(request, this.parameter)) {this.logger.debug("Remember-me login not requested.");return;}// 完成登录后的处理。这是原来的登录认证后的操作。需要为止生成凭证。这是个抽象方法,由子类实现onLoginSuccess(request, response, successfulAuthentication);}@Overridepublic void loginFail(HttpServletRequest request, HttpServletResponse response) {// 重置cookiecancelCookie(request, response);// 登录失败后处理。这是个空方法,也是个钩子方法。不过目前子类都没有使用到。onLoginFail(request, response);}@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {// 清理保存凭证的cookiecancelCookie(request, response);}protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {String cookieValue = encodeCookie(tokens);Cookie cookie = new Cookie(this.cookieName, cookieValue);cookie.setMaxAge(maxAge);cookie.setPath(getCookiePath(request));if (this.cookieDomain != null) {cookie.setDomain(this.cookieDomain);}if (maxAge < 1) {cookie.setVersion(1);}cookie.setSecure((this.useSecureCookie != null) ? this.useSecureCookie : request.isSecure());// 设置了httpOnly后,js脚本将无法读取到cookie信息// {@link https://cloud.tencent.com/developer/article/2097036}cookie.setHttpOnly(true);response.addCookie(cookie);}
}

除了实现RememberMeServices之外,还实现了LogoutHandler。如此一来,才能完成我们上面的流程分析的功能。这里面的还有个门道,就是为什么不将RememberMeServices直接继承LogoutHandler。因为这两本就是两个不同的功能,从组件设计上就应该解耦隔离。但在是实现上又需要配合,单独设计一个实现类来完成在用户注销登录态时清理RememberMe凭证也不是不可以。但这样的话,意味着RememberMe的实现就散落在两个地方了,没有内聚。而这,可能就是AbstractRememberMeServices存在的意义之一。

好了,到这里,我们需要关心的还有如下问题:

  1. 怎么存储RememberMe的凭证?
  2. 怎么把凭证给前端?
  3. 前端怎么再把凭证给后端?

这听起来都是公共功能,因为我们前端的每个请求都可能会丢失登录态,需要通过RememberMe凭证完成登录态恢复。
庆幸的是,这些问题前辈们已经解决了,并且对前端完全无感。那就是基于一个长期有效的cookie,通过这个cookie把凭证给前端。而后端需要校验凭证时,也从该cookie中读取。

至于凭证的校验,这里有两种方式:

  • 基于令牌的 remember-me:
    这是最常见的实现方式。当用户选择“记住我”选项并成功登录后,系统会生成一个唯一的凭证(token),并将该令牌存储在数据库中。同时,这个令牌会被设置为一个 cookie 发送到用户的浏览器。
    当用户下次访问应用时,即使没有显式登录,系统也会检查这个 cookie 中的令牌,并数据库中的令牌进行匹配。如果匹配成功,则自动登录用户。

  • 基于哈希的 remember-me:
    这种方式不需要在服务器端存储任何信息。它通过将用户的用户名、过期时间和一个密钥进行哈希计算,生成一个签名。这个签名会被设置为一个 cookie 发送到用户的浏览器。
    当用户下次访问应用时,系统会验证这个签名的有效性。如果签名有效,则自动登录用户。

由此,也引出SpringSecuirty的两个RememberMeServices的具体实现。

  1. PersistentTokenBasedRememberMeServices
    由服务端存储Token。因此他依赖于PersistentTokenRepository。Spring提供了两个实现:InMemoryTokenRepositoryImpl、JdbcTokenRepositoryImpl。如果你想自定义,又不知道怎么设计表结构,那么可以参考下JdbcTokenRepositoryImpl#CREATE_TABLE_SQL
    java">public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {@Overrideprotected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication) {String username = successfulAuthentication.getName();// 生成token,// 第二个参数是唯一键,后面要通过他来从数据库中读取凭证// 第三个参数是凭证// 第四个参数是凭证创建时间PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),generateTokenData(), new Date());// 保存到数据库this.tokenRepository.createNewToken(persistentToken);// 将凭证添加到cookieaddCookie(persistentToken, request, response);}protected String generateSeriesData() {byte[] newSeries = new byte[this.seriesLength];this.random.nextBytes(newSeries);return new String(Base64.getEncoder().encode(newSeries));}protected String generateTokenData() {byte[] newToken = new byte[this.tokenLength];// 随机生成tokenthis.random.nextBytes(newToken);return new String(Base64.getEncoder().encode(newToken));}@Overrideprotected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,HttpServletResponse response) {String presentedSeries = cookieTokens[0];String presentedToken = cookieTokens[1];// 从数据库中读取凭证PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);if (token == null) {// 抛出认证异常throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);}// 校验凭证是否有效,即是否跟数据库中的一致if (!presentedToken.equals(token.getTokenValue())) {// 凭证与数据库中的不一致,意味着可能遭到攻击了,清理掉该用户所有凭证this.tokenRepository.removeUserTokens(token.getUsername());// 抛出认证异常throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen","Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));}// 检查token有效期if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {// 抛出认证异常throw new RememberMeAuthenticationException("Remember-me login has expired");}PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),generateTokenData(), new Date());try {// 刷新凭证有效期		this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());// 重新放到tokenaddCookie(newToken, request, response);}catch (Exception ex) {// 抛出认证异常throw new RememberMeAuthenticationException("Autologin failed due to data access problem");}// 自动认证成功,返回用户信息return getUserDetailsService().loadUserByUsername(token.getUsername());}@Overrideprotected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication) {String username = successfulAuthentication.getName();// 生成凭证 -- 随机的PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),generateTokenData(), new Date());try {// 此处的方法名有点歧义,其实是保存将凭证保存到数据库this.tokenRepository.createNewToken(persistentToken);// 将凭证通过cookie返回给浏览器addCookie(persistentToken, request, response);}catch (Exception ex) {// 单纯地吃掉异常,因为登录成功了。不能因为rememberMe异常导致登录失败。}@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {super.logout(request, response, authentication);if (authentication != null) {// 清理掉服务端保存的RememberMe凭证this.tokenRepository.removeUserTokens(authentication.getName());}}
    }
    
    实现的关键就是,一个唯一值,用来从数据库中索引到当前凭证。另一个便是这凭证。因此这两个信息都是要保存在cookie中的。同样也要保存在数据库中。因为有效期是从数据库中读取到凭证的创建时间后再计算得到的。
  2. TokenBasedRememberMeServices
    java">public class TokenBasedRememberMeServices extends AbstractRememberMeServices {@Overrideprotected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,HttpServletResponse response) {// 获取凭证if (!isValidCookieTokensLength(cookieTokens)) {throw new InvalidCookieException("Cookie token did not contain 3 or 4 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");}// 检查凭证是否有效long tokenExpiryTime = getTokenExpiryTime(cookieTokens);if (isTokenExpired(tokenExpiryTime)) {throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)+ "'; current time is '" + new Date() + "')");}// 通过凭证查询用户UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);// 检查凭证签名String actualTokenSignature = cookieTokens[2];// 默认的凭证签名算法为sha256RememberMeTokenAlgorithm actualAlgorithm = this.matchingAlgorithm;if (cookieTokens.length == 4) {actualTokenSignature = cookieTokens[3];// 指定了签名算法,解析actualAlgorithm = RememberMeTokenAlgorithm.valueOf(cookieTokens[2]);}// 通过用户信息和凭证算法,计算得出一个预期的有效凭证String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),userDetails.getPassword(), actualAlgorithm);// 凭证签名与计算的一致就是有效凭证if (!equals(expectedTokenSignature, actualTokenSignature)) {throw new InvalidCookieException("Cookie contained signature '" + actualTokenSignature + "' but expected '"+ expectedTokenSignature + "'");}return userDetails;}@Overridepublic void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication) {String username = retrieveUserName(successfulAuthentication);String password = retrievePassword(successfulAuthentication);if (!StringUtils.hasLength(username)) {// 用户名为空return;}if (!StringUtils.hasLength(password)) {// 密码为空,从数据库加载UserDetails user = getUserDetailsService().loadUserByUsername(username);password = user.getPassword();if (!StringUtils.hasLength(password)) {// 密码依然为空,就退出处理了。为了不影响正常的登录流程return;}}// 计算凭证有效期,以便刷新有效期int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);long expiryTime = System.currentTimeMillis();// SEC-949expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);// 用新的有效期重新计算签名String signatureValue = makeTokenSignature(expiryTime, username, password, this.encodingAlgorithm);// 刷新凭证setCookie(new String[] { username, Long.toString(expiryTime), this.encodingAlgorithm.name(), signatureValue },tokenLifetime, request, response);}protected String makeTokenSignature(long tokenExpiryTime, String username, String password,RememberMeTokenAlgorithm algorithm) {// 拼接签名的目标数据String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();try {// 获取签名算法MessageDigest digest = MessageDigest.getInstance(algorithm.getDigestAlgorithm());// 完成签名return new String(Hex.encode(digest.digest(data.getBytes())));}catch (NoSuchAlgorithmException ex) {throw new IllegalStateException("No " + algorithm.name() + " algorithm available!");}}
    }
    
    可以看到签名的数据甚至包括用户密码,而其内部类TokenBasedRememberMeServices.RememberMeTokenAlgorithm提供的两个算法是MD5和SHA256安全性都不是很好。虽然其父类setCookie方法中将该cookie设置为httpOnly,相对提升安全性。

小结

rememberMe功能的核心主键RememberMeServices有两个实现:

RememberMeServices原理劣势优势
PersistentTokenBasedRememberMeServices通过随机产生凭证,保存到数据库中。占用服务端存储资源提高安全性,凭证不包含任何用户信息
TokenBasedRememberMeServices以时间换空间,将用户信息存储于凭证中,而凭证是保存在cookie中的。cookie需要在网络中传输,存在暴露用户信息风险。所幸spring将cookie设置为httpOnly。不占用服务端的存储资源

完整流程源码

  1. 登录成功后,会调用this.rememberMeServices.loginSuccess(request, response, authResult);,这点在我们专栏的Spring Security之认证过滤器的UsernamePasswordAuthenticationFilter之中也看到了。
  2. RememberMeAuthenticationFilter
    java">public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);}private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {if (this.securityContextHolderStrategy.getContext().getAuthentication() != null) {// 处于登录态中,无需处理chain.doFilter(request, response);return;}// 通过rememberMeServices完成凭证校验Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);if (rememberMeAuth != null) {// 通过AuthenticationManager进行校验,这里校验的凭证本身的合法性。对于RememberMe有对应的RememberMeAuthenticationProvider。try {rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);// 与认证过滤器一样,需要完成如下事项// 1. 保存安全上下文SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();context.setAuthentication(rememberMeAuth);this.securityContextHolderStrategy.setContext(context);// 这里是个空方法onSuccessfulAuthentication(request, response, rememberMeAuth);if (this.eventPublisher != null) {// 发布登录成功事件this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(this.securityContextHolderStrategy.getContext().getAuthentication(), this.getClass()));}if (this.successHandler != null) {// 执行登录处理器,跳转到指定地址this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);return;}}catch (AuthenticationException ex) {// 登录失败,注意这里可是通过AuthenticationManager登录失败,不是自动登录失败,因为自动登录不在这个try-catch代码块中。this.rememberMeServices.loginFail(request, response);onUnsuccessfulAuthentication(request, response, ex);}}chain.doFilter(request, response);}
    }
    
    这里必须要说一下,如果RememberMeServices#autoLogin失败了,抛出的异常是如何处理的。这将由专栏之前的安全异常处理完成。当抛出的是认证异常,将会跳转到登录页面。如果是持久化的,那么数据库中的凭证也不会被清理。关于这点,我推测Spring考虑的是,只有认证(非RememberMe)失败或者用户主动注销才清理。至于自动认证失败,也就意味着凭证已经无效了,清理与否也就无关紧要了。换而言之,只有有效的凭证才有清理的意义。

3.登出/登录态注销。
这个前面已经看到AbstractRememberServices也是LogoutHandler,将会被LogoutFilter调用。

配置RememberMe

HttpSecuirty.rememberMe(
customizer -> customizer.tokenValiditySeconds((int) Duration.ofDays(7).toSeconds())
);

该配置引入了RememberMeConfigurer,同时指定了凭证的有效时间。接下来我们看看RememberMeConfigurer。

java">public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>>extends AbstractHttpConfigurer<RememberMeConfigurer<H>, H> {@Overridepublic void init(H http) throws Exception {validateInput();String key = getKey();// 获取RememberMeServicesRememberMeServices rememberMeServices = getRememberMeServices(http, key);// 放入共享对象中,因为认证过滤器需要与之协同http.setSharedObject(RememberMeServices.class, rememberMeServices);LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);if (logoutConfigurer != null && this.logoutHandler != null) {// 登记RememberMe的登出处理器	logoutConfigurer.addLogoutHandler(this.logoutHandler);}// 这个用来校验RememberMeAuthentication的// 无论那种凭证都需要这个Provider来鉴定// 他会被放到ProviderManager(他是AuthenticationManager)中RememberMeAuthenticationProvider authenticationProvider = new RememberMeAuthenticationProvider(key);authenticationProvider = postProcess(authenticationProvider);http.authenticationProvider(authenticationProvider);initDefaultLoginFilter(http);}private RememberMeServices getRememberMeServices(H http, String key) throws Exception {// 如果用户指定了RememberMeServices if (this.rememberMeServices != null) {if (this.rememberMeServices instanceof LogoutHandler && this.logoutHandler == null) {// 确认实现了logoutHandlerthis.logoutHandler = (LogoutHandler) this.rememberMeServices;}return this.rememberMeServices;}// 创建AbstractRememberMeServices tokenRememberMeServices = createRememberMeServices(http, key);// 指定rememberMe参数,因为这是用户选择勾选才有的tokenRememberMeServices.setParameter(this.rememberMeParameter);tokenRememberMeServices.setCookieName(this.rememberMeCookieName);if (this.rememberMeCookieDomain != null) {tokenRememberMeServices.setCookieDomain(this.rememberMeCookieDomain);}if (this.tokenValiditySeconds != null) {// 指定了凭证有效期tokenRememberMeServices.setTokenValiditySeconds(this.tokenValiditySeconds);}if (this.useSecureCookie != null) {// 指定使用httpstokenRememberMeServices.setUseSecureCookie(this.useSecureCookie);}if (this.alwaysRemember != null) {// 配置了总是rememberMetokenRememberMeServices.setAlwaysRemember(this.alwaysRemember);}tokenRememberMeServices.afterPropertiesSet();this.logoutHandler = tokenRememberMeServices;this.rememberMeServices = tokenRememberMeServices;return tokenRememberMeServices;}	private AbstractRememberMeServices createRememberMeServices(H http, String key) {// 配置了tokenRepository,那就是持久化的,否则就是基于Hash算法,后端不存在凭证return (this.tokenRepository != null) ? createPersistentRememberMeServices(http, key): createTokenBasedRememberMeServices(http, key);}@Overridepublic void configure(H http) {// 前面的源码我们也发现需要来AuthenticationManager,因此在构建时需要从共享对象中获取RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter(http.getSharedObject(AuthenticationManager.class), this.rememberMeServices);if (this.authenticationSuccessHandler != null) {// 如果指定了rememberMe的认证成功处理器则配置。这个可以与认证过滤器不一样。如果没有配置的话,按照流程会继续执行原请求(还在请求的处理过程中)rememberMeFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);}SecurityContextConfigurer<?> securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {SecurityContextRepository securityContextRepository = securityContextConfigurer.getSecurityContextRepository();// 配置SecurityContextPepository,这与认证过滤器类似,认证成功需要保存安全上下文rememberMeFilter.setSecurityContextRepository(securityContextRepository);}rememberMeFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());rememberMeFilter = postProcess(rememberMeFilter);// 增加过滤器。http.addFilter(rememberMeFilter);}
}

就这样,我们把完成自动认证-恢复登录态的RememberMeAuthenticationFilter、清理凭证的LogoutHandler、和与认证过滤器协作的负责创建RememberMe凭证的RememberMeServices就都配置好了。
如果想要使用服务端存储方案的PersistentTokenBasedRememberMeServices,需要声明一个PersistentTokenRepository对象。例如:

java">@Bean
public PersistentTokenRepository persistentTokenRepository() {return new InMemoryTokenRepositoryImpl();
}

总结

  1. RememberMe可以作为保持登录态的一种手段,减少用户频繁使用系统的操作。
  2. RememberMe功能需要认证过滤器和RememberMeAuthenticationFilter、LogoutFilter(体现在LogoutHandler上)的协同。
  3. RememberMe最核心的凭证校验能力,分为:PersistentTokenBasedRememberMeServices(基于服务端存储凭证)TokenBasedRememberMeServices(基于Hash算法验证凭证)

后记

至此,Spring Security的核心内容基本就介绍完毕了。至于Logout这个太过于简单,不准备单独介绍了,无非就是清理登录态,也就是安全上下文。下次就是要进行功能定制了,例如:验证码校验、自定义AuthorizationManager(鉴权/授权过滤器)


http://www.ppmy.cn/news/1537523.html

相关文章

DBMS-3.4 SQL(4)——存储过程和函数触发器

本文章的素材与知识来自李国良老师和王珊老师。 存储过程和函数 一.存储过程 1.语法 2.示例 &#xff08;1&#xff09; 使用DELIMITER更换终止符后用于编写存储过程语句后&#xff0c;在下次执行SQL语句时记得再使用DELIMITER将终止符再换回分号。 使用DELIMITER更换终止符…

SQL进阶技巧:如何优雅求解指标累计去重问题?

目录 0 需求概述 1 数据准备 2 问题分析 3 小结 0 需求概述 近期公司开发某项学习功能,改功能有很多学习内容(如java,C,python等方向),每天都会有众多学习用户学习某一项或者多项学习内容。产生数据如下表: 产生数据如下表: 日期 内容 学习用户 2022…

Python字符串转JSON格式指南

Python字符串转JSON格式指南 一、需求分析 在数据处理和网络通信中&#xff0c;JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式&#xff0c;易于人阅读和编写&#xff0c;同时也易于机器解析和生成。本指南旨在介绍如何在Python中将字符串…

小程序项目实践(一)--项目的初始化以及前期的准备工作

目录 1.起步 1.1 uni-app 简介 1.2 开发工具 1.2.1 下载 HBuilderX 1.2.2 安装 HBuilderX 1.2.3 安装 scss/sass 编译 1.2.4 快捷键方案切换 1.2.5 修改编辑器的基本设置 1.3 新建 uni-app 项目 1.4 目录结构 1.5 把项目运行到微信开发者工具 1.6 使用 Git 管理项目 …

Python+Flask接口判断身份证省份、生日、性别、有效性验证+docker部署+Nginx代理运行

这里写目录标题 一、接口样式二、部署流程2.1 镜像打包2.1.1 准备工作2.1.2 build打包2.1.3 dokcer部署运行2.1.4 Nginx代理 三、代码及文件3.1 index.py3.2 areaCodes.json3.3 Dockerfile 一、接口样式 https://blog.henryplus.cn/idcardApi/idCard/query?idcard{idcard} 二、…

LeetCode.611有效三角形的个数

题目链接611. 有效三角形的个数 - 力扣&#xff08;LeetCode&#xff09; 1.常规解法&#xff08;会超时&#xff09; 由于构成三角形的条件为两边之和大于第三边&#xff0c;就可以遍历该数组&#xff0c;找到所有满足这个条件的三元组&#xff0c;代码如下&#xff1a; pub…

ArkTS基本语法详解

ArkTS基本语法详解&#xff1a; ArkTS页面布局 数据类型 条件判断 数组 ForEach循环遍历 List ListItem组件详解 官方文档 &#xff1a; https://developer.harmonyos.com/cn/develop/arkts/ ArkTS 是 HarmonyOS 优选的主力应用开发语言。 ArkTS 围绕应用开发在 TypeScript &…

Mac 需要杀毒软件?

大部分 mac用户普遍认为 Apple mac 不受病毒和恶意软件的影响。这导致许多 Mac 用户误以为无需为 Mac 安装防病毒软件&#xff0c;但事实并非如此。 在这篇文章中&#xff0c;将深入探讨 Mac 安全性的细节&#xff0c;探索针对 Apple 设备的恶意软件类型&#xff0c;并为您…