Spring Boot+Vue项目 微博系统(8):记住我功能配置源码分析

这里再多说下,对于记住我功能,同样可以查看RememberMeConfigurer,看看是怎么实现的:

public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<RememberMeConfigurer<H>, H> {
    private static final String DEFAULT_REMEMBER_ME_NAME = "remember-me";
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    private String key;
    private RememberMeServices rememberMeServices;
    private LogoutHandler logoutHandler;
    private String rememberMeParameter = "remember-me";
    private String rememberMeCookieName = "remember-me";
    private String rememberMeCookieDomain;
    private PersistentTokenRepository tokenRepository;
    private UserDetailsService userDetailsService;
    private Integer tokenValiditySeconds;
    private Boolean useSecureCookie;
    private Boolean alwaysRemember;

且不说别的,就这些类字段就能看到很多信息,比如对应参数名称默认是“remember-me”;类似的也有个RememberMeService,是否总是记住我(alwaysRemember)。。等等。可以按照自己的需求做自定义的配置。该类有个init方法,顾名思义,会做一些初始化操作,我们可以看看记住我的service是怎么操作的。

    public void init(H http) throws Exception {
        this.validateInput();
        String key = this.getKey();
        // 获取service
        RememberMeServices rememberMeServices = this.getRememberMeServices(http, key);
        http.setSharedObject(RememberMeServices.class, rememberMeServices);
        LogoutConfigurer<H> logoutConfigurer = (LogoutConfigurer)http.getConfigurer(LogoutConfigurer.class);
        if (logoutConfigurer != null && this.logoutHandler != null) {
            logoutConfigurer.addLogoutHandler(this.logoutHandler);
        }

        RememberMeAuthenticationProvider authenticationProvider = new RememberMeAuthenticationProvider(key);
        authenticationProvider = (RememberMeAuthenticationProvider)this.postProcess(authenticationProvider);
        http.authenticationProvider(authenticationProvider);
        this.initDefaultLoginFilter(http);
    }

同样地,有个get方法,进一步查看this.getRememberMeServices

    private RememberMeServices getRememberMeServices(H http, String key) throws Exception {
    	// 如果自定义了service
        if (this.rememberMeServices != null) {
            if (this.rememberMeServices instanceof LogoutHandler && this.logoutHandler == null) {
                this.logoutHandler = (LogoutHandler)this.rememberMeServices;
            }

            return this.rememberMeServices;
        } else { // 否则创建service
            AbstractRememberMeServices tokenRememberMeServices = this.createRememberMeServices(http, key);
            tokenRememberMeServices.setParameter(this.rememberMeParameter);
            tokenRememberMeServices.setCookieName(this.rememberMeCookieName);
            if (this.rememberMeCookieDomain != null) {
                tokenRememberMeServices.setCookieDomain(this.rememberMeCookieDomain);
            }

            if (this.tokenValiditySeconds != null) {
                tokenRememberMeServices.setTokenValiditySeconds(this.tokenValiditySeconds);
            }

            if (this.useSecureCookie != null) {
                tokenRememberMeServices.setUseSecureCookie(this.useSecureCookie);
            }

            if (this.alwaysRemember != null) {
                tokenRememberMeServices.setAlwaysRemember(this.alwaysRemember);
            }

            tokenRememberMeServices.afterPropertiesSet();
            this.logoutHandler = tokenRememberMeServices;
            this.rememberMeServices = tokenRememberMeServices;
            return tokenRememberMeServices;
        }
    }

虽然方法、变量都比较长,但是其实是很好懂的,对于没有对应配置的情况下,调用this.createRememberMeServices来创建。再进一步去看:

    private AbstractRememberMeServices createRememberMeServices(H http, String key) {
        return this.tokenRepository == null ? this.createTokenBasedRememberMeServices(http, key) : this.createPersistentRememberMeServices(http, key);
    }

    private AbstractRememberMeServices createTokenBasedRememberMeServices(H http, String key) {
        UserDetailsService userDetailsService = this.getUserDetailsService(http);
        return new TokenBasedRememberMeServices(key, userDetailsService);
    }

    private AbstractRememberMeServices createPersistentRememberMeServices(H http, String key) {
        UserDetailsService userDetailsService = this.getUserDetailsService(http);
        return new PersistentTokenBasedRememberMeServices(key, userDetailsService, this.tokenRepository);
    }

可以看到,这里是根据this.tokenRepository是否为null来选择调用下边两个方法,如果没有设置 tokenRepository,那么就新建一个 TokenBasedRememberMeServices,否则就新建一个PersistentTokenBasedRememberMeServices,从名字就可以看出来,前者是非持久化的,后者是持久化的。这两者都间接实现了RememberMeServices接口。
在这里插入图片描述
从源码也能看出来,对于前者,查看它的onLoginSuccess方法(这是RememberMeServices接口中的方法,用于登录成功时执行记住我的操作):

    public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = this.retrieveUserName(successfulAuthentication);
        String password = this.retrievePassword(successfulAuthentication);
		// ...省略无关代码
        int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
        long expiryTime = System.currentTimeMillis();
        expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
        String signatureValue = this.makeTokenSignature(expiryTime, username, password);
        this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);   
    }

对于这种非持久化的方案,它是把用户名、失效时间、签名信息等编码到Cookie中,那么以后通过这个Cookie就可以登录了,相对来说是比较简单易用的。不过这里是把用户名直接编码到Cookie中的,对于不想泄露用户名的应用来说可能安全性不够,(当然通过Cookie/Session这种方式也不是绝对安全的,可以自行了解更多更安全的认证方式)。

查看PersistentTokenBasedRememberMeServices的持久化实现方法:

    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

        try {
            this.tokenRepository.createNewToken(persistentToken);
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }

可以看到这里也使用了用户名username,但是这里是先执行了this.tokenRepository.createNewToken(persistentToken);,再把persistentToken添加到Cookie中。如果使用过JPA的同学应该知道这个xxxRepository就是做持久化工作的,比如访问数据库啥的。查看这个createNewToken方法,发现其是PersistentTokenRepository接口的一个方法,该方法有两个默认实现:
在这里插入图片描述

顾名思义,一个是持久化到内存,一个是持久化到数据库。这取决于我们的配置,因为如果我们选择持久化方案,就得配置一个tokenRepository,因为前边看过了,只有在配置中这个参数不为空才会选择持久化方案,而为空就会采用非持久化方案。

其实这里可以自己看下基于内存的实现,其实就是建立一个HashMap保存而已,没什么神奇的。

	private final Map<String, PersistentRememberMeToken> seriesTokens = new HashMap();
    
    public synchronized void createNewToken(PersistentRememberMeToken token) {
        PersistentRememberMeToken current = (PersistentRememberMeToken)this.seriesTokens.get(token.getSeries());
        if (current != null) {
            throw new DataIntegrityViolationException("Series Id '" + token.getSeries() + "' already exists!");
        } else {
            this.seriesTokens.put(token.getSeries(), token);
        }
    }

对于持久化到数据库的实现(JdbcTokenRepositoryImpl)其实更简单,就是执行一条SQL:

    public void createNewToken(PersistentRememberMeToken token) {
        this.getJdbcTemplate().update(this.insertTokenSql, new Object[]{token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate()});
    }

查看这个类的类字段部分:

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
    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 = ?";
    private String tokensBySeriesSql = "select username,series,token,last_used from persistent_logins where series = ?";
    private String insertTokenSql = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
    private String updateTokenSql = "update persistent_logins set token = ?, last_used = ? where series = ?";
    private String removeUserTokensSql = "delete from persistent_logins where username = ?";
    private boolean createTableOnStartup;

可以看到它内置了一些SQL语句,提供对数据库表的增删改查操作,并且提供了一个boolean createTableOnStartup,表示是否在启动时新建对应数据库表。

其实这里还有个疑问,就是前边说的,持久化方案也会保存用户名,并且也是写入Cookie,看起来它相比非持久化方式,非但没有提供更多的安全性保证,反而多了一些数据库操作。来看来它具体是怎么做的,回到PersistentTokenBasedRememberMeServicesonLoginSuccess方法:

    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

        try {
            this.tokenRepository.createNewToken(persistentToken);
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }

这里它把用户名(username)、当前时间(new Date())还有两个序列数(this.generateSeriesData(), this.generateTokenData())封装为PersistentRememberMeToken对象,随后调用this.addCookie将其放入Cookie中,查看这个addCookie方法:

    private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
        this.setCookie(new String[]{token.getSeries(), token.getTokenValue()}, this.getTokenValiditySeconds(), request, response);
    }

可以看到,这里并没有放对象中的用户名,只放了对象中的两个序列数,这也就防止了用户名在Cookie中来回传递。认证的时候,只需要借助这两个序列数查对应的数据库表,就能得到用户名和过期时间,进而达到记住我的功能。

总结
记住我功能使用Cookie来实现,默认使用的是非持久化方案,它会把用户名写在Cookie里。如果需要持久化方案,就需要配置一个PersistentTokenRepository,这里对于持久化方案有两种选择:基于内存和基于数据库。基于内存的方案是建立一个HashMap来保存,基于数据库的是新建一个专用表。持久化方案会生成序列数,把序列数保存在Cookie中,验证时通过序列数查数据库表 来得到用户名。

此外,官方文档也对记住我功能有很详细的描述
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-rememberme

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值