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()方法是成功认证的默认行为。

方法步骤:

  1. 将认证结果注入到Security的上下文中
  2. 调用rememberMeServices接口的loginSuccess()方法,默认为空实现
  3. 发布认证成功的事件
  4. 调用认证成功处理器,默认使用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。

方法步骤:

  1. 调用子类的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

方法步骤:

  1. 通过UserDetailsService获得UserDetail
  2. 计算 Token 的生命周期(默认时长两周)
  3. 获取 Token 值
  4. 将 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;
	}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值