create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${spring.security.remember-me.key}")
private String rememberKey;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置数据源
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.hasRole("USER")
.antMatchers("/app/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
//开启记住我功能
.rememberMe()
.userDetailsService(userDetailsService)
//1.设置加密的key
.key(rememberKey)
//2.持久化令牌方案
.tokenRepository(tokenRepository)
//设置令牌有效期,为7天有效期
.tokenValiditySeconds(60 * 60 * 24 * 7)
.and()
.csrf()
.disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
spring:
datasource:
url: jdbc:mysql://localhost:3306/db-security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
username: root
password: syc
security:
remember-me:
key: yyg
在我们的persistent_logins表中,你会看到保存的自动登录的用户信息。
这样我们就实现了基于持久化令牌的自动登录方案。
在自动登录成功后,也会在remember-me中保存用户的cookie信息,我们可以利用Base64在线编解码工具对该cookie信息进行解码。Base64在线编解码工具直接百度即可找到。
我们看到解码出来的结果以 “:” 分割为前后两部分,冒号前面的部分是我们保存在数据库里的series字段的内容,冒号后面的部分是token字段存储的内容。/**
* @author Luke Taylor
*/
public class PersistentRememberMeToken {
private final String username;
private final String series;
private final String tokenValue;
private final Date date;
......
getter & setter方法略
}
在PersistentTokenRepository接口中,定义了对令牌进行增删改查的4个方法,源码定义如下:public interface PersistentTokenRepository {
void createNewToken(PersistentRememberMeToken token);
void updateToken(String series, String tokenValue, Date lastUsed);
PersistentRememberMeToken getTokenForSeries(String seriesId);
void removeUserTokens(String username);
}
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
PersistentTokenRepository {
// ~ Static fields/initializers
// =====================================================================================
/** 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)";
/** The default SQL used by the <tt>getTokenBySeries</tt> query */
public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
/** The default SQL used by <tt>createNewToken</tt> */
public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
/** The default SQL used by <tt>updateToken</tt> */
public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
/** The default SQL used by <tt>removeUserTokens</tt> */
public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
......
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];
PersistentRememberMeToken token = tokenRepository
.getTokenForSeries(presentedSeries);
if (token == null) {
// No series match, so we can't authenticate using this cookie
throw new RememberMeAuthenticationException(
"No persistent token found for series id: " + presentedSeries);
}
// 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(
messages.getMessage(
"PersistentTokenBasedRememberMeServices.cookieStolen",
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
}
if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System
.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
}
// Token also matches, so login is valid. Update the token value, keeping the
// *same* series number.
if (logger.isDebugEnabled()) {
logger.debug("Refreshing persistent login token for user '"
+ token.getUsername() + "', series '" + token.getSeries() + "'");
}
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) {
logger.error("Failed to update token: ", e);
throw new RememberMeAuthenticationException(
"Autologin failed due to data access problem");
}
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
首先从前端传来的 cookie 中解析出 series 和 token;
根据 series 从数据库中查询出一个 PersistentRememberMeToken 实例;
如果查出来的 token 和前端传来的 token 不相同,说明账号可能被人盗用(别人用你的令牌登录之后,token 会变)。此时根据用户名移除相关的 token,相当于必须要重新输入用户名密码登录才能获取新的自动登录权限。
接下来校验 token 是否过期;
构造新的 PersistentRememberMeToken 对象,并且更新数据库中的 token(这就是我们文章开头说的,新的会话都会对应一个新的 token);
将新的令牌重新添加到 cookie 中返回;
根据用户名查询用户信息,再走一波登录流程。
@RestController
public class UserController {
@GetMapping("/admin/hello")
public String helloAdmin() {
return "hello, admin";
}
@GetMapping("/user/hello")
public String helloUser() {
return "hello, user";
}
@GetMapping("/visitor/hello")
public String helloVisitor() {
return "hello, visitor";
}
@GetMapping("/remember/hello")
public String remember() {
return "hello, remember-me功能";
}
}
@EnableWebSecurity(debug = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Value("${spring.security.remember-me.key}") private String rememberKey; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); http.authorizeRequests() .antMatchers("/admin/**").fullyAuthenticated() //.hasRole("ADMIN") .antMatchers("/user/**") .hasRole("USER") //需要开启remember-me功能才能访问 .antMatchers("/remember/**") .rememberMe() .antMatchers("/visitor/**") .permitAll() .anyRequest() .authenticated() .and() .formLogin() .permitAll() .and() //开启记住我功能 .rememberMe() .userDetailsService(userDetailsService) //1.散列加密方案 .key(rememberKey) //2.持久化令牌方案 .tokenRepository(tokenRepository) //7天有效期 .tokenValiditySeconds(60 * 60 * 24 * 7) .and() .csrf() .disable(); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } }
至此,壹哥 就结合着源码和底层原理,给大家讲解了基于持久化令牌方案实现了自动登录,并且在本案例中给大家介绍了持久化令牌的概念及实现原理,你掌握的怎么样呢?请在评论区给一一哥留言,说说你的感受吧!下一篇文章中,壹哥会给各位讲解 如何在Spring Security环境下实现注销登录,敬请期待哦!文末福利
留言评论,今天的内容你学会了吗?
点赞最多的同学获得水杯一个!

关注公众号,回复【SS】获取专栏大纲脑图
原文作者:Java架构栈;如有侵权,请联系管理员删除。