记住我功能的基本原理

当用户登录发起认证请求时,会通过UsernamePasswordAuthenticationFilter进行用户认证,认证成功之后,SpringSecurity 调用前期配置好的记住我功能,实际是调用了RememberMeService接口,其接口的实现类会将用户的信息生成Token并将它写入 response 的Cookie中,在写入的同时,内部的TokenRepositoryTokenRepository会将这份Token再存入数据库一份。

当用户再次访问服务器资源的时候,首先会经过RememberMeAuthenticationFiler过滤器,在这个过滤器里面会读取当前请求中携带的 Cookie,这里存着上次服务器保存 的Token,然后去数据库中查找是否有相应的 Token,如果有,则再通过UserDetailsService获取用户的信息。

记住我功能的过滤器

从图中可以得知记住我的过滤器在过滤链的中部,注意是在UsernamePasswordAuthenticationFilter之后。

前端页面checkbox设置

在 html 中增加记住我复选框checkbox控件,注意其中复选框的name 一定必须为remember-me

<input type="checkbox" name="remember-me" value="true"/>

配置cookie存储数据库源

本例中使用了 springboot 管理的数据库源,所以注意要配置spring-boot-starter-jdbc的依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

如果不配置会报编译异常:

The type org.springframework.jdbc.core.support.JdbcDaoSupport cannot be resolved. It is indirectly referenced from required .class files

记住我的安全认证配置:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource; @Override
protected void configure(HttpSecurity http) throws Exception {
// 将自定义的验证码过滤器放置在 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/login") // 设置登录页面
.loginProcessingUrl("/user/login") // 自定义的登录接口
.successHandler(myAuthenctiationSuccessHandler)
.failureHandler(myAuthenctiationFailureHandler)
.defaultSuccessUrl("/home").permitAll() // 登录成功之后,默认跳转的页面
.and().authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.antMatchers("/", "/index", "/user/login", "/code/image").permitAll() // 设置所有人都可以访问登录页面
.anyRequest().authenticated() // 任何请求,登录后可以访问
.and().csrf().disable() // 关闭csrf防护
.rememberMe() // 记住我配置
.tokenRepository(persistentTokenRepository()) // 配置数据库源
.tokenValiditySeconds(3600)
.userDetailsService(userDetailsService);
} @Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl persistentTokenRepository = new JdbcTokenRepositoryImpl();
// 将 DataSource 设置到 PersistentTokenRepository
persistentTokenRepository.setDataSource(dataSource);
// 第一次启动的时候自动建表(可以不用这句话,自己手动建表,源码中有语句的)
// persistentTokenRepository.setCreateTableOnStartup(true);
return persistentTokenRepository;
}
}

注意:在数据库源配置之前,建议手动在数据库中新增一张保存的cookie表,其数据库脚本在JdbcTokenRepositoryImpl的静态属性中配置了:

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
PersistentTokenRepository {
/** Default SQL for creating the database table to store the tokens */
public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
+ "token varchar(64) not null, last_used timestamp not null)";
}

因此可以事先执行以下sql 脚本创建表:

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);

当然,JdbcTokenRepositoryImpl自身还有一个setCreateTableOnStartup()方法进行开启自动建表操作,但是不建议使用。

当成功登录之后,RememberMeService会将成功登录请求的cookie存储到配置的数据库中:

源码分析

首次请求

首先进入到AbstractAuthenticationProcessingFilter过滤器中的doFilter()方法:

public abstract class AbstractAuthenticationProcessingFilter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException { …… try {
authResult = attemptAuthentication(request, response);
……
}
catch (InternalAuthenticationServiceException failed) {
……
} successfulAuthentication(request, response, chain, authResult);
}
}

其中当用户认证成功之后,会进入successfulAuthentication()方法,在用户信息被保存在了SecurityContextHolder之后,其中就调用了rememberMeServices.loginSuccess():

protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException { …… SecurityContextHolder.getContext().setAuthentication(authResult); // 调用记住我服务接口的登录成功方法
rememberMeServices.loginSuccess(request, response, authResult); // Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
} successHandler.onAuthenticationSuccess(request, response, authResult);
}

在这个RememberMeServices有个抽象实现类,在抽象实现类loginSuccess()方法中进行了记住我功能判断,为什么前端的复选框控件的 name 必须为remember-me,原因就在此:

public abstract class AbstractRememberMeServices implements RememberMeServices,
InitializingBean, LogoutHandler { public static final String DEFAULT_PARAMETER = "remember-me"; private String parameter = DEFAULT_PARAMETER; @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);
}
}

当识别到记住我功能开启的时候,就会进入onLoginSuccess()方法,其具体的方法实现在PersistentTokenBasedRememberMeServices类中:

public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {

    protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName(); logger.debug("Creating new persistent login for user " + username); PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
// 保存cookie到数据库
tokenRepository.createNewToken(persistentToken);
// 将cookie回写一份到响应中
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
}

上面的tokenRepository.createNewToken()和addCookie()就将 cookie 保存到数据库并回显到响应中。

第二次请求

当第二次请求传到服务器的时候,请求会被RememberMeAuthenticationFilter过滤器进行过滤:过滤器首先判定之前的过滤器都没有认证通过当前用户,也就是SecurityContextHolder中没有已经认证的信息,所以会调用rememberMeServices.autoLogin()的自动登录接口拿到已通过认证的rememberMeAuth进行用户认证登录:

public class RememberMeAuthenticationFilter extends GenericFilterBean implements
ApplicationEventPublisherAware {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res; // SecurityContextHolder 不存在已经认证的 authentication,表示前面的过滤器没有做过任何身份认证
if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 调用自动登录接口
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response); if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth); // Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); onSuccessfulAuthentication(request, response, rememberMeAuth); …… }
catch (AuthenticationException authenticationException) {
……
}
} chain.doFilter(request, response);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
} chain.doFilter(request, response);
}
}
}

这个自动登录的接口,又由其抽象实现类进行实现:

public abstract class AbstractRememberMeServices implements RememberMeServices,
InitializingBean, LogoutHandler {
@Override
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
// 从请求中获取cookie
String rememberMeCookie = extractRememberMeCookie(request); if (rememberMeCookie == null) {
return null;
} logger.debug("Remember-me cookie detected"); if (rememberMeCookie.length() == 0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
} UserDetails user = null; try {
// 解码请求中的cookie
String[] cookieTokens = decodeCookie(rememberMeCookie);
// 根据 cookie 找到用户认证
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user); logger.debug("Remember-me cookie accepted"); return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
……
} cancelCookie(request, response);
return null;
}
}

processAutoLoginCookie()的具体实现还是由PersistentTokenBasedRememberMeServices来实现,总得来说就是一顿判定当前的cookieTokens是不是在数据库中存在tokenRepository.getTokenForSeries(presentedSeries),并判断是不是一样的,如果一样,就是把当前请求的新 token 更新保存到数据库,最后通过当前请求token中的用户名调用UserDetailsService.loadUserByUsername()进行用户认证。

public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
protected UserDetails processAutoLoginCookie(String[] cookieTokens,
HttpServletRequest request, HttpServletResponse response) { if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2
+ " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1]; // 从数据库查询上次保存的token
PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries); if (token == null) {
// 查询不到抛异常
throw new RememberMeAuthenticationException(……);
} // token 不匹配抛出异常
// We have a match for this user/series combination
if (!presentedToken.equals(token.getTokenValue())) {
// Token doesn't match series value. Delete all logins for this user and throw
// an exception to warn them.
tokenRepository.removeUserTokens(token.getUsername()); throw new CookieTheftException(……);
} // 过期判断
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 {
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
addCookie(newToken, request, response);
}
catch (Exception e) {
……
} return getUserDetailsService().loadUserByUsername(token.getUsername());
}
}

SpringBoot + Spring Security 学习笔记(四)记住我功能实现的更多相关文章

  1. SpringBoot + Spring Security 学习笔记(五)实现短信验证码+登录功能

    在 Spring Security 中基于表单的认证模式,默认就是密码帐号登录认证,那么对于短信验证码+登录的方式,Spring Security 没有现成的接口可以使用,所以需要自己的封装一个类似的 ...

  2. SpringBoot + Spring Security 学习笔记(三)实现图片验证码认证

    整体实现逻辑 前端在登录页面时,自动从后台获取最新的验证码图片 服务器接收获取生成验证码请求,生成验证码和对应的图片,图片响应回前端,验证码保存一份到服务器的 session 中 前端用户登录时携带当 ...

  3. SpringBoot + Spring Security 学习笔记(二)安全认证流程源码详解

    用户认证流程 UsernamePasswordAuthenticationFilter 我们直接来看UsernamePasswordAuthenticationFilter类, public clas ...

  4. SpringBoot + Spring Security 学习笔记(一)自定义基本使用及个性化登录配置

    官方文档参考,5.1.2 中文参考文档,4.1 中文参考文档,4.1 官方文档中文翻译与源码解读 SpringSecurity 核心功能: 认证(你是谁) 授权(你能干什么) 攻击防护(防止伪造身份) ...

  5. Spring Security学习笔记

    Spring Web Security是Java web开发领域的一个认证(Authentication)/授权(Authorisation)框架,基于Servlet技术,更确切的说是基于Servle ...

  6. Spring Security学习笔记(三)

    之前提到过认证后怎么存放用户信息,令牌token是一种方式,session是另一种方式,这里介绍使用spring session data redis存储httpSession. 添加了以上依赖后,我 ...

  7. Spring Security学习笔记-自定义Spring Security过滤链

    Spring Security使用一系列过滤器处理用户请求,下面是spring-security.xml配置文件. <?xml version="1.0" encoding= ...

  8. Spring Security学习笔记一

    一.使用Spring Security 1.在pom 文件中添加Spring Security的依赖. <dependency> <groupId>org.springfram ...

  9. Spring security学习笔记(二)

    对比两种承载认证信息的方式: session vs token token验证方案: session验证方案: session即会话是将用户信息保存在服务端,根据请求携带的session_id,从服务 ...

随机推荐

  1. BZOJ_1042_[HAOI2008]硬币购物_容斥原理+背包

    BZOJ_1042_[HAOI2008]硬币购物_容斥原理+背包 题意: 硬币购物一共有4种硬币.面值分别为c1,c2,c3,c4.某人去商店买东西,去了tot次.每次带di枚ci硬币,买s i的价值 ...

  2. Python实现微信消息防撤回

    微信(WeChat)是腾讯公司于2011年1月21日推出的一款社交软件,8年时间微信做到日活10亿,日消息量450亿.在此期间微信也推出了不少的功能如:“摇一摇”.“漂流瓶”.“朋友圈”.“附近的人” ...

  3. 数据库分片(Database Sharding)详解

    本文由云+社区发表 作者:腾讯云数据库 Introduction 导言 任何看到显著增长的应用程序或网站,最终都需要进行扩展,以适应流量的增加.以确保数据安全性和完整性的方式进行扩展,对于数据驱动的应 ...

  4. 关于Redis的常见面试题解析

    1. 使用redis有哪些好处? (1) 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1) (2) 支持丰富数据类型,支持string,li ...

  5. 我眼中的 Nginx(三):Nginx 变量和变量插值

    张超:又拍云系统开发高级工程师,负责又拍云 CDN 平台相关组件的更新及维护.Github ID: tokers,活跃于 OpenResty 社区和 Nginx 邮件列表等开源社区,专注于服务端技术的 ...

  6. String字符串类总结

    object类 int hashCode() Object定义的hashCode方法能为不同对象返回不同的整数.实际上是把JVM给对象分配的地址转化为整数,确保了逻辑上的唯一性.而转化的散列算法,可能 ...

  7. JSON Web Token入门教程

    目录 一.跨域认证的问题 二.JWT的原理 三.JWT的数据结构 3.1Header 3.2Payload 3.3 Signature 3.4 Base64URL 四.JWT的使用方式 五.JWT的几 ...

  8. 纯css折叠区域-基于checkbox

    Accordion Accordion即可折叠区域,和<details>标签类似,不过更灵活些.折叠区域往常多用JavaScript实现,这里就纯粹用CSS,就想法上也是异途同归. 折叠区 ...

  9. React 项目生产版本迭代页面不刷新问题

    React 页面缓存 react 打包项目进行服务端部署后,会发现index.html被浏览器缓存,访问项目url指向的还是上个版本的内容.原理是index.html被缓存后,路由指向和跳转都是旧版的 ...

  10. SuperMap iObject入门开发系列七管线横断面分析

    本文是一位好友“托马斯”授权给我来发表的,介绍都是他的研究成果,在此,非常感谢. 管线横断面分析功能是管线系统常见的一个分析功能,地下管线横断面分析的原理,是根据需求,在管线区域画一条横截面线,使其与 ...