1.为什么需要自动登录
当我们在某个网站上注册账号时,网站会对我们设置的登录密码提出要求。例如,有的网站要求使用固定位数的纯数字密码,有的网站则强制要求用户使用英文+数字组合成的密码,甚至要求加一些特殊符号来组成密码。总体而言,设定一个密码并不困难,真正的困难总是在下次登录时才会遇到。要么想不出网站要求的密码格式是什么,要么还原不了设置密码时的思维状态。总之,在几次尝试登录失败之后,大部分人会选择找回密码,从而再次陷入如何设置密码的循环里。为了尽可能减少用户重新登录的频率,在系统开发之初就需要考虑加入可以提升用户登录体验的功能。自动登录便是这样一个会给用户带来便利,同时也会给用户带来风险的体验性功能。自动登录是将用户的登录信息保存在用户浏览器的cookie中,当用户下次访问时,自动实现校验并建立登录态的一种机制。
Spring Security提供了两种非常好的令牌:
- 用散列算法加密用户必要的登录信息并生成令牌。
- 数据库等持久性数据存储机制用的持久化令牌。
散列算法在Spring Security中是通过加密几个关键信息实现的。
2.实现自动登录
(1)散列加密方案
在Spring Security中加入自动登录的功能非常简单。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailService userDetailService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.formLogin()
.and()
.rememberMe().userDetailsService(userDetailService);
}
}
前提是已经实现了一个 UserDetailsService。重启服务后访问受限 API,这次在表单登录页中多了一个可选框。
勾选“Remember me on this computer”可选框(简写为Remember-me),按照正常的流程登录,并在开发者工具中查看浏览器cookie,可以看到除JSESSIONID外多了一个值
这就是Spring Security默认自动登录的cookie字段。在不配置的情况下,过期时间是两个星期。
public abstract class AbstractRememberMeServices implements RememberMeServices,
InitializingBean, LogoutHandler {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";
public static final String DEFAULT_PARAMETER = "remember-me";
public static final int TWO_WEEKS_S = 1209600;
private static final String DELIMITER = ":";
// ~ Instance fields
// ================================================================================================
protected final Log logger = LogFactory.getLog(getClass());
protected final MessageSourceAccessor messages = SpringSecurityMessageSource
.getAccessor();
private UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;
private String cookieDomain;
private String parameter = DEFAULT_PARAMETER;
private boolean alwaysRemember;
private String key;
private int tokenValiditySeconds = TWO_WEEKS_S;
private Boolean useSecureCookie = null;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
protected AbstractRememberMeServices(String key, UserDetailsService userDetailsService) {
Assert.hasLength(key, "key cannot be empty or null");
Assert.notNull(userDetailsService, "UserDetailsService cannot be null");
this.key = key;
this.userDetailsService = userDetailsService;
}
@Override
public void afterPropertiesSet() {
Assert.hasLength(key, "key cannot be empty or null");
Assert.notNull(userDetailsService, "A UserDetailsService is required");
}
/**
* 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) {
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 {
String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
cancelCookie(request, response);
throw cte;
}
catch (UsernameNotFoundException noUser) {
logger.debug("Remember-me login was valid but corresponding user not found.",
noUser);
}
catch (InvalidCookieExc