前言
前面的章节已经介绍了,我们如何quick start一个spring security,然后还有做了一些图片验证之类的功能。还有从数据库中获取我们用户的信息。研究了spring security整个认证的流程。
本章呢,主要讲解如何记录我们的认证信息在下次会话被关闭之后还能够继续使用,而不用重复登录。
本章内容
- remember-me自动登录怎么玩?
- Remember me的源码分析
- 持久化remember me
- 二次校验功能
- remember-me前后端分离
为什么有remember-me?
小黑: 小白啊, 问一个问题,如果让你自己实现remember me功能,你应该怎么实现呢?
小白: 为什么要实现? 直接给cookie
设置过期时间不就好了? 让浏览器将jessionID
保存到电脑上完事, 何必再去实现一个劳什子remember-me? 你这不是给我们后端人员增加工作量么? 你不清楚我们劳动人民的辛苦么? 你....
是啊? 为什么 spring security还要自己实现一个 remember-me 呢? 他有什么考量的地方是我们不知道的呢?
- 不安全. cookie有好多安全问题, 不安全(即使使用了spring security的remember-me也还是会有一点不安全, 但比没有好多了)
- session不可控. 我们无法感知session怎么样
- 接入spring security后会有更多的功能
quick start
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RememberApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(RememberApplication.class, args);
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class IndexController {
@GetMapping("hello")
public String hello() {
return "hello";
}
}
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.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
.rememberMe()
.key("zhazha") // 这里有点像撒盐
.and()
.csrf()
.disable()
.build();
}
}
复制代码
底层都做了什么?
要了解remember-me底层都做了什么非常简单, 只要关注RememberMeServices接口就好
public interface RememberMeServices {
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
void loginFail(HttpServletRequest request, HttpServletResponse response);
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
}
复制代码
小黑: "我们的quick start 就是走的最底下的那个类"
小白: "那我们的remember-me功能在哪调用的呢?"
小黑: "我们核心认证流程的最后一步骤是保存用户登录信息, 这也是我们remember-me功能使用的位置"
// AbstractRememberMeServices
@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
if (!rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
复制代码
小黑: "首先会判断你是否开启了remember-me功能, 然后在调用真正的记住我功能"
// TokenBasedRememberMeServices
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
if (!StringUtils.hasLength(password)) {
UserDetails user = getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
}
// 默认返回两周时间
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
// 拿到当前时间
long expiryTime = System.currentTimeMillis();
// 默认过期时间计算是当前时间之后的两周时间
expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
// 下面函数中的核心代码: String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
// 紧接着将上面的字符串使用md5加密一下
// 其中 getKey() 就是我们配置的 key("zhazha")
String signatureValue = makeTokenSignature(expiryTime, username, password);
// 紧接着将用户名, 过期时间和上面计算的签名三个元素组成一个使用Base64加密的token
// 创建一个 Cookie , 将值保存到cookie中, 并设置好过期时间, 一般是 两周
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request,
response);
}
复制代码
该类的注释都有这段说明
小白: "这里是在认证时调用的remember-me功能, 怎么进行自动登录的呢? "
小黑: "在自动登录时, 通常会将在浏览器中的 cookie 中remember-me相关数据读取出来, 整个过程的开始, 就是下面这个方法"
// RememberMeAuthenticationFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 如果已经登录, 那么就不需要remember-me功能了
if (SecurityContextHolder.getContext().getAuthentication() != null) {
chain.doFilter(request, response);
return;
}
// 这里触发了remember-me功能
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
try {
// 认证授权, 这里明显需要
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
// 将新的认证信息保存到SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(rememberMeAuth);
SecurityContextHolder.setContext(context);
// 认证成功之后执行别的操作, 这个方法没有执行任何代码, 是给程序员自定义实现的
onSuccessfulAuthentication(request, response, rememberMeAuth);
// 持久化SecurityContext
this.securityContextRepository.saveContext(context, request, response);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
if (this.successH