当用户登陆发起认证请求时,会经过UsernamePasswordAuthenticationFilter
进行用户认证,认证成功以后,SpringSecurity 调用前期配置好的记住我功能,实际是调用了RememberMeService
接口,其接口的实现类会将用户的信息生成Token
并将它写入 response 的Cookie
中,在写入的同时,内部的TokenRepositoryTokenRepository
会将这份Token
再存入数据库一份。html
当用户再次访问服务器资源的时候,首先会通过RememberMeAuthenticationFiler
过滤器,在这个过滤器里面会读取当前请求中携带的 Cookie,这里存着上次服务器保存 的Token
,而后去数据库中查找是否有相应的 Token,若是有,则再经过UserDetailsService
获取用户的信息。前端
从图中能够得知记住个人过滤器在过滤链的中部,注意是在UsernamePasswordAuthenticationFilter
以后。java
在 html 中增长记住我复选框checkbox控件,注意其中复选框的name
必定必须为remember-me
git
<input type="checkbox" name="remember-me" value="true"/>
复制代码
本例中使用了 springboot 管理的数据库源,因此注意要配置spring-boot-starter-jdbc
的依赖:github
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
复制代码
若是不配置会报编译异常:spring
The type org.springframework.jdbc.core.support.JdbcDaoSupport cannot be resolved. It is indirectly referenced from required .class files
复制代码
记住个人安全认证配置:sql
@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()
方法进行开启自动建表操做,可是不建议使用。springboot
当成功登陆以后,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());
}
}
复制代码
我的博客:woodwhale's blog
博客园:木鲸鱼的博客