Spring Security之RememberMe
前言
今天我们来聊聊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接口:
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
public abstract class AbstractRememberMeServices
implements RememberMeServices, InitializingBean, LogoutHandler, MessageSourceAware {
@Override
public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
// 寻找目标cookie
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
if (rememberMeCookie.length() == 0) {
// 清理重置cookie
cancelCookie(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) {
// 被攻击了,清理cookie
cancelCookie(request, response);
throw ex;
}
catch (UsernameNotFoundException ex) {
// 没解析到用户
}
catch (InvalidCookieException ex) {
// cookie已失效
}
catch (AccountStatusException ex) {
// 用户状态异常
}
catch (RememberMeAuthenticationException ex) {
// 登录异常
}
// 清理cookie
cancelCookie(request, response);
// 返回空,意味着通过rememberMe登录失败了。交由原来的登录过滤器处理。
return null;
}
@Override
public void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
// 检查是否勾选了rememberMe
if (!rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
return;
}
// 完成登录后的处理。这是原来的登录认证后的操作。需要为止生成凭证。这是个抽象方法,由子类实现
onLoginSuccess(request, response, successfulAuthentication);
}
@Override
public void loginFail(HttpServletRequest request, HttpServletResponse response) {
// 重置cookie
cancelCookie(request, response);
// 登录失败后处理。这是个空方法,也是个钩子方法。不过目前子类都没有使用到。
onLoginFail(request, response);
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 清理保存凭证的cookie
cancelCookie(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中的。同样也要保存在数据库中。因为有效期是从数据库中读取到凭证的创建时间后再计算得到的。public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices { @Override protected 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); // 将凭证添加到cookie addCookie(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]; // 随机生成token this.random.nextBytes(newToken); return new String(Base64.getEncoder().encode(newToken)); } @Override protected 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()); // 重新放到token addCookie(newToken, request, response); } catch (Exception ex) { // 抛出认证异常 throw new RememberMeAuthenticationException("Autologin failed due to data access problem"); } // 自动认证成功,返回用户信息 return getUserDetailsService().loadUserByUsername(token.getUsername()); } @Override protected 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异常导致登录失败。 } @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { super.logout(request, response, authentication); if (authentication != null) { // 清理掉服务端保存的RememberMe凭证 this.tokenRepository.removeUserTokens(authentication.getName()); } } }
- TokenBasedRememberMeServices
可以看到签名的数据甚至包括用户密码,而其内部类public class TokenBasedRememberMeServices extends AbstractRememberMeServices { @Override protected 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]; // 默认的凭证签名算法为sha256 RememberMeTokenAlgorithm 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; } @Override public 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-949 expiryTime += 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)失败或者用户主动注销才清理。至于自动认证失败,也就意味着凭证已经无效了,清理与否也就无关紧要了。换而言之,只有有效的凭证才有清理的意义。public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware { @Override public 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。
public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<RememberMeConfigurer<H>, H> {
@Override
public void init(H http) throws Exception {
validateInput();
String key = getKey();
// 获取RememberMeServices
RememberMeServices 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) {
// 确认实现了logoutHandler
this.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) {
// 指定使用https
tokenRememberMeServices.setUseSecureCookie(this.useSecureCookie);
}
if (this.alwaysRemember != null) {
// 配置了总是rememberMe
tokenRememberMeServices.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);
}
@Override
public 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对象。例如:
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new InMemoryTokenRepositoryImpl();
}
总结
- RememberMe可以作为保持登录态的一种手段,减少用户频繁使用系统的操作。
- RememberMe功能需要认证过滤器和RememberMeAuthenticationFilter、LogoutFilter(体现在LogoutHandler上)的协同。
- RememberMe最核心的凭证校验能力,分为:PersistentTokenBasedRememberMeServices(基于服务端存储凭证)TokenBasedRememberMeServices(基于Hash算法验证凭证)
后记
至此,Spring Security的核心内容基本就介绍完毕了。至于Logout这个太过于简单,不准备单独介绍了,无非就是清理登录态,也就是安全上下文。下次就是要进行功能定制了,例如:验证码校验、自定义AuthorizationManager(鉴权/授权过滤器)