loginPage() 一般来说不直接设置成(登录)接口,因为设置了接口会配置成无权限即可访问(当然设置成登录页面也需要配置无权限访问),所以会走匿名filter, 也就意味着不会走�%A
2 、 添加rememberMe配置 信息
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 如果token表不存在,使用下面语句可以初始化 persistent_logins(ddl在db目录下) 表;若存在,请注释掉这条语句,否则会报错。
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
formAuthenticationConfig.configure(http);
http. ....
.and()
// 开启 记住我功能,意味着 RememberMeAuthenticationFilter 将会 从Cookie 中获取token信息
.rememberMe()
// 设置 tokenRepository ,这里默认使用 jdbcTokenRepositoryImpl,意味着我们将从数据库中读取token所代表的用户信息
.tokenRepository(persistentTokenRepository())
// 设置 userDetailsService , 和 认证过程的一样,RememberMe 有专门的 RememberMeAuthenticationProvider ,也就意味着需要 使用UserDetailsService 加载 UserDetails 信息
.userDetailsService(userDetailsService)
// 设置 rememberMe 的有效时间,这里通过 配置来设置
.tokenValiditySeconds(securityProperties.getLogin().getRememberMeSeconds())
.and()
.csrf().disable(); // 关闭csrf 跨站(域)攻击防控
}
这里解释下配置:
-
rememberMe() 开启 记住我功能,意味着 RememberMeAuthenticationFilter 将会 从Cookie 中获取token信息
-
tokenRepository() 配置 token的获取策略,这里配置成从数据库中读取
-
userDetailsService() 配置 UserDetaisService (如果不熟悉该对象,建议回顾认证过程)
-
tokenValiditySeconds() 设置 rememberMe 的有效时间,这里通过 配置来设置
另一个重要的配置在登录页面,这里的 必须是 name="remember-me" ,rememberMe就是通过验证这个配置来开启remermberMe功能的。
<input name="remember-me" type="checkbox" value="true"/>记住我</td>
??实操结果应该为:进入登陆页面 ——> 勾选记住我后登录 ——> 成功后,查看persistent_logins 表发现有一条数据——> 重启项目 ——> 重新访问需要登录才能访问的页面,发现无需登录即可访问——> 删除 persistent_logins 表数据,等待token设置的有效时间过期,然后重新刷新页面发现跳转到登陆页面。
(二) RembemberMe 实现源码解析
?? 首先我们查看UsernamePasswordAuthenticationFiler(AbstractAuthenticationProcessingFilter) 的 successfulAuthentication() 方法内部源码:
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
// 1 设置 认证成功的Authentication对象到SecurityContext中
SecurityContextHolder.getContext().setAuthentication(authResult);
// 2 调用 RememberMe 相关service处理
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//3 调用成功处理器
successHandler.onAuthenticationSuccess(request, response, authResult);
}
其中我们发现我们本次重点关注的一行代码: rememberMeServices.loginSuccess(request, response, authResult) , 查看这个方法内部源码:
@Override
public final void loginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
// 这里就在判断用户是否勾选了记住我
if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
通过 rememberMeRequested() 判断是否勾选了记住我。
onLoginSuccess() 方法 最终会调用到 PersistentTokenBasedRememberMeServices 的 onLoginSuccess() 方法,贴出其方法源码如下:
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
// 1 获取账户名
String username = successfulAuthentication.getName();
// 2 创建 PersistentRememberMeToken 对象
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
// 3 通过 tokenRepository 存储 persistentRememberMeToken 信息
tokenRepository.createNewToken(persistentToken);
// 4 将 persistentRememberMeToken 信息添加到Cookie中
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
分析下源码步骤:
-
获取 账户信息 username
-
传入 username 创建 PersistentRememberMeToken 对象
-
通过 tokenRepository 存储 persistentRememberMeToken信息
-
将 persistentRememberMeToken 信息添加到Cookie中
??这里的 tokenRepository 就是我们配置 rememberMe功能所设置的。经过上面的解析我们看到了rememberServices 将 创建一个 token 信息,并存储到数据库(因为我们配置的是数据库存储方式 JdbcTokenRepositoryImpl )中,并将token信息添加到Cookie中了。到这里,我们看到了RememberMe实现前的一些业务处理,那么后面如何实现RememberMe,我想大家心里大概都有个底了。这里直接抛出之前授权过程中我们没有提及到的 filter 类 RememberMeAuthenticationFilter,它是介于 UsernamePasswordAuthenticationFilter 和 AnonymousAuthenticationFilter 之间的一个filter,它主要负责的就是前面的filter都没有认证成功后从Cookie中获取token信息然后再通过tokenRepository 获取 登录用户名,然后UserDetailsServcie 加载 UserDetails 信息 ,最后创建 Authticaton(RememberMeAuthenticationToken) 信息再调用 AuthenticationManager.authenticate() 进行认证过程。
RememberMeAuthenticationFilter
??我们来看下 RememberMeAuthenticationFilter 的dofiler方法源码:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 1 调用 rememberMeServices.autoLogin() 获取Authtication 信息
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
// 2 调用 authenticationManager.authenticate() 认证
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
......
}
}
catch (AuthenticationException authenticationException) {
.....
}
chain.doFilter(request, response);
}
我们主要关注 rememberMeServices.autoLogin(request,response) 方法实现,查看器源码:
@Override
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
// 1 从Cookie 中获取 token 信息
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
if (rememberMeCookie.length() == 0) {
cancelCookie(request, response);
return null;
}
UserDetails user = null;
try {
// 2 解析 token信息
String[] cookieTokens = decodeCookie(rememberMeCookie);
// 3 通过 token 信息 生成 Uerdetails 信息
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
// 4 通过 UserDetails 信息创建 Authentication
return createSuccessfulAuthentication(request, user);
}
.....
}
内部实现步骤:
-
从Cookie中获取 token 信息并解析
-
通过 解析的token 生成 UserDetails (processAutoLoginCookie() 方法实现 )
-
通过 UserDetails 生成 Authentication ( createSuccessfulAuthentication() 创建 RememberMeAuthenticationToken )
其中最关键的一部是 processAutoLoginCookie() 方法是如何生成UserDetails 对象的,我们查看这个方法源码实现:
protected UserDetails processAutoLoginCookie(String[] cookieTokens,
HttpServletRequest request, HttpServletResponse response) {
final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];
// 1 通过 tokenRepository 加载数据库token信息
PersistentRememberMeToken token = tokenRepository
.getTokenForSeries(presentedSeries);
PersistentRememberMeToken newToken = new PersistentRememberMeToken(
token.getUsername(), token.getSeries(), generateTokenData(), new Date());
// 2 判断 用户传入token和数据中的token是否一致,不一致可能存在安全问题
if (!presentedToken.equals(token.getTokenValue())) {
tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(
messages.getMessage(
"PersistentTokenBasedRememberMeServices.cookieStolen",
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
}
try {
// 3 更新 token 并添加到Cookie中
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
newToken.getDate());
addCookie(newToken, request, response);
}
catch (Exception e) {
throw new RememberMeAuthenticationException(
"Autologin failed due to data access problem");
}
// 4 通过 UserDetailsService().loadUserByUsername() 方法加载UserDetails 信息并返回
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
我们看下其内部步骤:
-
通过 tokenRepository 加载数据库token信息
-
判断 用户传入token和数据中的token是否一致,不一致可能存在安全问题
-
更新 token 并添加到Cookie中
-
通过 UserDetailsService().loadUserByUsername() 方法加载UserDetails 信息并返回
?? 看到这里相信大家以下就明白了,当初为啥在启用rememberMe功能时要配置 tokenRepository 和 UserDetailsService了。
这里我就不再演示整个实现的流程了,老规矩,上流程图:
?? 本文介绍个性化认证和RememberMe的代码可以访问代码仓库中的 security 模块 ,项目的github 地址 : https://github.com/BUG9/spring-security
?? ?? ?? 如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!
如果您觉得本文的内容对您的学习有所帮助:
关键字:Spring Security