spring security http.rememberMe()使用和原理解析
spring security http.rememberMe()使用和原理解析
转载请贴上本文链接
http.rememberMe()的使用
指定Http记住我参数
使用
//默认为remember-me
http.rememberMe().rememberMeParameter("remeberme");
前端代码
<input type="checkbox" name="remeberme"><span style="color: orange">记住</span>
指定Token识别字段
使用
//默认使用uuid随机数
http.rememberMe().key("123")
修改remember-me的cookie时长
使用
//默认2周(14天)
http.rememberMe().tokenValiditySeconds(24*60*60);
指定 Remember-Me 功能自动登录过程使用的 UserDetailsService 对象
使用
http.rememberMe().userDetailsService(UserDetailsService userDetailsService)
自定义UserDetailsService接口实现实现类
/**
* 作用:
* 这玩意是用来自定义认证用户信息的查找过程的。
* 密码的对比是spring security框架帮我们做了,
* 我们只需要从数据库中找到用户信息,封装成一个UserDetails类return给spring security即可
*
* 过程:
* 实现UserDetailsService,通过loadUserByUsername中传递的username,
* 去数据库中查找用户信息,并封装成UserDetails的对象传递给spring security框架
*/
@Service
public class AppUserDetailService implements UserDetailsService {
//mybatis的mapping(对应用户信息表)
@Resource
private AdminMapping adminMapping;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Admin admin = adminMapping.selectByUsername(username);
if (admin == null){
return null;
}else {
//权限组(设置权限,一个权限字符串)
List<GrantedAuthority> list = AuthorityUtils.createAuthorityList(admin.getRole());
//如果你数据里面的密码是经过bCryptPasswordEncoder加密的,就无需用bCryptPasswordEncoder
User user = new User(admin.getUser ,admin.getPwd(),list);
//User user = new User(admin.getUser ,bCryptPasswordEncoder.encode(admin.getPwd()),list);
return user;
}
}
}
设置始终创建记住我
使用
//没错登录都会创建remenber-me(默认是false)
http.rememberMe().alwaysRemember(true);
使用必须使用安全传输的cookie
使用
//设置后必须https下的cookie才生效
http.rememberMe().useSecureCookie(true);
原理分析
spring security的remember-me功能是通过RememberMeAuthenticationFilter 过滤器实现,通过该过滤器实现自动登录。
ctrl+n:快速进入类
ctrl+f12:查看该类方法
实现原理
JSESSIONID的cookie会在浏览器关闭时消失,而用户再次打开时,是要出现登录的,而remember-me这个cookie是不会在浏览器关闭时销毁(因为设置了cookie时长),而spring security会通过remember-me这个cookie中存储的token值,实现自动登录。后面的会介绍Token 值等于(“username:tokenExpiryTime:password:key”)+MD5加密,所以才能用Token进行重新登录。
当 JSESSIONID 过期后,浏览器中只存在 remember-me 的 Cookie。用户再次请求访问时,由于请求没有携带 JSESSIONID,SecurityContextPersistenceFilter 过滤器无法获取 Session 中的 SecurityContext 对象,也就是说你会认为你没登录认证过。但是如果remember-me 的 Cookie 存在,RememberMeAuthenticationFilter 过滤器会将请求进行拦截,根据 remember-me 存储的 Token 值实现自动登录,并将成功登录后的认证用户信息对象 Authentiacaion 存储到 SecurityContext 中。
情况总结:
- 首次登录,需要认证,如果成功后会生成remember-me,并在JSESSIONID 对应 Session 中存储的 SecurityContext 对象添加认证用户信息对象 Authentiacaion
- (remember-me的cookie未过期的情况)关闭浏览器,再次打开网页,因为关闭浏览器JSESSIONID会销毁,原认证用户信息对象 Authentiacaion无法获取,但是有remember-me,会通过remember-me 存储的 Token 值实现自动登录,并将登录后的认证用户信息对象 Authentiacaion存入现有的Session 中的SecurityContext 。
- (remember-me的cookie过期的情况)关闭浏览器,再次打开网页,情况跟首次登录,需要重新认证,如果成功后会生成remember-me。
首次登录过程
下面我将介绍首次登录中RememberMeServices到底干了什么。
执行流程:
- 认证
- 成功,调用认证成功方法(successfulAuthentication方法),其中包含rememberMeServices
- 失败,调用认证失败方法(unsuccessfulAuthentication方法)
AbstractAuthenticationProcessingFilter
successfulAuthentication
从源码注解中可以得知successfulAuthentication()方法是成功认证的默认行为。
方法步骤:
- 将认证结果注入到Security的上下文中
- 调用rememberMeServices接口的loginSuccess()方法,默认为空实现
- 发布认证成功的事件
- 调用认证成功处理器,默认使用SavedRequestAwareAuthenticationSuccessHandler类去实现
/**
* Default behaviour for successful authentication.
* <ol>
* <li>Sets the successful <tt>Authentication</tt> object on the
* {@link SecurityContextHolder}</li>
* <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
* <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
* <tt>ApplicationEventPublisher</tt></li>
* <li>Delegates additional behaviour to the
* {@link AuthenticationSuccessHandler}.</li>
* </ol>
*
* Subclasses can override this method to continue the {@link FilterChain} after
* successful authentication.
* @param request
* @param response
* @param chain
* @param authResult the object returned from the <tt>attemptAuthentication</tt>
* method.
* @throws IOException
* @throws ServletException
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
//将认证结果注入到Security的上下文中
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
//这个loginSuccess方法是交给rememberMeServices接口的实现类去实现的
//等下在下面的抽象类AbstractRememberMeServices中,会介绍这个方法的实现
//点开this.rememberMeServices,发现默认是new NullRememberMeServices();也就是空实现(即默认不开启rememberMe)
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
//发布认证成功的事件
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
//调用认证成功处理器
//this.successHandler的值是new SavedRequestAwareAuthenticationSuccessHandler();
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
AbstractRememberMeServices
loginSuccess
AbstractRememberMeServices实质是去调用子类的onLoginSuccess()方法,而它的子类是TokenBasedRememberMeServices。
方法步骤:
- 调用子类的onLoginSuccess()方法
@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);
}
TokenBasedRememberMeServices
onLoginSuccess
方法步骤:
- 通过UserDetailsService获得UserDetail
- 计算 Token 的生命周期(默认时长两周)
- 获取 Token 值
- 将 Token 放入 Cookie,传递给浏览器
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
//获取用户名和密码
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
// If unable to find a username and password, just abort as
// TokenBasedRememberMeServices is
// unable to construct a valid token in this case.
if (!StringUtils.hasLength(username)) {
this.logger.debug("Unable to retrieve username");
return;
}
if (!StringUtils.hasLength(password)) {
//通过UserDetailsService获得UserDetail
//如果有自定义UserDetailsService那么就会使用自定义的
UserDetails user = getUserDetailsService().loadUserByUsername(username);
//从UserDetailsService获取的UserDetails中拿到封装的信息
password = user.getPassword();
if (!StringUtils.hasLength(password)) {
this.logger.debug("Unable to obtain password for user: " + username);
return;
}
}
//获取 Token 的生命周期,默认为 (两周)
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
//计算过期时间
// SEC-949
expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
//计算并获取 Token 值
String signatureValue = makeTokenSignature(expiryTime, username, password);
//设置 Cookie,将 Token 传递给浏览器
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request,
response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
}
}
makeTokenSignature
看makeTokenSignature中获取 Token 值,可以发现返回的Token 值应该等于(“username:tokenExpiryTime:password:key”),并且这个值是经过MD5加密的。
/**
* Calculates the digital signature to be put in the cookie. Default value is MD5
* ("username:tokenExpiryTime:password:key")
*/
protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return new String(Hex.encode(digest.digest(data.getBytes())));
}
catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("No MD5 algorithm available!");
}
}
二次登录过程
RememberMeAuthenticationFilter
doFilter
RememberMeAuthenticationFilter重写了doFilter方法,然后调用了类中重载的doFilter方法。
执行步骤:
- 判断当前线程的 SecurityContext 对象是否存储 Authentication 对象
- 当前线程没有对应用户信息,调用 AbstractRememberMeServices 类的 autoLogin() 方法进行自动登录(获取用户信息)
- 调用 ProviderManager 实现类的 authenticate() 方法进行身份认证
- 认证成功后,将 Authentication 对象存储当前线程的 SecurityContext
- 调用本类的认证成功处理
- 发布认证成功的事件
- 调用认证成功的处理器
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//调用了下面的doFilter
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
//判断当前线程的 SecurityContext 对象是否存储 Authentication 对象;
// 如果存在,意味着当前线程已经获取了用户信息,无需再次进行登录
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//当前线程没有对应用户信息,调用 AbstractRememberMeServices 类的 autoLogin() 方法进行自动登录
//其实是获取用户信息
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
// 获取用户信息成功
try {
// 调用 ProviderManager 实现类的 authenticate() 方法进行身份认证
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
//认证成功后,将 Authentication 对象存储当前线程的 SecurityContext
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
//调用本类的认证成功处理,是一个空方法
this.onSuccessfulAuthentication(request, response, rememberMeAuth);
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
if (this.eventPublisher != null) {
//发布认证成功的事件
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
if (this.successHandler != null) {
//调用认证成功的处理器
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
} catch (AuthenticationException var8) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8);
}
//认证失败
this.rememberMeServices.loginFail(request, response);
this.onUnsuccessfulAuthentication(request, response, var8);
}
}
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
chain.doFilter(request, response);
}
}
AbstractRememberMeServices
autoLogin
/**
* Template implementation which locates the Spring Security cookie, decodes it into a
* delimited array of tokens and submits it to subclasses for processing via the
* <tt>processAutoLoginCookie</tt> method.
* <p>
* The returned username is then used to load the UserDetails object for the user,
* which in turn is used to create a valid authentication token.
*/
@Override
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
//获取remember-me的cookie值
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
this.logger.debug("Remember-me cookie detected");
//
if (rememberMeCookie.length() == 0) {
this.logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}
try {
//解码
String[] cookieTokens = decodeCookie(rememberMeCookie);
//从cookie中提取UserDetails
//里面是空实现,交给子类TokenBasedRememberMeServices实现
UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
//将 UserDetails 对象封装到 Authentication 对象里,并返回
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException ex) {
cancelCookie(request, response);
throw ex;
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Remember-me login was valid but corresponding user not found.", ex);
}
catch (InvalidCookieException ex) {
this.logger.debug("Invalid remember-me cookie: " + ex.getMessage());
}
catch (AccountStatusException ex) {
this.logger.debug("Invalid UserDetails: " + ex.getMessage());
}
catch (RememberMeAuthenticationException ex) {
this.logger.debug(ex.getMessage());
}
//获取失败就删除remember-me 对应的 cookie
cancelCookie(request, response);
return null;
}
TokenBasedRememberMeServices
processAutoLoginCookie
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) {
if (cookieTokens.length != 3) {
throw new InvalidCookieException(
"Cookie token did not contain 3" + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}
//获取 Token 过期时间
long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
//判断 Token 是否过期
if (isTokenExpired(tokenExpiryTime)) {
throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)
+ "'; current time is '" + new Date() + "')");
}
// Check the user exists. Defer lookup until after expiry time checked, to
// possibly avoid expensive database call.
//通过 UserDetailsService 对象获取对应用户信息 UserDetails
UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
Assert.notNull(userDetails, () -> "UserDetailsService " + getUserDetailsService()
+ " returned null for username " + cookieTokens[0] + ". " + "This is an interface contract violation");
// Check signature of token matches remaining details. Must do this after user
// lookup, as we need the DAO-derived password. If efficiency was a major issue,
// just add in a UserCache implementation, but recall that this method is usually
// only called once per HttpSession - if the token is valid, it will cause
// SecurityContextHolder population, whilst if invalid, will cause the cookie to
// be cancelled.
//比较 Token 中信息是否和预期的一样,即判断 Token 是否合法
String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
userDetails.getPassword());
if (!equals(expectedTokenSignature, cookieTokens[2])) {
throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
+ "' but expected '" + expectedTokenSignature + "'");
}
//放回
return userDetails;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)