前言
今天我们来聊聊RemenberMe功能,他的实现或许跟你的最初的想法不一样哦。
什么是RememberMe
其实就是“记住我”功能。在我们工作/生活中,总会存在被打断的情况,临时需要去做其他事情。而当我们想回来继续处理的时候,通常都会发现,网页已经退出登录态了。也就是开发同学常说的,session超时了。而“记住我”则可以完美解决该问题。
除此之外,对于移动端的APP而言,也有同样的妙用。可以让用户长时间保持登录态。
“记住我”意味着,只要用户不是主动退出的,都应该认为用户还处于登录态。
如何实现RememberMe
我们来看看实现RememberMe需要做什么?一个完整流程是怎么样的?
- 登录认证成功后,我们需要生成一个RememberMe的凭证,然后返回给浏览器。这里通常是一个长期有效的Cookie,有效期与你预期的RememberMe的时间一样。
- 检测用户是否处于登录态。当用户没有处于登录态,且cookie中存在RememberMe凭证,在确认凭证有效之后,自动恢复登录态。
- 用户主动注销登录态,我们就要清理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存在的意义之一。
好了,到这里,我们需要关心的还有如下问题:
- 怎么存储RememberMe的凭证?
- 怎么把凭证给前端?
- 前端怎么再把凭证给后端?
这听起来都是公共功能,因为我们前端的每个请求都可能会丢失登录态,需要通过RememberMe凭证完成登录态恢复。
庆幸的是,这些问题前辈们已经解决了,并且对前端完全无感。那就是基于一个长期有效的cookie,通过这个cookie把凭证给前端。而后端需要校验凭证时,也从该cookie中读取。
至于凭证的校验,这里有两种方式:
-
基于令牌的 remember-me:
这是最常见的实现方式。当用户选择“记住我”选项并成功登录后,系统会生成一个唯一的凭证(token),并将该令牌存储在数据库中。同时,这个令牌会被设置为一个 cookie 发送到用户的浏览器。
当用户下次访问应用时,即使没有显式登录,系统也会检查这个 cookie 中的令牌,并与数据库中的令牌进行匹配。如果匹配成功,则自动登录用户。 -
基于哈希的 remember-me:
这种方式不需要在服务器端存储任何信息。它通过将用户的用户名、过期时间和一个密钥进行哈希计算,生成一个签名。这个签名会被设置为一个 cookie 发送到用户的浏览器。
当用户下次访问应用时,系统会验证这个签名的有效性。如果签名有效,则自动登录用户。
由此,也引出SpringSecuirty的两个RememberMeServices的具体实现。
- PersistentTokenBasedRememberMeServices
由服务端存储Token。因此他依赖于PersistentTokenRepository。Spring提供了两个实现:InMemoryTokenRepositoryImpl、JdbcTokenRepositoryImpl。如果你想自定义,又不知道怎么设计表结构,那么可以参考下JdbcTokenRepositoryImpl#CREATE_TABLE_SQL
实现的关键就是,一个唯一值,用来从数据库中索引到当前凭证。另一个便是这凭证。因此这两个信息都是要保存在cookie中的。同样也要保存在数据库中。因为有效期是从数据库中读取到凭证的创建时间后再计算得到的。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());}} }
- 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。 | 不占用服务端的存储资源 |
完整流程源码
- 登录成功后,会调用
this.rememberMeServices.loginSuccess(request, response, authResult);
,这点在我们专栏的Spring Security之认证过滤器的UsernamePasswordAuthenticationFilter之中也看到了。 - RememberMeAuthenticationFilter
这里必须要说一下,如果RememberMeServices#autoLogin失败了,抛出的异常是如何处理的。这将由专栏之前的安全异常处理完成。当抛出的是认证异常,将会跳转到登录页面。如果是持久化的,那么数据库中的凭证也不会被清理。关于这点,我推测Spring考虑的是,只有认证(非RememberMe)失败或者用户主动注销才清理。至于自动认证失败,也就意味着凭证已经无效了,清理与否也就无关紧要了。换而言之,只有有效的凭证才有清理的意义。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);} }
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();
}
总结
- RememberMe可以作为保持登录态的一种手段,减少用户频繁使用系统的操作。
- RememberMe功能需要认证过滤器和RememberMeAuthenticationFilter、LogoutFilter(体现在LogoutHandler上)的协同。
- RememberMe最核心的凭证校验能力,分为:PersistentTokenBasedRememberMeServices(基于服务端存储凭证)TokenBasedRememberMeServices(基于Hash算法验证凭证)
后记
至此,Spring Security的核心内容基本就介绍完毕了。至于Logout这个太过于简单,不准备单独介绍了,无非就是清理登录态,也就是安全上下文。下次就是要进行功能定制了,例如:验证码校验、自定义AuthorizationManager(鉴权/授权过滤器)