SpringSecurity入门5---自动登录(RememberMe)

代码地址

实现方式

SpringSecurity提供了两种令牌

  1. 散列算法加密用户必要的登录信息并生成令牌
  2. 数据库等持久性数据存储机制用的持久化令牌

散列加密方式

使用方式很简单,修改配置文件,加入RememberMe即可

protected void configure(HttpSecurity http) throws Exception {
     
        http.authorizeRequests()
                .antMatchers("/css/**", "/img/**", "/js/**", "/bootstrap/**", "/captcha.jpg").permitAll()
                .antMatchers("/app/api/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/myLogin.html")
                .loginProcessingUrl("/login")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .authenticationDetailsSource(myWebAuthenticationDetailsSource)
                .permitAll()
                .and()
                // 添加记住我,需要提供UserDetailService
                .rememberMe().userDetailsService(userDetailService)
                // 使登录页不受限
                .and()
                .csrf().disable();
    }

修改前端代码,添加记住我选择框,name要为remember-me

							<div class="form-group">
                                <label for="captcha">验证码
                                label>
                                <input id="captcha" type="text" class="form-control" name="captcha" required>
                                <img src="/captcha.jpg" alt="captcha" height="50px" width="150px">
                            div>
                            <div class="form-group">
                                <label>
                                    <input type="checkbox" name="remember-me" value="true"> Remember Me
                                label>
                            div>

重启项目登录系统,勾选记住我
SpringSecurity入门5---自动登录(RememberMe)_第1张图片
关闭浏览器后再重新访问路径,可以看到不需要再次登录就可以访问
SpringSecurity入门5---自动登录(RememberMe)_第2张图片
F12打开谷歌浏览器的开发者工具,查看cookie中,除了JSessionId之外还多出了一个Remember-me的值,这个就是令牌
SpringSecurity入门5---自动登录(RememberMe)_第3张图片
生成和验证Token的逻辑在AbstractRememberMeServices中有基本的实现,我们来看一下它的子类TokenBasedRememberMeServices,它有一个方法用于生成Token

protected String makeTokenSignature(long tokenExpiryTime, String username,
			String password) {
     
		String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
		MessageDigest digest;
		try {
     
			digest = MessageDigest.getInstance("MD5");
		}
		catch (NoSuchAlgorithmException e) {
     
			throw new IllegalStateException("No MD5 algorithm available!");
		}

		return new String(Hex.encode(digest.digest(data.getBytes())));
	}

先将用户名、过期时间、密码、key(salt)进行一个MD5加密,再用base64加密用户名+过期时间+上一步md5加密的值。

当验证的时候,使用base64对令牌进行解密获取到用户名、过期时间和加密的散列值,在通过用户名(我们注入了UserDetailService)查询到用户密码,再次正向进行一次散列算法,与之前加密散列值进行对比来判断令牌是否有效。

需要注意的是,我们用到的getKey(),key是在构造函数传入的,通过我们配置的Remember点进去,查找到RememberMeConfigurer,里面对我们的RememberService进行了初始化,传入了Key值,其中的getKey方法如下

private String getKey() {
     
        if (this.key == null) {
     
            if (this.rememberMeServices instanceof AbstractRememberMeServices) {
     
                this.key = ((AbstractRememberMeServices)this.rememberMeServices).getKey();
            } else {
     
                this.key = UUID.randomUUID().toString();
            }
        }

        return this.key;
    }

当我们未指定key的情况下,key会被设置为UUID,也就是说当系统每次重启之后的key值是不一样的,当我们系统重启后,用户之前的RememberMe的令牌肯定就失效了,为了避免这个问题我们可以自己指定key的值

.rememberMe().userDetailsService(userDetailService).key("anntly")

在该类中,当登陆成功后,会刷新我们的令牌并将其放入Cookie中

@Override
	public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication successfulAuthentication) {
     

		String username = retrieveUserName(successfulAuthentication);
		String password = retrievePassword(successfulAuthentication);

		// If unable to find a username and password, just abort as
		// TokenBasedRememberMeServices is
		// unable to construct a valid token in this case.
		if (!StringUtils.hasLength(username)) {
     
			logger.debug("Unable to retrieve username");
			return;
		}

		if (!StringUtils.hasLength(password)) {
     
			UserDetails user = getUserDetailsService().loadUserByUsername(username);
			password = user.getPassword();

			if (!StringUtils.hasLength(password)) {
     
				logger.debug("Unable to obtain password for user: " + username);
				return;
			}
		}

		int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
		long expiryTime = System.currentTimeMillis();
		// 计算过期时间,在AbstractRememberMeServices默认设置的是两周
		expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
		// 生成Token
		String signatureValue = makeTokenSignature(expiryTime, username, password);
		// 放入cookie
		setCookie(new String[] {
      username, Long.toString(expiryTime), signatureValue },
				tokenLifetime, request, response);

		if (logger.isDebugEnabled()) {
     
			logger.debug("Added remember-me cookie for user '" + username
					+ "', expiry: '" + new Date(expiryTime) + "'");
		}
	}

使用非持久化的方式不用使用其他的存储空间,但是只要获取到用户的RememberId之后,把他放入cookie中,在不同的session中也可以进行访问,存在一定的安全隐患,需要保证在记住我功能下对敏感操作的限制,下图即为使用MsEdge浏览器放入令牌后访问路径成功
SpringSecurity入门5---自动登录(RememberMe)_第4张图片

持久化方案

保存两个令牌,series和token,均为MD5散列值,series在用户使用密码重新登录时更新,token在每次创建一个新的session的时候就会重新生成。

在非持久化方案中有一个问题是多个客户端使用同一个token都可以登录,在持久化方案中,每个新的session都会导致token的更新,也就是说token只支持单实例登录。

series不会因为自动登录而更改,当自动登录的时候,会验证series和token两个值,当用户在未使用过自动登录的时候被盗,会刷新token值,此时用户的token已经失效,当用户使用自动登录的时候由于在数据库中存储的和series匹配的token不一致,系统就会腿短令牌是否已经被盗用,作出对应的操作

实现

在SpringSecurity中使用PersistentRememberMeToken封装series和token,对应的表如下,在数据库中创建对应的表,可以看到主键是series,当自动登录解析出series时从数据库查询对应的信息就可以比对登录时的token和数据库的是否一致了

DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins`  (
  `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `series` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `token` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `last_used` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
  PRIMARY KEY (`series`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

修改配置文件

	@Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
     
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        http.authorizeRequests()
                .antMatchers("/css/**", "/img/**", "/js/**", "/bootstrap/**", "/captcha.jpg").permitAll()
                .antMatchers("/app/api/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/myLogin.html")
                .loginProcessingUrl("/login")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .authenticationDetailsSource(myWebAuthenticationDetailsSource)
                .permitAll()
                .and()
                .rememberMe().userDetailsService(userDetailsService()).tokenRepository(jdbcTokenRepository)
                //.rememberMe().userDetailsService(userDetailService).key("anntly")
                // 使登录页不受限
                .and()
                .csrf().disable();
    }

使用默认提供的JdbcTokenRepositoryImpl来获取表的相关信息,如果需要我们也可以自己实现PersistentTokenRepository接口

配置完毕后重启项目,登录,查看生成的remember-me ,并将其使用base64解密

// remember-me
b01hMmlVaXprcnloblFHNFFlcGc1QSUzRCUzRDpta2xTN1ljaEZVMWpoVWNzZmdzekpnJTNEJTNE
// 解密后
oMa2iUizkryhnQG4Qepg5A%3D%3D:mklS7YchFU1jhUcsfgszJg%3D%3D

解密后的值,冒号前半部分就是我们的series,后半部分当然就是token,可以查看我们创建的表中数据是否一致

AbstractRememberMeServices的另外一个实现类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) + "'");
		}
		// series
		final String presentedSeries = cookieTokens[0];
		// token
		final String presentedToken = cookieTokens[1];
		// 根据series获取令牌信息
		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);
		}
		// 当数据库查询出来token和当前token不匹配
		// 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 {
     
			// 刷新token
			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());
	}

当登录成功时令牌的生成

protected void onLoginSuccess(HttpServletRequest request,
		HttpServletResponse response, Authentication successfulAuthentication) {
     
		String username = successfulAuthentication.getName();

		logger.debug("Creating new persistent login for user " + username);
		// 生成令牌
		PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
				username, generateSeriesData(), generateTokenData(), new Date());
		try {
     
			tokenRepository.createNewToken(persistentToken);
			addCookie(persistentToken, request, response);
		}
		catch (Exception e) {
     
			logger.error("Failed to save persistent token ", e);
		}
	}

你可能感兴趣的:(SpringSecurity,java,java,spring)