栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Python

Spring Security系列教程16--基于持久化令牌方案实现自动登录

Python 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Spring Security系列教程16--基于持久化令牌方案实现自动登录

前言

在上一章节中,一一哥 带各位基于散列加密方案实现了自动登录,并且给各位介绍了散列加密算法,其实还有另一种自动登录的实现方案,也就是基于持久化令牌方案来进行实现。接下来请跟 一一哥 学习这种方案该怎么实现吧。

一. 持久化令牌方案简介

1. 持久化令牌方案

有的小伙伴会问,既然我们要基于持久化令牌来实现自动登录,那啥是持久化令牌啊?所以 一一哥 先给大家做个概念解释。

所谓的持久化令牌的实现方案,其实在交互上与我们前面章节讲的散列加密方案一致,只不过是在用户勾选Remember-me之后,将生成的token令牌发送到用户浏览器,并在用户下次访问系统时读取该令牌进行认证。不同的是,持久化令牌方案采用了更加严谨的安全性设计,也就是安全性更好一些。

在持久化令牌方案中,最核心的是series和token两个值,这两个值都是用MD5散列计算生成的随机字符串。不同的是,series仅在用户使用密码重新登录时更新,而 token 会在每一个新的session会话中都重新生成。

2. 持久化令牌方案优点

我们前面已经学习了基于散列算法的自动登录方案了,为啥还要再学习持久化方案呢?肯定是因为它有独特之处吧。

首先,持久化令牌方案 避免了散列加密方案中,一个令牌可以同时在多端登录的问题,这是因为每个session会话都会引发token的更新,即每个token仅支持单实例登录。

其次,自动登录不会导致series变更,但每次自动登录都需要同时验证 series和 token两个值,所以这样的设计会更安全。因为当该令牌还未使用过自动登录就被盗取时,系统会在非法用户验证通过后刷新 token 值,此时在合法用户的浏览器中,该token值已经失效。当合法用户使用自动登录时,由于该series对应的 token 不同,系统可以推断该令牌可能已被盗用,从而做一些处理。例如,清理该用户的所有自动登录令牌,并通知该用户可能已被盗号等。

了解了持久化令牌的概念和优点之后,接下来就跟着 壹哥 进行代码实现吧。

二. 持久化令牌方案的代码实现

1. 创建persistent_logins表

在持久化令牌方案中,是要把令牌进行持久化保存的,那么把令牌持久化到哪里去呢?我们首选数据库!

所以我们首先创建一张persistent_logins表,用来存储我们自动登录时生成的持久化令牌信息,该表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)

在该表中,series是主键,可以根据series进行令牌信息的查询等操作。

2. 配置SecurityConfig类

请按之前的方式创建一个新的项目module,具体过程略。

首先我们创建SucurityConfig配置类,在configure(HttpSecurity http)方法中通过tokenRepository()方法关联JdbcTokenRepositoryImpl,进而对persistent_logins表进行增删改查。

@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
public class PersistentRememberMeToken {
	private final String username;
	private final String series;
	private final String tokenValue;
	private final Date date;

	......
    getter & setter方法略    
}

会发现在该源码中,有series和tokenValue字段,可以分别存储persistent_logins表中的series和token字段内容。

3. PersistentTokenRepository

Spring Security之所以可以实现令牌的持久化存储,主要是基于PersistentTokenRepository接口,该接口的父子类关系图如下:

在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);

}    

4. JdbcTokenRepositoryImpl实现类

JdbcTokenRepositoryImpl是对PersistentTokenRepository接口的具体实现,该实现类的实现方法其实很简单,就是定义了5个SQL语句,分别是建表语句,以及对持久化令牌表的增删改查操作的SQL语句,源码如下:

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
		PersistentTokenRepository {
	// ~ Static fields/initializers
	// =====================================================================================

	
	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)";
	
	public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
	
	public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
	
	public static final String DEF_UPDATe_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
	
	public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
    
    ......
    

然后就是利用Spring自带的JdbcTemplate来实现对“persistent_logins”表的增删改查操作。

5. PersistentTokenbasedRememberMeServices类

我们再看看另一个带有记住我功能的持久化令牌服务类PersistentTokenbasedRememberMeServices,在该类中有一个处理自动登录的重要方法processAutoLogincookie(),源码如下:

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());
	}

以上源码的实现逻辑如下:

  1. 首先从前端传来的 cookie 中解析出 series 和 token;
  2. 根据 series 从数据库中查询出一个 PersistentRememberMeToken 实例;
  3. 如果查出来的 token 和前端传来的 token 不相同,说明账号可能被人盗用(别人用你的令牌登录之后,token 会变)。此时根据用户名移除相关的 token,相当于必须要重新输入用户名密码登录才能获取新的自动登录权限。
  4. 接下来校验 token 是否过期;
  5. 构造新的 PersistentRememberMeToken 对象,并且更新数据库中的 token(这就是我们文章开头说的,新的会话都会对应一个新的 token);
  6. 将新的令牌重新添加到 cookie 中返回;
  7. 根据用户名查询用户信息,再走一波登录流程。

四. 两种自动登录实现方案对比

至此,我已经给大家详细讲解了基于散列加密方案和持久化令牌方案的自动化登录实现,这里我对两种方案做一个简单对比。

散列加密方案和持久化令牌方案,这两种方案都是把信息存储在cookie中,所以都有被盗取用户身份信息的可能性,当然持久化令牌方案的安全性更高一些。但是如果要你追求最安全的方式,那就尽量不要实现自动登录功能,所以我们要在用户体验和提高安全性之间选择平衡点。

如果我们一定要实现自动登录功能,可以限制以cookie身份登录时的部分执行权限,比如在修改密码、修改邮箱(防止找回密码)、查看隐私信息(如完整的手机号码、银行卡号等)时,我们可以进一步校验用户的登录密码,或者设置独立密码来做二次校验,以提高安全性。

五. 二次校验功能的实现

我们上面虽然讲解了2种自动登录的实现方案,但是依然存在用户身份被盗用的问题,这个问题其实是很难完美解决。那么我们能做的,只能是当发生用户身份被盗用这样的事情时,将损失降低到最小。因此,我们采用二次校验来增强项目的安全性。

1. 定义新的测试接口

我们在上面项目的基础上,添加一个新的测试接口/remember。

@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功能";
    }

}

2. 配置二次校验权限

在SecurityConfig类中对/remember/hello接口配置二次校验,主要是对该接口利用rememberMe()方法进行配置。

@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();
    }
    
}

当我们访问"/remember/hello"接口时,需要二次校验才能访问,即该接口需要开启remember-me功能才能访问:

至此,壹哥 就结合着源码和底层原理,给大家讲解了基于持久化令牌方案实现了自动登录,并且在本案例中给大家介绍了持久化令牌的概念及实现原理,你掌握的怎么样呢?请在评论区给 一一哥 留言,说说你的感受吧!下一篇文章中,壹哥 会给各位讲解 如何在Spring Security环境下实现注销登录,敬请期待哦!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/313373.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号