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 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 对象,也就是从数据库中根据用户名查询出来的

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

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

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

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

	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;
		}
        ...
	}

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

你可能感兴趣的:(java)