SpringSecurity 的登录流程


用过SpringSecurity的小伙伴,都知道 Authentication 这个接口,我们在任何地方通过这个接口来获取到用户登录的信息,而我们用的频繁的一个它的一个实现类就是 UsernamePasswordAuthenticationToken。那么我们的登录信息是如何保存在这个类中的?那我们就需要知道SpringSecurity 的登录流程了。这两个类很重要!

在 SpringSecurity 中 认证和授权的校验是通过一系列的过滤器链。

首先到 AbstractAuthenticationProcessingFilter中,doFilter 方法:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
		throws IOException, ServletException {

	HttpServletRequest request = (HttpServletRequest) req;
	HttpServletResponse response = (HttpServletResponse) res;

	if (!requiresAuthentication(request, response)) {
		chain.doFilter(request, response);

		return;
	}

	Authentication authResult;

	try {
		authResult = attemptAuthentication(request, response);
		if (authResult == null) {
			// return immediately as subclass has indicated that it hasn't completed
			// authentication
			return;
		}
		sessionStrategy.onAuthentication(authResult, request, response);
	}
	catch (InternalAuthenticationServiceException failed) {
		logger.error(
				"An internal error occurred while trying to authenticate the user.",
				failed);
		unsuccessfulAuthentication(request, response, failed);

		return;
	}
	catch (AuthenticationException failed) {
		// Authentication failed
		unsuccessfulAuthentication(request, response, failed);

		return;
	}

	// Authentication success
	if (continueChainBeforeSuccessfulAuthentication) {
		chain.doFilter(request, response);
	}

	successfulAuthentication(request, response, chain, authResult);
}

分析:

1,authResult = attemptAuthentication(request, response); 调用登录相关的过滤器中的方法

也就是 UsernamePasswordAuthenticationFilter这个过滤器

2,unsuccessfulAuthentication(request, response, failed); 登录失败的处理

此方法中会调用我们在 SecurityConfig 中重写的方法 failureHandler

3,successfulAuthentication(request, response, chain, authResult); 登录成功的处理

此方法中会调用我们在 SecurityConfig 中重写的方法 successHandler

UsernamePasswordAuthenticationFilter(用户名密码身份验证过滤器)。其中比较重要的几个方法:

public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {

public Authentication attemptAuthentication(HttpServletRequest request,
		HttpServletResponse response) throws AuthenticationException {
	if (postOnly && !request.getMethod().equals("POST")) {
		throw new AuthenticationServiceException(
				"Authentication method not supported: " + request.getMethod());
	}

	String username = obtainUsername(request);
	String password = obtainPassword(request);

	if (username == null) {
		username = "";
	}

	if (password == null) {
		password = "";
	}

	username = username.trim();

	UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
			username, password);

	// Allow subclasses to set the "details" property
	setDetails(request, authRequest);

	return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
	return request.getParameter(passwordParameter);
}


@Nullable
protected String obtainUsername(HttpServletRequest request) {
	return request.getParameter(usernameParameter);
}


protected void setDetails(HttpServletRequest request,
		UsernamePasswordAuthenticationToken authRequest) {
	authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

}

通过上面的代码逻辑我们可以看出:

1,获取用户名和密码通过两个方法 obtainUsername(request);obtainPassword(request); 这两个方法都是通过 request.getParameter() 来获取的。

2,new 了一个 UsernamePasswordAuthenticationToken 来保存获取到的用户名和密码。

用户名保存到了 principal 属性中,密码保存到了 credentials 属性中。请注意:此时的               principal(主要的) 的类型是字符串类型,到后面会变为一个对象类型。

3,通过 setDetails() 方法来设置 details 属性。而 UsernamePassworAuthenticationToken 是没有这个属性的,而他的父类 AbstractAuthenticationTocken 中有,所以就保存在了它的父类中。这个details 属性是一个对象类型,这个对象里放的是 WebAuthenticationDetails 的实例,这个类中有两个属性 remoteAddress(远程地址) 和 sessionId。

4,最后调用 authenticate(authRequest) 方法进行验证,里面传了一个刚才new的 UsernamePassworAuthenticationToken 类的实例

接下来就是具体的校验操作了。

在上面 attemptAuthentication 方法的最后一步是开始做校验的操作,该行代码的首先拿到一个 AuthenticationManager ,然后调用它的 authenticate() 方法进行身份验证,而 AuthenticationManager 是一个接口,需要由它的实现类去执行 authenticate() 方法,而这时候获取到的实现类是 ProviderManger ,这时我们进入到它的 authenticate() 方法中,这里仅提供比较重要的部分:

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	Class<? extends Authentication> toTest = authentication.getClass();
	AuthenticationException lastException = null;
	AuthenticationException parentException = null;
	Authentication result = null;
	Authentication parentResult = null;
	boolean debug = logger.isDebugEnabled();

	for (AuthenticationProvider provider : getProviders()) {
		if (!provider.supports(toTest)) {
			continue;
		}

		if (debug) {
			logger.debug("Authentication attempt using "
					+ provider.getClass().getName());
		}

		try {
			result = provider.authenticate(authentication);

			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
		}
		...
	}

	if (result == null && parent != null) {
		// Allow the parent to try.
		try {
			result = parentResult = parent.authenticate(authentication);
		}
		...
	}

	if (result != null) {
		if (eraseCredentialsAfterAuthentication
				&& (result instanceof CredentialsContainer)) {
			// Authentication is complete. Remove credentials and other secret data
			// from authentication
			((CredentialsContainer) result).eraseCredentials();
		}

		// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
		// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
		if (parentResult == null) {
			eventPublisher.publishAuthenticationSuccess(result);
		}
		return result;
	}

	...
}

认证逻辑在这个方法完成

1,我们首先拿到 Authentication 类的 Class对象

2,进入到 for 循环,通过 getProviders() 方法获取到 AuthenticationProvider(身份验证提供者),然后调用 provider.supports(toTest) 来判断 当前拿到的 provider 是否支持对应的 Authentication(这里的Authentication 是 UsernamePasswordAuthenticationToken。如果不支持就 continue 。而第一次只拿到了一个 AuthenticationProvider  ,所以会结束for循环。如果支持调用 result = provider.authenticate(authentication); 来进行身份校验。

3,调用 copyDetails() 方法,这个方法会把旧的 Token 的details信息复制到新的 Token 中

5,调用 eraseCredentials() 方法,来擦除凭证信息,也就是你登录时的密码信息。

for循环分析:第一次循环 拿到的 provider 是 AnonymousAuthenticationProvider(匿名身份验证提供者),它根本就不支持验证 UsernamePasswordAuthenticationToken,然后判断成立 执行 continue。结束了此次循环。然后调用 parent.authenticate(authentication); 来继续验证,而 parent 就是 AuthenticationProvider ,所以又回到了此方法。而第二次拿到的是一个 DaoAuthenticationProvider,这个 provider 是支持验证 UsernamePasswordAuthenticationToken的,但是 DaoAuthenticationProvider 中并没有重写此方法,但是它的父类 AbstractUserDetailsAuthenticationProvider 类中定义了此方法,所以会来到它的父类的这个方法中,依旧是比较重要的部分:

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {

        ...
    // Determine username
	String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
			: authentication.getName();

		try {
			user = retrieveUser(username,
					(UsernamePasswordAuthenticationToken) authentication);
		}
        ...

	try {
		preAuthenticationChecks.check(user);
		additionalAuthenticationChecks(user,
				(UsernamePasswordAuthenticationToken) authentication);
	}
	catch (AuthenticationException exception) {
		if (cacheWasUsed) {
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username,
					(UsernamePasswordAuthenticationToken) authentication);
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		else {
			throw exception;
		}
	}

	postAuthenticationChecks.check(user);

	if (!cacheWasUsed) {
		this.userCache.putUserInCache(user);
	}

	Object principalToReturn = user;

	if (forcePrincipalAsString) {
		principalToReturn = user.getUsername();
	}

	return createSuccessAuthentication(principalToReturn, authentication, user);
}

分析:

1,首先拿到用户名,然后调用 retrieveUser() 方法来检索用户,这个方法会调用自己重写后的 loadUserByUsername() 方法 返回一个 User 对象,也就是从数据库中根据用户名查询出来的

protected final UserDetails retrieveUser(String username,
		UsernamePasswordAuthenticationToken authentication)
		throws AuthenticationException {
	prepareTimingAttackProtection();
	try {
		UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
		if (loadedUser == null) {
			throw new InternalAuthenticationServiceException(
					"UserDetailsService returned null, which is an interface contract violation");
		}
		return loadedUser;
	}
    ...
}

2,接下来调用 preAuthenticationChecks.check(user); 来校验用户的状态。比如:账户是否锁定,是否禁用,是否过期

3,接下来调用 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); 来校验密码是否正确

4,然后再调用 postAuthenticationChecks.check(user); 来校验密码是否过期

5,接下来有一个 forcePrincipalAsString 属性,这个是 是否强制将 Authentication 中的 principal 属性设置为字符串。这个默认值是 false ,不用改,这样获取信息会很方便。

6,最后通过 createSuccessAuthentication(principalToReturn, authentication, user); 创建一个新的 UsernamePasswordAuthenticationToken 对象返回。

posted @ 2022-05-08 18:12  lz-zxy  阅读(400)  评论(0编辑  收藏  举报