目录
3.1.processAutoLoginCookie 解码认证方法
5.3 JdbcTokenRepositoryImpl 源码
一、介绍
默认使用的remebermeService 的实现类 TokenBasedRememberMeServices 的 他做的比较简单,对用户名 、过期时间+生成令牌 进行Base64的加密,在放入cookie,还又就是解码比较认证,没有什么安全措施,如果cookie被外部拦截到,那么就会肆无忌惮的进行攻击服务器,是比较不安全的。spring Security 也给我们提供了比较安全的实现方式,使用 PersistentTokenBasedRememberMeServices 进行remeberme的功能实现。
PersistentTokenBasedRememberMeServices 提升安全性的点:
- cookie 里只存放了两个数据 一个 series和 token ,没有存放用户名等信息,将主要信息存放到了服务器中。
- 认证成功后返回cookie 值 是 series 和 token ,两个值是通过Base64 编码后的。
- 在jsession过期后,认证成功后,每次都会生成新的 token 和 date ,并且更新到前端,从而提升安全性。
二、原理分析
- 不同 TokenBasedRemornberMeServices 中的 processAutologinCookie 方法,这里cookieTokens 数组的长度为2,第一项是servies,第二项是token 。
- 从cookieTokens 数组中分别提取出series 和 token ,然后根据 series 去内存中查询出一个 PersistentRememberMeToken 对象。如果查询出来的对象为null,表示内存中并没有series 对应的值,本次自动登录失败。如果查询出来的token和从 cookieTokens 中解析出来的 token不相同,说明自动登录令牌已经泄露(恶意用户利用令牌登录后,内存中的token变了),此时移除当前用户的所有自动登录记录并抛出异常。
- 根据数据库中查询出来的结果判断令牌是否过期,如果过期就抛出异常。
- 生成一个新的PersistentRememberMeToken 对象,用户名和 series 不变,token 重新生成,date 也是使用当前时间。newToken生成后,根据series 去修改内存中的token 和 date (即每次自动登录后都会产生新的 token 和 date)
- 调用 addCookie 方法添加 Cookie,在addCookie 方法中,会调用到我们前面所说的setCookie方法,但是要注意第一个数组参数中只有两项:series和 token (即返回到前端的令牌是通过对series 和 token 进行 Base64 编码得到的)
- 最后将根据用户名查询用户对象并返回。
三、源码详情
3.1.processAutoLoginCookie 解码认证方法
protected UserDetails processAutoLoginCookie(String[] cookieTokens,
HttpServletRequest request, HttpServletResponse response) {
// 判断当前的cookie的 长度 里面应该是series 和 token
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2
+ " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}
// 获取 series 和 token
final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];
//根据series 获取认证对象
/**
这种方式是将生成敏感信息都写入到内存当中。
如果只是到这里的话那么只需要不破坏cookie,最终还是会认证成功。
在这方式他会在认证成功后重新生成一个令牌并去更新之前的令牌
*/
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);
}
// 拿着传过来的 series 和 解码后的series去比较
// 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() + "'");
}
// 生成新的 token
PersistentRememberMeToken newToken = new PersistentRememberMeToken(
token.getUsername(), token.getSeries(), generateTokenData(), new Date());
try {
// 进行series 的更新
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());
}
对以上代码的剖析
- PersistentRememberMeToken 对象
- 他就是一个实体对象,存放这认证的信息 和时间信息
-
public class PersistentRememberMeToken { private final String username; private final String series; private final String tokenValue; private final Date date; public PersistentRememberMeToken(String username, String series, String tokenValue, Date date) { this.username = username; this.series = series; this.tokenValue = tokenValue; this.date = date; } public String getUsername() { return username; } public String getSeries() { return series; } public String getTokenValue() { return tokenValue; } public Date getDate() { return date; } }
- getTokenForSeries 根据series获取认证对象
- 默认调用的 InMemoryTokenRepositoryImpl 基于内存
- 其实基于内存的就是将数据放到一个Map中,根据 series 从map中取PersistentRememberMeToken 对象
-
private final Map<String, PersistentRememberMeToken> seriesTokens = new HashMap<>(); public synchronized PersistentRememberMeToken getTokenForSeries(String seriesId) { return seriesTokens.get(seriesId); }
- removeUserTokens 比较不通过是移除token
- 就是从map中移除
-
public synchronized void removeUserTokens(String username) { Iterator<String> series = seriesTokens.keySet().iterator(); while (series.hasNext()) { String seriesId = series.next(); PersistentRememberMeToken token = seriesTokens.get(seriesId); if (username.equals(token.getUsername())) { series.remove(); } } }
-
生成 token 和 series 的方法
- 通过 random 对 byte 数组处理,在通过Base64 编码。
-
protected String generateSeriesData() { // 16位 byte[] newSeries = new byte[seriesLength]; random.nextBytes(newSeries); return new String(Base64.getEncoder().encode(newSeries)); } protected String generateTokenData() { // 16 位 byte[] newToken = new byte[tokenLength]; random.nextBytes(newToken); return new String(Base64.getEncoder().encode(newToken)); }
- 生成 token 和 series 的方法
- 通过 random 对 byte 数组处理,在通过Base64 编码。
-
protected String generateSeriesData() { // 16位 byte[] newSeries = new byte[seriesLength]; random.nextBytes(newSeries); return new String(Base64.getEncoder().encode(newSeries)); } protected String generateTokenData() { // 16 位 byte[] newToken = new byte[tokenLength]; random.nextBytes(newToken); return new String(Base64.getEncoder().encode(newToken)); }
- updateToken 更新内存中的 认证信息
- 根据series进行map中替换
-
public synchronized void updateToken(String series, String tokenValue, Date lastUsed) { PersistentRememberMeToken token = getTokenForSeries(series); PersistentRememberMeToken newToken = new PersistentRememberMeToken( token.getUsername(), series, tokenValue, new Date()); // Store it, overwriting the existing one. seriesTokens.put(series, newToken); }
- addCookie 添加到cookie里
-
// 添加cookie private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) { // 调用方法进行设置 setCookie(new String[] { token.getSeries(), token.getTokenValue() }, getTokenValiditySeconds(), request, response); } /** * Sets the cookie on the response. * * By default a secure cookie will be used if the connection is secure. You can set * the {@code useSecureCookie} property to {@code false} to override this. If you set * it to {@code true}, the cookie will always be flagged as secure. By default the cookie * will be marked as HttpOnly. * * @param tokens the tokens which will be encoded to make the cookie value. * @param maxAge the value passed to {@link Cookie#setMaxAge(int)} * @param request the request * @param response the response to add the cookie to. */ protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) { // 将值进行编码 String cookieValue = encodeCookie(tokens); Cookie cookie = new Cookie(cookieName, cookieValue); // 设置存活时间 默认是 2周 cookie.setMaxAge(maxAge); // 设置cookie路径 cookie.setPath(getCookiePath(request)); // 区域 if (cookieDomain != null) { cookie.setDomain(cookieDomain); } // 判断存活时间 if (maxAge < 1) { cookie.setVersion(1); } if (useSecureCookie == null) { cookie.setSecure(request.isSecure()); } else { cookie.setSecure(useSecureCookie); } cookie.setHttpOnly(true); response.addCookie(cookie); }
-
3.2 onLoginSuccess 生成cookie 方法
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
logger.debug("Creating new persistent login for user " + username);
// 调用本类的 生成 series 和 token 的方法 直接创建对象
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
// 调用创建方法根据 series 放入map 中
tokenRepository.createNewToken(persistentToken);
// 添加cookie 写回操作
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
底层代码解析
- createNewToken 放入到map中
-
private final Map<String, PersistentRememberMeToken> seriesTokens = new HashMap<>(); public synchronized void createNewToken(PersistentRememberMeToken token) { PersistentRememberMeToken current = seriesTokens.get(token.getSeries()); if (current != null) { throw new DataIntegrityViolationException("Series Id '" + token.getSeries() + "' already exists!"); } seriesTokens.put(token.getSeries(), token); }
以上就是PersistentTokenBasedRememberMeServices 的核心源码的分析和讲解 。
四、基于内存的令牌实现
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置安全策略
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/index").permitAll()
.anyRequest()
.authenticated()
.and()
.rememberMe() // 开启rememberMe 功能
.rememberMeServices(rememberMeServices()) // 更换记住我的方式的Service 层
// .alwaysRemember(true) // 总是记住我
.key(UUID.randomUUID().toString()) // 自定义 key 值
.and()
.formLogin()
.and()
.csrf()
.disable();
}
/**
* 创建基于安全性高的
*/
@Bean
public RememberMeServices rememberMeServices (){
// 三个参数:1.key 2. 数据库方式 3.认证信息的存储方式
return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),userDetailsService(),new InMemoryTokenRepositoryImpl());
}
五、持久化令牌
默认的 PersistentTokenBasedRememberMeServices 是基于内存的,服务器已宕机数据就会丢失,安全性和容错率不太好,需要将他更换为基于数据库的实现。
5.1.结构分析
默认是基于内存,但是我们可以通过自定义 PersistentTokenBasedRememberMeServices 通过他的构造方法来替换成基于数据库的
5.2 PersistentTokenRepository
PersistentTokenRepository 是一个接口,它的作用就是存取 生成的 认证信息的。
public interface PersistentTokenRepository {
// 添加 认证信息
void createNewToken(PersistentRememberMeToken token);
// 修改认证信息
void updateToken(String series, String tokenValue, Date lastUsed);
// 获取认证信息
PersistentRememberMeToken getTokenForSeries(String seriesId);
// 移除认证信息
void removeUserTokens(String username);
}
他的实现有基于内存和 数据库的两个实现类
5.3 JdbcTokenRepositoryImpl 源码
这个类已经将类的结构的sql写好,直接拿来用就可以 。默认是通过 JDBCTemplate进行实现的,所以需要引入对应的 jdbc 相关依赖。
/**
* JDBC based persistent login token repository implementation.
*
* @author Luke Taylor
* @since 2.0
*/
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 = ?";
// ~ Instance fields
// ================================================================================================
private String tokensBySeriesSql = DEF_TOKEN_BY_SERIES_SQL;
private String insertTokenSql = DEF_INSERT_TOKEN_SQL;
private String updateTokenSql = DEF_UPDATE_TOKEN_SQL;
private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL;
private boolean createTableOnStartup;
protected void initDao() {
if (createTableOnStartup) {
getJdbcTemplate().execute(CREATE_TABLE_SQL);
}
}
public void createNewToken(PersistentRememberMeToken token) {
getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(),
token.getTokenValue(), token.getDate());
}
public void updateToken(String series, String tokenValue, Date lastUsed) {
getJdbcTemplate().update(updateTokenSql, tokenValue, lastUsed, series);
}
/**
* Loads the token data for the supplied series identifier.
*
* If an error occurs, it will be reported and null will be returned (since the result
* should just be a failed persistent login).
*
* @param seriesId
* @return the token matching the series, or null if no match found or an exception
* occurred.
*/
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
try {
return getJdbcTemplate().queryForObject(tokensBySeriesSql,
(rs, rowNum) -> new PersistentRememberMeToken(rs.getString(1), rs
.getString(2), rs.getString(3), rs.getTimestamp(4)), seriesId);
}
catch (EmptyResultDataAccessException zeroResults) {
if (logger.isDebugEnabled()) {
logger.debug("Querying token for series '" + seriesId
+ "' returned no results.", zeroResults);
}
}
catch (IncorrectResultSizeDataAccessException moreThanOne) {
logger.error("Querying token for series '" + seriesId
+ "' returned more than one value. Series" + " should be unique");
}
catch (DataAccessException e) {
logger.error("Failed to load token for series " + seriesId, e);
}
return null;
}
public void removeUserTokens(String username) {
getJdbcTemplate().update(removeUserTokensSql, username);
}
/**
* Intended for convenience in debugging. Will create the persistent_tokens database
* table when the class is initialized during the initDao method.
*
* @param createTableOnStartup set to true to execute the
*/
public void setCreateTableOnStartup(boolean createTableOnStartup) {
this.createTableOnStartup = createTableOnStartup;
}
}
5.4 使用步骤
1.引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
2.配置数据源
spring.thymeleaf.cache=false
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?
characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.entity
3.配置持久化令牌
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 数据源
*/
private final DataSource dataSource;
public WebSecurityConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 定义数据库访问层
* @return
*/
@Override
protected UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager ;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
/**
* 配置安全策略
* 启动项⽬并查看数据库
* 注意::启动项⽬会⾃动创建⼀个表,,⽤来保存记住我的 token token 信息
* 再次测试记住我
* 在测试发现即使服务器重新启动,依然可以⾃动登录。
* ⾃定义记住我
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/index").permitAll()
.mvcMatchers("/test").rememberMe() // 设置指定路径支持记住我
.anyRequest()
.authenticated()
.and()
.rememberMe() // 开启rememberMe 功能
// .rememberMeServices(rememberMeServices()) // 更换记住我的方式的Service 层
.tokenRepository(persistentTokenRepository()) // 修改底层数据源默认就会修改 remeberMeService
// .alwaysRemember(true) // 总是记住我
.key(UUID.randomUUID().toString()) // 自定义 key 值
.and()
.formLogin()
.and()
.csrf()
.disable();
}
/**
* 创建基于安全性高的 基于内存
*/
@Bean
public RememberMeServices rememberMeServices (){
// 三个参数:1.key 2. 数据库方式 3.认证信息的存储方式
return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),userDetailsService(),new InMemoryTokenRepositoryImpl());
}
/**
* 基于持久化方式
*/
@Bean
public PersistentTokenRepository persistentTokenRepository (){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
jdbcTokenRepository.setCreateTableOnStartup(true); // 是否启动时创建表结构,建议关闭,因为每次都会重新创建
return jdbcTokenRepository;
}
以上就是 全部内容!!