RememberMe

1.简介

Remember这个功能非常常见,下图就是QQ邮箱登录时的“记住我”选项。提到RememberMe,一些初学者往往会有一些误解,认为RememberMe功能就是把用户名/密码用Cookie保存在浏览器中,下次登录时不用再次输入用户名/密码。这个理解显然是不对的。我们这里所说的RememberMe是一种服务器端的行为。传统的登录方式基于Session会话,一旦用户会话超时过期,就要再次登录,这样太过于繁琐。如果能有一种机制,让用户会话过期之后,还能继续保持认证状态,就会方便很多,Remember就是为了解决这一需求而生的。

具体的实现思路就是通过Cookie来记录当前用户身份。当用户登录成功之后,会通过一定算法,将用户信息、时间戳等进行加密,加密完成后,通过响应头返回前端存储在cookie中,当浏览器会话过期之后,如果再次访问该网站,会自动将Cookie中的信息发送给服务器,服务器对Cookie中的信息进行校验分析,进而确定出用户的身份,Cookie中所保存的用户信息也是有实效的,例如三天、一周等。

2.基本使用

开启记住我

    @Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated().and().formLogin().and().rememberMe().and().csrf().disable();}

3.原理分析

3.1.RememberMeAuthenticationFilter

从上图中,当在SecurityConfig配置中开启了“记住我”功能之后,在进行认证时如果勾选了“记住我”选项,此时打开浏览器控制台,分析整个过程。首先当我们登录时,在登录请求中多了一个RememberMe的参数。

很显然,这个参数就是告诉服务器应该开启RememberMe功能的。如果自定也登录页面开启Remember功能应该多加入一个一样的请求参数就可以啦。该请求会被RememberMeAuthenticationFilter进行拦截然后自动登录。

具体源码:

  1. 请求到达过滤器后,首先判断SecurityContextHolder中是否有值,没值的话表示用户尚未登录,此时调用autoLogin方法进行自动登录
  2. 当自动登录成功后返回的rememberMeAuth不为null时,表示自动登录成功,此时调用authenticate方法对key进行校验,并且将登录成功的用户信息保存到SecurityContextHolder对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调并不包含RememberMeServices中的loginSuccess方法。
  3. 如果自动登录失败,则调用rememberMeServices.loginFail方法处理登录失败回调。onUnsuccessfulAuthentication和onSuccessfulAuthentication都是该过滤器中定义的空方法,并没有任何实现这就是RememberMeAuthenticationFilter过滤器所做的事情,成功将RememberMeService的服务继承进来。

3.2.RememberMeServices

这里一共定义了三个方法:

  1. autoLogin方法可以从请求中提取出需要的参数,完成自动登录功能。
  2. loginFail方法是自动登录失败的回调。
  3. loginSuccess方法是自动登录成功的回调。

3.3.TokenBasedRememberMeService

在开启记住我后如果没有加入额外配置默认实现就是由TokenBasedRememberMeServices进行的实现。查看这个类源码中processAutoLoginCookie方法实现:

processAutoLoginCooke方法主要用来验证Cookie中的令牌信息是否合法:

  1. 直接判断cookieTokens长度是否为3,不为3说明格式不对,则直接抛出异常。
  2. 从cookieTokens数组中提取出第1项,也就是过期时间,判断令牌是否过期,如果已经过期,则抛出异常。
  3. 根据用户名(cookieTokens数组的第0项)查询出当前用户对象。
  4. 调用makeTokenSignature方法生成一个签名,签名的生成过程如下:首先将用户名、令牌过期时间、用户密码以及key组成一个字符串,中间用“:”隔开,然后通过MD5消息摘要算法对该字符串进行加密,并将加密结果转为一个字符串返回。
  5. 判断第4步生成的签名和通过Cookie传来的签名是否相等(即cookieTokens数组的第2项),如果相等,表示令牌合法,则直接返回用户对象,否则抛出异常。

如果用户开启了“记住我”选项,则在用户登录成功后会调用这个方法。(不是自动登录)

  1. 在这个回调中,首先获取用户名和密码信息,如果用户密码在用户登录成功后从successfulAuthentication对象中擦除,则从数据库中重新加载出用户密码。
  2. 计算出令牌的过期时间,令牌默认有效期是两周。
  3. 根据令牌的过期时间、用户名以及用户密码,计算出一个签名。
  4. 调用setCookie方法设置Cookie,第一个参数是一个数组,数组中一共包含三项。用户名、过期时间以及签名,在setCookie方法中会将组转为字符串,并进行Base64编码后相应给前端。

3.4.总结

当用户通过用户名/密码的形式登录成功后,系统会根据用户的用户名、密码以及令牌的过期时间计算出一个签名,这个签名使用MD5消息摘要算法生成,是不可逆的。然后再将用户名、令牌过期时间以及签名拼接成一个字符串,中间用“:”隔开,对拼接好的字符串进行Base64编码,然后将编码后的结果返回到前端,也就是我们在浏览器中看到的令牌。当会话过期之后,访问系统资源时会自动携带上Cookie中的令牌,服务端拿到Cookie中的令牌后,先进行Base64阶码,解码后分别提取出令牌中的三项数据:接着根据令牌中的数据判断令牌是否已经过期,如果没有过期,则根据令牌中的用户名查询出用户信息:接着再计算出一个签名和令牌中的签名进行对比,如果一致,表示令牌是合法令牌,则自动登录成功,否则自动登录失败。

4.内存令牌

  1. 不同于TokenBasedRememberMeServices中的processAutologinCookie方法,这里cookieTokens数组的长度为2,第一项是series,第二项是token。
  2. 从cookieTokens数组中分别提取出series和token,然后根据series去内存中查询出一个PersistentRememberMeToken对象。如果查询出来的对象为null,表示内存中并没有series对应的值,本次自动登录失败。如果查询出来的token和从cookieTokens中解析出来的token不相同,说明自动登录令牌已经泄露(恶意用户利用令牌登录后,内存中的token变了),此时移除当前用户的所有自动登录记录并抛出异常。
  3. 根据数据库中查询出来的结果判断令牌是否过期,如果过期就抛出异常。
  4. 生成一个新的PersisitentRememberMeToken对象,用户名和series不变,token重新生成,date也使用当前时间。newToken生成后,根据series去修改内存中的token和date(即每次自动登录后都会产生新的token和date)
  5. 调用addCookie方法添加Cookie,在addCookie方法中,会调用到我们前面所说的setCookie方法,但是要注意第一个数组参数中只有两项:series和token(即返回到前端的令牌是通过对series和token进行Base64编码得到的)
  6. 最后将根据用户名查询用户对象并返回。

实现

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}@Override@Beanpublic UserDetailsService userDetailsServiceBean() throws Exception {return super.userDetailsServiceBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated().and().formLogin().and().rememberMe().rememberMeServices(rememberMeServices())  //指定remember service实现.and().csrf().disable();}@Beanpublic RememberMeServices rememberMeServices(){return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),userDetailsService(),new InMemoryTokenRepositoryImpl())}
}

5.持久化令牌

实现一:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate DataSource dataSource;@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}@Override@Beanpublic UserDetailsService userDetailsServiceBean() throws Exception {return super.userDetailsServiceBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated().and().formLogin().and().rememberMe().tokenRepository(persistentTokenRepository())
//                .rememberMeServices(rememberMeServices())  //指定remember service实现.and().csrf().disable();}//指定数据库持久化@Beanpublic PersistentTokenRepository persistentTokenRepository(){JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);jdbcTokenRepository.setCreateTableOnStartup(true);  //启动创建表结构return jdbcTokenRepository;}}

实现二:

手动创建表

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "+ "token varchar(64) not null, last_used timestamp not null)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate DataSource dataSource;@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}@Override@Beanpublic UserDetailsService userDetailsServiceBean() throws Exception {return super.userDetailsServiceBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated().and().formLogin().and().rememberMe().rememberMeServices(rememberMeServices())  //指定remember service实现.and().csrf().disable();}@Beanpublic RememberMeServices rememberMeServices(){JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource);return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),userDetailsService(),tokenRepository);}
}

启动项目后并查看数据库

使用记住我登录测试

系统重新启动发现仍然可以自动登录成功

6.传统Web开发自定义记住我

在认证请求中加入参数remember-me值为“true,on,yes,1”其中任意一个菜可以完成记住我功能。这个时候修改认证界面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head><meta charset="UTF-8"><title>登录</title>
</head>
<body>
<h1>用户登录</h1>
<form method="post" th:action="@{/doLogin}>">用户名:<input name="uname" type="text"/> <br>密码:<input name="passwd" type="password"/> <br>记住我:<input type="checkbox" name="remember-me" value="on|yes|true|1"/><br><input type="submit" value="登录"/>
</form>
</body>
</html>

配置中开启记住我

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated().and().formLogin().and().rememberMe().and().csrf().disable();}
}

7.前后端分离自定义记住我

自定义PersistentTokenBasedRememberMeServices

public class MyPersistentTokenBasedRememberMeService extends PersistentTokenBasedRememberMeServices {public MyPersistentTokenBasedRememberMeService(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {super(key, userDetailsService, tokenRepository);}/** 自定义前后端分离获取 remember-me 方式* */@Overrideprotected boolean rememberMeRequested(HttpServletRequest request, String parameter) {System.out.println(parameter);String paramValue =request.getAttribute(parameter).toString();if (paramValue != null) {if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {return true;}}return false;}}

在自定义LoginFilter中写入Remember-me参数

/*
* 自定义前后端分离认证 Filter
* */
public class LoginFilter extends UsernamePasswordAuthenticationFilter {@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {//1.判断是否是 post 方式请求if (!request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}//2.判断是否是 json 格式请求类型if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {//3.从json数据中获取用户输入用户名和密码进行认证{"uname":"xxx","password":"xxx"}try {//将request中的内容转化为map类型Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);String username = userInfo.get(getUsernameParameter());String password = userInfo.get(getPasswordParameter());String rememberMeValue = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);if(!ObjectUtils.isEmpty(rememberMeValue)){request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER,rememberMeValue);}System.out.println("用户名:" + username + " 密码:" + password+" 是否记住我:"+rememberMeValue);UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);setDetails(request, authRequest);//需要保证自定义类中有AuthenticationManagerreturn this.getAuthenticationManager().authenticate(authRequest);} catch (IOException e) {e.printStackTrace();}}return super.attemptAuthentication(request,response);}
}

在自定义SpringSecurity配置中开启Remember

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {//声明数据源@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());return inMemoryUserDetailsManager;}//自定义数据源@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}//将工厂中的AuthenticationManager暴露出来@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
//    自定义filter 交给工厂管理@Beanpublic LoginFilter loginFilter() throws Exception {LoginFilter loginFilter = new LoginFilter();loginFilter.setFilterProcessesUrl("/doLogin");  //指定认证UrlloginFilter.setUsernameParameter("uname");  //指定接收json 用户名 keyloginFilter.setPasswordParameter("passwd");  //指定接收json 密码 keyloginFilter.setRememberMeServices(rememberMeServices());//1.设置认证成功时使用自定义rememberMeService//注入AuthenticationManagerloginFilter.setAuthenticationManager(authenticationManagerBean());//认证成功处理loginFilter.setAuthenticationSuccessHandler((req,resp,authentication)->{Map<String,Object> result = new HashMap<String,Object>();result.put("msg","登录成功");result.put("用户信息",authentication.getPrincipal());resp.setContentType("application/json;charset=UTF-8");resp.setStatus(HttpStatus.OK.value());String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);});//认证失败处理loginFilter.setAuthenticationFailureHandler((req,resp,ex)->{Map<String,Object> result = new HashMap<>();result.put("msg","登录失败:"+ex.getMessage());
//            result.put("status",500);resp.setContentType("application/json;charset=UTF-8");resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);});return loginFilter;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().anyRequest().authenticated()  //所有请求必须认证.and().formLogin().and().rememberMe()  //开启记住我功能 cookie 进行实现 1.认证成功保存记住我 cookie到客户端 2.只有cookie写入客户端成功才能实现自动登录功能.rememberMeServices(rememberMeServices()) //2.设置自动登录使用哪个 rememberMeServices.and().exceptionHandling().authenticationEntryPoint((req,resp,ex)->{resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);resp.setStatus(HttpStatus.UNAUTHORIZED.value());resp.getWriter().println("请认证之后再去处理!");}).and().logout()
//                .logoutUrl("/logout").logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()),new AntPathRequestMatcher("/logout",HttpMethod.GET.name()))).logoutSuccessHandler((req,resp,auth)->{Map<String,Object> result = new HashMap<String,Object>();result.put("msg","注销成功");result.put("用户信息",auth.getPrincipal());resp.setContentType("application/json;charset=UTF-8");resp.setStatus(HttpStatus.OK.value());String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);}).and().csrf().disable();//at:用来某个filter 替换过滤器链中的哪个filter//before:放在过滤器链中哪个filter之前//after:放在过滤器链中哪个filter之后http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);//        http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);}@Beanpublic RememberMeServices rememberMeServices(){return new MyPersistentTokenBasedRememberMeService(UUID.randomUUID().toString(),userDetailsService(),new InMemoryTokenRepositoryImpl());}}

SpringSecurity中“记住我”功能使用及介绍相关推荐

  1. SpringSecurity实现记住我功能

    ⒈表单添加 1 <form action="/authentication/form" method="post"> 2 <table> ...

  2. aida64使用方法_AIDA64中的详细功能使用步骤介绍

    你们知道AIDA64吗?很多的新用户不熟悉AIDA64是怎么使用的?在这里就为你们呈现了AIDA64的详细使用步骤介绍. 1.[使用AIDA64查看电脑简单信息] 打开计算机--系统概述,即可查看计算 ...

  3. linux中bash的功能主要有,Linux系统中的Bash功能的介绍

    今天小编要跟大家分享的文章是关于Linux系统中的Bash功能的介绍.一个完整计算机的体系结构包括:硬件与软件,而软件又分为系统软件与应用软件,负责对硬件仅需管理与操作的是系统软件的内核部分,用户是无 ...

  4. SpringSecurity的remember me记住我功能

    SpringSecurity的remember me记住我功能,即下次访问系统的时候无需再次登录,当使用这个功能的时候SpringSecurity会生成一个令牌,令牌一方面保存在数据库中,另一方面生成 ...

  5. 介绍 Spring 3.1 M1 中的缓存功能

    介绍 Spring 3.1 M1 中的缓存功能- 中文版 (转) Spring 3.1 提供了对已有的 Spring 应用增加缓存的支持,这个特性对应用本身来说是透明的,通过缓存抽象层,使得对已有代码 ...

  6. cpu中计算机的主要功能,cpu主要功能详解介绍

    cpu即中央处理器,是计算机的运算核心和控制核心.下面是学习啦小编为大家介绍cpu的主要功能详解,欢迎大家阅读. cpu的基本介绍 cpu中央处理器(CPU,Central Processing Un ...

  7. 为大家介绍Authorware中的交互功能

    2019独角兽企业重金招聘Python工程师标准>>> 大家都知道其实Authorware软件是一款用于多媒体制作的工具,那么大家知道在Authorware中的交互功能有什么作用吗? ...

  8. 浏览器中 F12 功能的简单介绍

    chrome浏览器中 F12 功能的简单介绍 由于F12是前端开发人员的利器,所以我自己也在不断摸索中,查看一些博客和资料后,自己总结了一下来帮助自己理解和记忆,也希望能帮到有需要的小伙伴,嘿嘿! 首 ...

  9. TFS2018-WIK介绍03-VSTS的特有功能,TFS2018中没有的功能

    VSTS的特有功能,TFS2018中没有的功能 在VSTS上WIKI有分支版本的管理,在TFS2018上,没有分支管理,WIKI只有发布状态的管理. Publish a Git repository ...

最新文章

  1. Ampere 携手 Rigetti 开发混合量子经典计算机
  2. 随机分配效果在task中的应用,公司内部代码片
  3. python简单代码加法-Python tkinter实现简单加法计算器代码实例
  4. effective c++ 学习
  5. C#中的cookie编程
  6. hibernate(nested transactions not supported)异常
  7. 【线段树】Optimal Insertion(CF751E)
  8. 李洪强-C语言5-函数
  9. linux之dd命令
  10. IDEA使用lombok时warn:Generating equals/hashCode implementation but without a call to superclass
  11. java压缩文件太慢_java 解压6万个ZIP文件,如何提升速度?
  12. 封闭解(Closed-form solution)、解析解(Analytical solution)、数值解(Numerical solution) 释义
  13. Nordic nrf 蓝牙 ble 透传应用
  14. #5.2探讨时空同时考虑的相关理论的软肋
  15. 实用的电脑绘图软件——亿图图示
  16. 如何完美解决catia出现-运行异常,单击“确定终止”-问题
  17. ISO26262对软件开发的规定
  18. 基于深度学习的图像识别,实现APP自动打麻将
  19. Qt 错误 The process was ended forcefully.
  20. 【RFID】射频滤波器的设计

热门文章

  1. Java. Warning – Build path specifies execution environment J2SE-1.5
  2. 基于单片机的红外光控灯系统
  3. 【Unity】第一人称控制器
  4. PLC程序实例二:ModBusTCP客户端编程实例与测试方法
  5. 阿里云IoT流转到postgresql数据库方案
  6. quartz2D 如何绘制圆形图片, 及圆环图片
  7. 5-6通过information_schema拿下数据库案例演示
  8. 混乱到清晰-重构之拆离
  9. 魔力宝贝手游版服务器维护,魔力宝贝手游3月20更新维护公告
  10. 【Element】卸载Element