【1】基于会话技术的实现
也就是基于Cookie的实现,用户信息通过某种规则进行加密然后生成一个字符串设置为cookie。
① 登录页面
这里name="remember-me"表示“记住我”的复选框,默认key是remember-me
。
<form action="/user/login" method="post"> <input type="text" name="username" /> <input type="text" name="password" /> <input type="checkbox" name="remember-me" /> <input type="submit" /> </form>
② 配置类开启记住我功能
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(password()); } @Bean PasswordEncoder password() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling().accessDeniedPage("/unauth.html"); http.formLogin() .loginPage("/login.html") //登录页面 .loginProcessingUrl("/user/login") // 处理登录的请求 .defaultSuccessUrl("/test/index",true)// 登录成功后跳转路径 .permitAll(); http.authorizeRequests() .antMatchers("/static/**","/images/**","/css/**","/js/**")//可以直接放行 .permitAll() .antMatchers("/findAll").hasAuthority("admin") // 用户访问findAll 必须有 admin 权限 .antMatchers("/find").hasAnyAuthority("admin","sale") // 用户访问 find ,拥有admin或者sale之一即可 .antMatchers("/sale/**").hasRole("sale") // 需要用户具有sale角色 .antMatchers("/product/**").hasAnyRole("admin","product") //用户具有admin或者product角色之一即可 .anyRequest().authenticated();//其他请求都需要认证 http.csrf().disable(); //开启记住我 http.rememberMe(); } }
如下所示,提交表单时请求参数会带上remember-me: on
:
提交表单后响应头会设置cookie-remember-me,默认是14天有效期。其值加密规则如下所示:
username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
从这个规则也可以看出,如果用户修改了密码那么记住我功能将会自动失效。
得到signatureValue规则如下所示(Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
):
得到最终cookie值源码如下:
protected String encodeCookie(String[] cookieTokens) { StringBuilder sb = new StringBuilder(); for(int i = 0; i < cookieTokens.length; ++i) { try { sb.append(URLEncoder.encode(cookieTokens[i], StandardCharsets.UTF_8.toString())); } catch (UnsupportedEncodingException var5) { this.logger.error(var5.getMessage(), var5); } if (i < cookieTokens.length - 1) { sb.append(":"); } } String value = sb.toString(); sb = new StringBuilder(new String(Base64.getEncoder().encode(value.getBytes()))); while(sb.charAt(sb.length() - 1) == '=') { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); }
.
此时关闭浏览器再重新打开,可直接访问受保护的请求(请求头的Cookie会带上remember-me)。
其本质就是根据某种规则生成一个加密串(串中有用户名和密码)设置为cookie,再次请求时带上该cookie。在autoLogin方法中会解密该cookie,然后根据解密中的用户信息与数据库的数据进行对比来实现自动登录。
原理部分可以查阅一下RememberMeConfigurer、TokenBasedRememberMeServices与AbstractRememberMeServices三个类。
AbstractRememberMeServices关于自动登录的处理逻辑如下:
这种方式无疑是不安全的,将用户信息通过某种加密规则生成字符串保存到浏览器是有几率被逆向的。故而spring官方推荐使用基于数据库的实现如PersistentTokenBasedRememberMeServices。
【2】基于数据库的实现
AbstractRememberMeServices的另外一个实现是PersistentTokenBasedRememberMeServices,也就是持久化token实现。其核心支撑PersistentTokenRepository又有两个实现:基于内存的InMemoryTokenRepositoryImpl和基于数据库的JdbcTokenRepositoryImpl。前者是通过维护了private final Map<String, PersistentRememberMeToken> seriesTokens = new HashMap();这样一个map来实现,后者则依赖于数据库表。
在其源码中可以看到,默认表名与列都已定义persistent_logins (username, series, token, last_used) ,并提供了创建表的策略(如果createTableOnStartup为true)。
如下所示,注入PersistentTokenRepository 。
@Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); // 赋值数据源 jdbcTokenRepository.setDataSource(dataSource); // 自动创建表,第一次执行会创建,以后要执行就要删除掉! jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; }
开启记住我功能并设置tokenRepository。
@Autowired private PersistentTokenRepository tokenRepository; // 开启记住我功能 http.rememberMe() .tokenRepository(tokenRepository) .userDetailsService(usersService);
CREATE TABLE `persistent_logins` ( `username` varchar(64) NOT NULL, `series` varchar(64) NOT NULL, `token` varchar(64) NOT NULL, `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`series`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
每次自动登录后,会根据series重新生成token并更新:
其原理示意图如下所示: