在大多数网站中,都会实现RememberMe这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,下面,主要讲解如何使用Spring Security实现记住我这个功能以及深入源码探究它的原理。
首先,如下图所示为Spring Security实现RememberMe功能的原理图:
首先你进入登录页面,输入用户名以及密码进行登录,通过前几篇文章我们知道了Spring Security首先会进入UsernamePasswordAuthenticationFilter进行验证,如果认证成功它会调用一个叫RememberMeService的类,这个类会调用TokenRepository这个类,而这个类会将一个生成的Token写入数据库中。当你下次再进入该网站的时候,它会直接进入RemeberMeAuthticationFilter这个过滤器中,通过Cookie读取之前的Token,然后拿着这个Token通过RememberService到数据库中查找用户信息,如果存在该Token,则取出该Token对应的用户名以及其他信息放入UserDetailService,然后进入调用的URL。
下面,先来看看如何实现这一功能,最后会通过源码来查看它的实现原理。
Spring Security实现RememberMe功能比较简单。首先定义在配置类中定义一个Repository类进行数据库的读写。实现一个UserServiceDetail类,然后进行相关配置即可。
package cn.shinelon.security.browser;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import cn.shinelon.security.authenticate.ImoocAuthenctiationFailureHandler;
import cn.shinelon.security.authenticate.ImoocAuthenticationSuccessHandler;
import cn.shinelon.security.core.SecurityProperties;
import cn.shinelon.security.core.validate.code.ValidateCodeFilter;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Bean
public PasswordEncoder passwordEncoder() {
return new SCryptPasswordEncoder();
}
@Autowired
private DataSource dataSource;
//记住我后的登录页面
@Autowired
private UserDetailsService userDetailsService;
//记住我的功能
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl();
jdbcTokenRepositoryImpl.setDataSource(dataSource);
//启动时创建一张表,这个参数到第二次启动时必须注释掉,因为已经创建了一张表
// jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);
return jdbcTokenRepositoryImpl;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/autentication/forms")
.and()
.rememberMe()
.tokenRepository(getPersistentTokenRepository())
.tokenValiditySeconds(3600) //Token过期时间为一个小时
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
.antMatchers(
"login.html"
).permitAll() //因为所有的请求都会需要认证处理,所以允许访问这些页面
.antMatchers("/login.html").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable(); //取消放置csrf攻击的防护
}
}
然后就是UserDetailService的实现类:
package cn.shinelon.security.browser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class MyUserDetailServices implements UserDetailsService{
@Autowired
private PasswordEncoder passwordEncoder;
private Logger log=LoggerFactory.getLogger(getClass());
/**
* 从数据库中获取密码与前端提交的密码进行对比
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("用户名:"+username);
//指定用户授权登录信息,这里密码指定了,实际中需要到数据库中验证密码
String password=passwordEncoder.encode("123456");
log.info("数据库中密码是:"+password);
return new User(username, password,
true,true,true,true, //用户被锁定
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
资源文件中连接数据库的信息:
application.properties:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springboot?useUnicode=yes&characterEncoding=UTF8
spring.datasource.username=root
spring.datasource.password=123456
登录页面:
<html>
<head>
<meta charset="UTF-8">
<title>登录title>
head>
<body>
<h1>登录h1>
<form action="/autentication/forms" method="POST">
用户名:<input type="text" name="username"/>br>
密码 :<input type="password" name="password"/>br>
图形验证:<input type="text" name="imageCode" /><img src="/code/image"/>br>
<input type="submit" value="登录"/>
<input type="checkbox" name="remember-me" value="true"/>记住我
form>
body>
html>
至此,就已经配置好RememberMe这个功能,启动项目进入登录页面点击记住我这个选项就会发现数据库中Spring Security自动为创建了一张表并且记录了Token。
然后你可以重新启动项目,然后重新访问你需要的资源,就不会被拦截到登录页面可以直接访问,因为它已经在数据库中存入了Token。
下面,我们来深入源码看看Spring Security到底是怎么实现这一功能的。
关于Spring Security的认证流程不了解的读者可以查看前面的文章,当进行登录的时候,它会进入UsernamePasswordAuthticationFilter进行用户信息验证,验证成功之后它会调用一个AbstractAuthenticationProcessingFilter的过滤器进入一个successfulAuthentication方法中将用户的信息存入session中,然后调用RememberService调用loginSuccess方法,在这个方法中它又会调用rememberMeRequested方法,这个方法返回一个Boolean值,判断是否设置好RemeberMe这个功能。在rememberMeRequested这个方法中它会获取登录页面传送过来的remember-me这个参数,因为我们设置了value=”true”,因此它不为空并且该方法返回true。
上面的方法一路返回到进入一个PersistentTokenBasedRememberMeServices的类中调用TokenRepository将Token存入数据库中,并且将Token存入cookie中。在TokenRepository中调用我们上面实现的JdbcTokenRepositoryImpl bean的createNewToken方法内部实际上使用JdbcTemplate在更新数据库插入了当前Token。
在存入cookie是读取设置的过期参数,然后创建一个cookie通过response响应到浏览器存入cookie中。
当我们重新启动项目访问页面时,由于session被销毁,但是cookie中存在Token,它会到RememberMeAuthenticationFilter中读取session中的值,由于是重启项目,session被销毁,因此session为空,它会去到cookie中取是否存在Token,遍历cookie获取到RememberMe的Token,然后根据Token获取用户信息返回调用的URL信息。
上面就是Spring Security实现RemeberMe功能的原理,关键是掌握功能的原理图,然后查看源码即可清楚的理解它的流程。