Spring Security登录流程

1、Authentication

存放用户信息类的顶层接口为Authentication,我们从这个类往下找,发现常用的实现类有UsernamePasswordAuthenticationToken。

我们对与之关联的5个类进行观察:

  • Authentication

  • AbstractAuthenticationToken

  • UsernamePasswordAuthenticationToken

  • GrantedAuthority

  • SimpleGrantedAuthority

我们可以发现,在UsernamePasswordAuthenticationToken类中存在5个属性:authorities(权限和角色的集合),details(细节),authenticated(是否已认证),principal(主要信息),credentials(凭证),其余的都是些getter/setter方法。

看下权限/角色集合属性,Collection< GrantedAuthority >中的GrantedAuthority 类,你去查看它的简单实现类源码(SimpleGrantedAuthority)会发现,它仅仅只有一个属性:String role,其余都是getter/setter方法。

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

    private final Collection<GrantedAuthority> authorities;

	private Object details;

	private boolean authenticated = false;
    
	private final Object principal;

	private Object credentials;
	...
}
public final class SimpleGrantedAuthority implements GrantedAuthority {

	private final String role;
    ...
}
2、登录流程

Spring Security本质是一系列的过滤器,与登录认证相关的过滤器是UsernamePasswordAuthenticationFilter。

过滤器,我们的关注点还是要从doFilter()方法开始,AbstractAuthenticationProcessingFilter就是doFilter()方法实现的类。

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
    
	private void doFilter(
    	HttpServletRequest request, 
        HttpServletResponse response, 
        FilterChain chain) throws IOException, ServletException {
        // 1. 判断当前的过滤器能处理当前路径的请求
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
           	// 2.认证用户
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				return;
			}
            // 3. 调用认证成功的回调
			successfulAuthentication(request, response, chain, authenticationResult);
		}
        // 4. 调用认证失败后的回调
		catch (InternalAuthenticationServiceException failed) {
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			unsuccessfulAuthentication(request, response, ex);
		}
	}
}

根据上面的代码,doFilter()方法中的代码逻辑很简单:

1、判断当前请求路径能不能处理

2、调用attemptAuthentication()方法认证用户

3、认证成功,则调用successfulAuthentication();认证失败,则调用unsuccessfulAuthentication()方法

下面,我们来看下attemptAuthentication()方法:

public class UsernamePasswordAuthenticationFilter extends 			 AbstractAuthenticationProcessingFilter {

    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 1. 判断类和请求的方式是否一致为POST
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            // 2. 获取用户名
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            // 2. 获取密码
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            
            // 3. 设置属性Token
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            // 3. 设置details
            this.setDetails(request, authRequest);
            // 4. 执行校验
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

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

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

}

通过上面的源码,我们可以得到:

1、通过obtainPassword()和obtainUsername()方法从请求获取值,其本质上就是request.getParameter()方法,但这同时也要求请求中的数据只能已key-value的方式来传递,不行看这-传送门

2、在UsernamePasswordAuthenticationToken中,username对应principal属性,password对应credentials。

3、doFilter()先判断当前请求是否为POST,再调用obtainPassword()和obtainUsername()来获取用户名和密码来创建UsernamePasswordAuthenticationToken对象。

4、调用setDetails()方法给details属性赋值,其实际保存的对象为WebAuthenticationDetails实例,主要属性有remoteAddress和sessionId。

5、调用authenticate()方法执行认证流程

public class WebAuthenticationDetails implements Serializable {

	private final String remoteAddress;

	private final String sessionId;

	public WebAuthenticationDetails(HttpServletRequest request) {
        // 获取ip地址,如果存在代理情况,则获取不到真实的ip地址
		this.remoteAddress = request.getRemoteAddr();
		HttpSession session = request.getSession(false);
		this.sessionId = (session != null) ? session.getId() : null;
	}
}

最后一步调用的authenticate()方法,执行的对象为是AuthenticationManager的实现,根据接口我们发现它的实现,它仅实现的类为ProviderManager,其余都是静态内部类,那就是它了,我们看下它的authenticate()方法。

	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 1. 获取UsernamePasswordAuthenticationToken的class对象
		Class<? extends Authentication> toTest = authentication.getClass();
		...
        // providers是AuthenticationProvider的集合
		int size = this.providers.size();
        // 2. 遍历provider集合
		for (AuthenticationProvider provider : getProviders()) {
            // 3. 判断当前provider是否支持该UsernamePasswordAuthenticationToken,同类或是其子类
			if (!provider.supports(toTest)) {
				continue;
			}
			...
            // 4. provider调用认证方法,方法返回一个Authentication实例
			result = provider.authenticate(authentication);
            // 4. Authentication不为空,则复制details过去
			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
			...
		}
        // 5. parent是ProviderManager,会再次进行当前方法
        if (result == null && parent != null) {
			result = parentResult = parent.authenticate(authentication);
        }
        // 6.调用 eraseCredentials方法擦除凭证信息,也就是credentails属性
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                ((CredentialsContainer) result).eraseCredentials();
            }
            if (parentResult == null) {
                // 7. 登录成功调用publishAuthenticationSuccess()方法广播出去
                eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }
		...
	}

通过以上源码,得到以下结论:

1、ProviderManager中存在providers属性来保存provider对象

2、authenticate()方法的实质就是将集合的provider一个一个调用authenticate()方法,直到遍历结束或某个provider返回的result不为null且设置details成功。

3、遍历集合的要点:一:判断当前provider是否能处理当前Authentication类型;二:provider是否能返回Authentication实例。

4、当认证成功,查出Authentication中credentails属性,并调用广播方法。

那Provider的authenticate的认证方法又是怎样的呢?下面来看

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// 1. 获取principal属性值
		String username = determineUsername(authentication);
    	
		boolean cacheWasUsed = true;
    	// 2. 根据用户名从缓存中获取用户信息
		UserDetails user = this.userCache.getUserFromCache(username);
    
		if (user == null) {
			cacheWasUsed = false;
            // 3. 最后调用 loadUserByUsername(),也就是需要我们实现的那个UserDetailsService接口
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
		}
        // 4. 检查用户状态,如账户是否被禁用、账户是否被锁定、账户是否过期等等
		this.preAuthenticationChecks.check(user);
        // 4. 做密码比对
		additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    	// 4. 检查密码是否过期
		this.postAuthenticationChecks.check(user);
    	// 5. 往缓存中添加用户数据
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
    	// 4. 是否强制将 Authentication 中的 principal 属性设置为字符串
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		} 
    	// 6. 创建一个token
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

通过以上的源码,我们可以发现authenticate()方法,

1、获取用户名

2、从缓存中获取用户信息,没有则从调用loadUserByUsername()方法获取

3、检查各种机制,检查完毕没有问题后添加信息到缓存中

4、创建一个Token返回

3、信息的保存与获取

在AbstractAuthenticationProcessingFilter抽象类的doFIlter()方法中定义了验证的主要流程,我们去查看认证成功后调用的方法successfulAuthentication(),它后面有一步SecurityContextHolder.getContext().setAuthentication(authResult);就是保存用户信息到上下文的,你也可以试着通过SecurityContextHolder.getContext().getAuthentication();来获取用户信息。

	successfulAuthentication(request, response, chain, authenticationResult);

	protected void successfulAuthentication(HttpServletRequest request, 
                                            HttpServletResponse response,
                                            FilterChain chain,
             Authentication authResult) throws IOException, ServletException {
        // 保存用户信息
		SecurityContextHolder.getContext().setAuthentication(authResult);
        ...
	}
	// 获取信息
	SecurityContextHolder.getContext().getAuthentication();
4、登录认证流程总结

Spring Security是一大串的过滤器,那一切的开始都是doFilter()方法,在Spring Security中用于用户认证的过滤器是UsernamePasswordAuthenticationFilter,但它并没有实现doFilter()方法,方法实现在于它的父类:AbstractAuthenticationProcessingFilter

doFiter()方法:该方法中,无非定义3件事:①:用户认证的流程;②:认证成功怎么办;③:认证失败怎么办。

这三件事,我们都可以进行配置如何进行。

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException { 
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
        // 第一件事
		try {
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				return;
			}
            // 第二件事
			successfulAuthentication(request, response, chain, authenticationResult);
		}
        // 第三件事
		catch (InternalAuthenticationServiceException failed) {
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			unsuccessfulAuthentication(request, response, ex);
		}
	}

我们顺着第一件事继续看下去,我们会找到UsernamePasswordAuthenticationFilter的attemptAuthentication()方法。

attemptAuthentication()方法:它无非有做了两件事:①:获取Authentication所需要的信息并填上去;②:调用下一级认证方法。

	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
        // 第一件事
		String username = obtainUsername(request);
		username = (username != null) ? username : "";
		username = username.trim();
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		setDetails(request, authRequest);
        // 第二件事
		return this.getAuthenticationManager().authenticate(authRequest);
	}

我们再顺着第二件往下看,我们会找到AuthenticationManager接口和它的唯一实现子类ProviderManager

authenticate():该类中存在属性providers来保存AuthenticationProvider接口的实例。该方法中只做了一件事:遍历providers集合,调用他们的authenticate()方法,不过有两种可能:①:不支持当前authentication,parent来,再次进入该方法:②:认证成功,清除credentials属性并调用publishAuthenticationSuccess()方法进行广播。

	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        
		Class<? extends Authentication> toTest = authentication.getClass();
		int size = this.providers.size(); 
        // 第一件事
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
        // 第一种情况
		if (result == null && this.parent != null) {
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
        // 第二中情况
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				((CredentialsContainer) result).eraseCredentials();
			}
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}
	}

我们再顺着provider来看下,它的接口是AuthenticationProvider,可以来看下DaoAuthenticationProvider是如何实现的。

authenticate()方法:事儿不多,三件事:①:获取用户名,先去缓存里查,没有再到定义的方法loadUserByUsername()里查;②:对查询出来的用户信息各种认证,如果缓存中没有,则添加进去;③:返回一个Authentication实例

	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// 获取用户名
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
        // 第一件事
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				...
			}
		}
        	// 第二件事
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);

		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
        // 第三件事
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

Spring Security总体的登录流程就是这样!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值