springSecurity的两种rememberMe方式

Posted by hcy on October 30, 2019

springSecurity的两种rememberMe方式

1
springSecurity 使用RememberMeAuthenticationFilter处理记住我功能,代码逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		//当前处于未登录状态
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			//使用rememberMeServices进行自动登录,返回登录成功标识
			Authentication rememberMeAuth = rememberMeServices.autoLogin(request,response);

			if (rememberMeAuth != null) {

				try {
					//验证rememberMeAuth的key和rememberMeServices的key是否一致
					rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);

					// Store to SecurityContextHolder
					SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);

					onSuccessfulAuthentication(request, response, rememberMeAuth);

					if (successHandler != null) {
						successHandler.onAuthenticationSuccess(request, response,
								rememberMeAuth);

						return;
					}

				}
				catch (AuthenticationException authenticationException) {

			}

			chain.doFilter(request, response);
		}
		else {
			chain.doFilter(request, response);
		}
	}


springSeciruity提供的实现

1
2
3
上面的filter使用RememberMeServices来处理自动登录,
TokenBasedRememberMeServices和PersistentTokenBasedRememberMeServices是springSecurity提供的两种处理remenberMe的实现类
他俩都继承自 AbstractRememberMeServices

0.AbstractRememberMeServices

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
	//自动登录方法
	public final Authentication autoLogin(HttpServletRequest request,
			HttpServletResponse response) {

		//获取cookie中的指定cookie
		String rememberMeCookie = extractRememberMeCookie(request);
		
		UserDetails user = null;

		try {
			//base64转成字符串,再按照.分隔成数组
			String[] cookieTokens = decodeCookie(rememberMeCookie);
			//子类实现的登录方法
			user = processAutoLoginCookie(cookieTokens, request, response);
			//检查用户状态,是否锁定,是否有效等
			userDetailsChecker.check(user);
			//返回验证成功的Authentication
			return createSuccessfulAuthentication(request, user);
		}
		catch (CookieTheftException cte) {
			cancelCookie(request, response);
			throw cte;
		}
		catch (UsernameNotFoundException noUser) {
			logger.debug("Remember-me login was valid but corresponding user not found.",
					noUser);
		}
		catch (InvalidCookieException invalidCookie) {
			logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
		}
		catch (AccountStatusException statusInvalid) {
			logger.debug("Invalid UserDetails: " + statusInvalid.getMessage());
		}
		catch (RememberMeAuthenticationException e) {
			logger.debug(e.getMessage());
		}

		cancelCookie(request, response);
		return null;
	}


1. 使用TokenBasedRememberMeServices如何实现processAutoLoginCookie方法的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected UserDetails processAutoLoginCookie(String[] cookieTokens,
			HttpServletRequest request, HttpServletResponse response) {
		//验证cookie中解码完成的值数组长度是3,分别是[用户名,过期时间戳,md5(用户名:过期时间:密码:key)]
		if (cookieTokens.length != 3) {
			throw new InvalidCookieException("Cookie token did not contain 3"
					+ " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
		}

		long tokenExpiryTime;
		//数组第二个元素转成Long
		try {
			tokenExpiryTime = new Long(cookieTokens[1]).longValue();
		}
		catch (NumberFormatException nfe) {
			throw new InvalidCookieException(
					"Cookie token[1] did not contain a valid number (contained '"
							+ cookieTokens[1] + "')");
		}
		//验证是否过期
		if (isTokenExpired(tokenExpiryTime)) {
			throw new InvalidCookieException("Cookie token[1] has expired (expired on '"
					+ new Date(tokenExpiryTime) + "'; current time is '" + new Date()
					+ "')");
		}
		//使用UserDetailsService 查询出用户信息
		UserDetails userDetails = getUserDetailsService().loadUserByUsername(
				cookieTokens[0]);
		//重新构造验证签名
		String expectedTokenSignature = makeTokenSignature(tokenExpiryTime,
				userDetails.getUsername(), userDetails.getPassword());
		//比较重新构造的签名和cookie中数组第三位中的签名是否一致
		if (!equals(expectedTokenSignature, cookieTokens[2])) {
			throw new InvalidCookieException("Cookie token[2] contained signature '"
					+ cookieTokens[2] + "' but expected '" + expectedTokenSignature + "'");
		}

		return userDetails;
	}


1
2
上面的方法就是根据cookie中查询的用户名到数据库中查询出真实用户信息。根据真实的信息,构造出签名与cookie中的签名进行比较。
这样可以防止cookie伪造,但不能防止cookie被别人窃取使用。

2. 使用PersistentTokenBasedRememberMeServices如何实现processAutoLoginCookie方法的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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) + "'");
		}
		//获取cookie中的series和token。series是标识唯一cookie,token是生成cookie时创建的随机值
		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);
		}

		//比较数据库中查询出的token中的Token值是否与cookie中的token值相同,防止cookie伪造
		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");
		}

		//上面两步验证说明,cookie是有效的,这里创建新的token(Series不变,Token变成随机值)
		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());
	}

1
2
这个rememberMeServices的处理逻辑是,每次自动登录成功后将cookie中的某个随机值和数据库同步更新,假设cookie别别人盗用,自动登录后盗用者的cookie被更新了。
主人的cookie就会变无效。下次主人会自动登录失败,系统就能发现cookie被盗用,此时删除数据库中的对应cookie验证,通知用户改密码等.

转载请注明出处:https://www.huangchaoyu.com/2019/10/30/springSecurity的两种rememberMe方式/