3、Spring Security认证原理
3.1、认证流程
Spring Security是如何完成身份认证的?
1、用户名和密码被UsernamePasswordAuthenticationFilter过滤器获取到,封装成 Authentication ,通常情况下是UsernamePasswordAuthenticationToken 这个实现类。
2、AuthenticationManager 身份管理器(委托给DaoAuthenticationProvider调用UserDeatilsService的loadUserByUsername()获取用户信息并返回)负责验证这个 Authentication ,
3、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。
4、SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
3.2、相关接口
UsernamePasswordAuthenticationFilter
当用户登录时发出post请求,会先被UsernamePasswordAuthenticationFilter(假设不设置addFilterBefore)所拦截,这个拦截器主要的作用就是拦截登录请求,在封装成UsernamePasswordAuthenticationToken(username,password)注意此时还未通过验证,然后调用AuthenticationManager 进行身份认证。但是实际流程可不是这么简单,我们来分析一下源码就知道UsernamePasswordAuthentiactionFilter在SpringSecurity中的作用
- 首先我们先来看一下UsernamePasswordAuthenticationFilter的继承树;可以看到UsernamePasswordAuthenticationFilter最终也是实现了Filter,而我们在UsernamePasswordAuthenticationFilter当中并没有找到Filter接口的象征性方法doFilter(request,response,chain),到这一步我们就应该意识到springsecurity的拦截器链中最先调用的拦截器并不是UsernamePasswordAuthenticationFilter,所以我们可以按照继承树的继承顺序往上找
- 顺着继承树我们发现AbstractAuthenticationProcessingFilter有doFilter方法,我们来分析一下这个方法
- 这里有两个方法,一个是重写Filter的doFilter,还有一个是私有的doFilter ; 在代码的第9行我们看到一个条件判断,如果不需要认证就直接放行,我们去看一下requiresAuthentication(request, response)方法的实现
AbstractAuthenticationProcessingFilter.class
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) { // 如果requiresAuthentication(request, response)为false则不 需要去认证,直接放行
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
- 可以看到在该方法中又调用了一个看起来很陌生的方法this.requiresAuthenticationRequestMatcher.matches(request),但其实在AbstractAuthenticationProcessingFilter的其中一个构造方法中就已经对requiresAuthenticationRequestMatcher进行了赋值,由于这是一个构造方法,当子类继承父类的时候,子类的构造方法是可以去调用父类的构造方法(使用super关键字),所以我们在UsernamePasswordAuthenticationFilter的构造方法就发现了对requiresAuthenticationRequestMatcher的赋值
AbstractAuthenticationProcessingFilter.class
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) { // 调用了一个新的方法
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
// AbstractAuthenticationProcessingFilter其中一个构造方法
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
- 我们看到赋值为new AntPathRequestMatcher("/login",“POST”),是post风格路径为"login",想必走到这里大家就可以理解为什么只有用户发起登录的请求时才会被拦截,其他请求则会被放行吧
UsernamePasswordAuthenticationFilter.class
// 定义静态属性并赋值
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER
= new AntPathRequestMatcher("/login","POST");
// 调用AbstractAuthenticationProcessingFilter的构造方法进行赋值
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
- 而 AbstractAuthenticationProcessingFilter的另一个构造器也会为requiresAuthenticationRequestMatcher进行赋值
AbstractAuthenticationProcessingFilter.class
protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
setFilterProcessesUrl(defaultFilterProcessesUrl);
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(filterProcessesUrl));
}
public final void setRequiresAuthenticationRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requestMatcher;
}
- 在分析之后我们得出一个结论requiresAuthentication(request, response),返回值是通过客户端发起的请求是否为/login,且是post风格的请求,如果是则继续进行认证,如果不是则直接放行,我们来看一下继续进行认证该怎么走
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) { // ture,继续认证
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
- 通过上面代码的第8行,发现调用了attemptAuthentication(request, response)这个方法,点进去方法这是一个抽象方法,就意味着子类要重写这个方法,所以我们又回到了UsernamePasswordAuthenticationFilter中去,attemptAuthentication()这个方法主要是对用户名密码规范化后通过UsernamePasswordAuthenticationToken封装用户名密码,再通过委托类
AuthenticationManager
的验证方法authenticate()
进行身份验证
UsernamePasswordAuthenticationFilter.class
@Override
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);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
AuthenticationManager
AuthenticationManger涉及到的接口和类比较多
- AuthenticationManager 为认证管理接口类,其定义了认证方法 authenticate()
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
- ProviderManager为认证管理类,实现了接口 AuthenticationManager ,并在认证方法
authenticate()
中将身份认证委托给具有认证资格的 AuthenticationProvider进行身份认证 。总的来说,ProviderManager是 AuthenticationManager 的一个实现类,提供了基本的认证逻辑和方法;它包含了一个List属性,通过 AuthenticationProvider 接口来扩展出多种认证方式,实际上这是委托者模式的应用(Delegate)
@Override
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;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) { // 寻找具有认证资格的AuthenticationProvider进行身份认证
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication); // AuthenticationProvider 接口来扩展出多种认证方式,来进行委托认证
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastExcepion = ex;
}
}
········
}
- ProviderManager中的成员变量 providers [List] 存储了一个 AuthenticationProvider类型的 List,在ProviderManager的authenticate()方法会去循环遍历这个providers,寻找合适的xxxAuthenticationProvider去委托处理authentication
ProviderManager.class
private List<AuthenticationProvider> providers = Collections.emptyList();
AuthenticationProvider
- AuthenticationProvider为认证接口类,其定义了身份认证方法 authenticate()。AuthenticationProvider有多个实现类,这每一个实现类都会被遍历直到寻找的合适的xxxAuthenticationProvider去验证authentication
- AbstractUserDetailsAuthenticationProvider为认证抽象类,实现了接口 AuthenticationProvider定义的认证方法 authenticate()
。
AbstractUserDetailsAuthenticationProvider还定义了retrieveUser()用作查询数据库用户信息,以及 additionalAuthenticationChecks()用作身份认证。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
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.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); // 校验密码
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// 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);
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);
}
- DaoAuthenticationProvider继承自类 AbstractUserDetailsAuthenticationProvider,实现该类的方法
retrieveUser()
和 additionalAuthenticationChecks()。
DaoAuthenticationProvider中还具有成员变量 userDetailsService [UserDetailsService] 用作用户信息查询,以及成员变量passwordEncoder [PasswordEncoder]
用作密码的加密及验证。
- retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication)
DaoAuthenticationProvider.class
@Override
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;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
- additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
Authentication
Authentication在spring security中是最高级别的身份/认证的抽象,由这个顶级接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。
public interface Authentication extends Principal, Serializable {
//1.权限信息列表,可使用 AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_ADMIN")返回字符串权 限集合
Collection<? extends GrantedAuthority> getAuthorities();
//2.密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
Object getCredentials();
//3.认证时包含的一些信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了 访问者的ip地址和sessionId的值。
Object getDetails();
//4.身份信息,大部分情况下返回的是UserDetails接口的实现类
Object getPrincipal();
//5.是否被认证,认证为true
boolean isAuthenticated();
//6.设置是否能被认证
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken 实现了 Authentication 主要是将用户输入的用户名和密码进行封装,并供给 AuthenticationManager 进行验证;验证完成以后将返回一个认证成功的Authentication 对象
SecurityContextHolder
用于存储安全上下文(security context)的信息, SecurityContextHolder 默认使用 ThreadLocal策略来存储认证信息。
// 获取当前用户名
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
UserDetailsService
public interface UserDetailsService {
// 根据用户名加载用户信息
UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException;
}