https://edu.csdn.net/course/detail/36074
https://edu.csdn.net/course/detail/35475
spring security安全框架作为spring系列组件中的一个,被广泛的运用在各项目中,那么spring security在程序中的工作流程是个什么样的呢,它是如何进行一系列的鉴权和认证呢,下面让我们走进源码,从源码的角度来从头走一遍spring security的工作流程。
当一个外部请求进入到我们应用中的时候,首先会通过我们的应用过滤器链ApplicationFilterChain,我们将遍历该过滤器链中每一个Filter进行对应的处理,下面我们来看下ApplicationFiterChain一般情况下有哪些Filter
**//ApplicationFilterChain遍历内部Fiter进行doFilter()处理**private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.pos < this.n) {
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
try {
Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute("org.apache.catalina.ASYNC\_SUPPORTED", Boolean.FALSE);
}
if (Globals.IS\_SECURITY\_ENABLED) {
Principal principal = ((HttpServletRequest)request).getUserPrincipal();
Object[] args = new Object[]{request, response, this};
SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
从上面的图中我们可以看到,chain在一般情况下中主要存在着这么几个filter,其中有我们比较熟悉的characterEncodeingFilter字符编码的过滤器等等,以及我们本次内容的主角:springSecurityFiterChain spring security的过滤器链,可以说这就是spring security的核心所在,springSecurityFiterChain虽然为filter,但他在这里实际扮演的是一个filterChain的角色,从他的的BeanName也可以看出,那我们接下来进入springSecurityFiterChain.doFilter()方法中,看看它内部又有哪些filter,以及内部的逻辑是怎样的
可以看到springSecurityFiterChain其实是个代理bean,它的doFilter()中实际用的delegate.doFilter(),delegate是个FilterChainProxy,下面来看下FiterChainProxy的内部实现。
**FilterChainProxy.doFilter()方法内部逻辑** @Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { //如果当前游标==additionalFilters的长度,即已经遍历完该列表内的Filter,则结束FilterChainProxy.doFilter()
if (this.currentPosition == this.size) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest)));
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
this.originalChain.doFilter(request, response);
return;
}
this.currentPosition++; //获取列表中下一个Filter
Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(),
this.currentPosition, this.size));
} //执行下一个Filter的doFilter()方法
nextFilter.doFilter(request, response, this);
}
}
我们看到FilterChainProxy中维护了一个Filter列表 additionalFilters,doFilter()中会顺序遍历这个列表,执行每一个Filter的doFilters,那这个列表中具体有哪些Filter呢,让我们来看一下
可以看到该列表中一共有13个spring security内置实现的Filter,系列文章中我们也主要来看SecurityContextPersistenceFilter security上下文持久化的过滤器,主要用来将认证过后的Authentication从session中提取注入本地线程变量中,以及UsernamePasswordAuthenticationFilter,用户密码认证过滤器,主要用来处理通过指定的登录的POST方式的请求url来进行认证…如果我们的项目中实现了JWT+Spring security的话,一般我们的我们会将自定义实现的JWT过滤器也加入到这条执行链中,并且执行位置放到UserNamePasswordAuthenticationFilter之前。
那么Spring security的大体工作流程就如下图:
这里我们并没有按照上面的列表顺序从头开始讲,第一个原因是本系列不会解析列表里所有的过滤器,第二个则是个人觉得登录是开启security的入口,从登录开始解析,之后再反过头串联前面的Filter,会有更好的效果。
通常情况下我们在security的配置中配置了哪些请求路径是开放的,哪些路径的需要权限的,访问了需要权限的请求时,如果没有权限便会跳转到security默认的登录页中,这时候我们可以进行输入账号密码进行登录,那这次登录请求security是如何处理的呢,让我们来看看UserNamePasswordAuthenticationFilter.doFilter()方法
//UserNamePasswordAuthenticationFilter本身并没有实现doFilter()方法,使用的是其父类的doFilter()方法public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
}
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
@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)) {
chain.doFilter(request, response);
return;
}
try { //当前请求满足认证条件,开始尝试去认证,attemptAuthentication()由UserNamePasswordAuthencationFilter自己实现
Authentication authenticationResult = attemptAuthentication(request, response); //如果没有一个认证管理器能认证,结果为null,则直接结束
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
} //调用session策略将认证通过的凭证储存在Session中,保证登陆之后后续同浏览器登陆不需要再登录,这个详细逻辑等到后续展开讲解
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
} //认证通过后的处理,这里会将当前的凭证填充到本地线程变量中,以及会如何在security的配置中配置了successForwardUrl,将会跳转至该URL
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);
}
}
}
requiresAuthentication()判断请求是否满足认证条件
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { //内部调用了成员属性中的RequestMatcher去进行匹配,下面逻辑可以看到只有请求是POST的并且路径跟pattern完全匹配才会返回true
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;
}
我们来看下UserNamePasswordAuthencationFilter内置的AntPathRequestMatcher的属性以及匹配逻辑
可以看到该AntPathRequestMatcher有两个重要的属性,pattern="/login", httpMethod=“POST”,这两个属性将在matchs()方法中起到决定性作用。
public boolean matches(HttpServletRequest request) { //如果请求方式不是httpMthod不一致,也就是非POST请求,则不匹配,返回false
if (this.httpMethod != null && StringUtils.hasText(request.getMethod())
&& this.httpMethod != HttpMethod.resolve(request.getMethod())) {
return false;
} //如果当前的pattern=/**,即任意请求,那么匹配,返回true
if (this.pattern.equals(MATCH\_ALL)) {
return true;
} //获取当前请求的路径(去除掉项目根路径)
String url = getRequestPath(request); //通过debug源码,这里采取的是完全匹配,即需要请求路径和pattern(/login)完全一致才返回true,当然这个pattern是可以配置的,之后会详细指出
return this.matcher.matches(url);
}
来看一下UserNamePasswordAuthenticationFilter实现的attemptAuthentication()方法
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException { //判断security当前是否只支持POST请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} //从请求中获取当前登录的用户名,即从request.getParamter("username")中获取
String username = obtainUsername(request); //没有该参数的话默认空串
username = (username != null) ? username : ""; //去除空格
username = username.trim(); //获取密码,即request.getPassword("password")
String password = obtainPassword(request);
password = (password != null) ? password : ""; //根据用户密码初始化Authentication凭证,具体为UsernamePasswordAuthentication这种类型的凭证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest); //调用当前环境的认证管理器进行认证, 这里内置的是ProviderManager
return this.getAuthenticationManager().authenticate(authRequest);
}
ProviderManager.authenticate()认证方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //获取传入参数中的认证凭证的类型,当前为UsernamePasswordAuthenticationToken
Class 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)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try { //当前授权者对该凭证进行认证授权,并将结果保存在result中
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
result = parentResult;
}
catch (ProviderNotFoundException ex) {
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) { //身份验证已完成。删除凭据和其他机密数据
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
//如果尝试了父级AuthenticationManager并成功,则 //将发布AuthenticationSuccessEvent //如果父级验证失败,此检查将防止重复AuthenticationSuccessEvent //AuthenticationManager已经发布了它
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
} //返回认证结果
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
可以看到当前ProviderManager下只有一个授权者,并且该授权者也不支持对UsernamePasswordAuthenticationToken这种凭证进行授权,所以需要调用该ProviderManager的父亲ProviderManager进行尝试认证
来看下它的父亲ProviderManager有哪些授权者,是否能够对UsernamePasswordAuthenticationToken进行认证授权
可以看到这个父亲认证管理器中也只有一个授权者,DaoAuthenticationProvider,利用数据库数据进行认证授权的,而这个就是能够支持对UsernamePasswordAuthenticationToken这种凭证进行认证授权的,下面我们就来看下DaoAuthenticationProvider是如何认证授权的
DaoAuthenticationProvider.authenticate()方法,该方法由它的父类AbstractUserDetailsAuthenticationProvider实现
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //该授权者只支持对UsernamePasswordAuthenticationToken这种类型凭证进行认证授权,不是的话即抛出异常
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 { //缓存中没有,则直接根据username从数据库中查,这里用的其实就是我们自己实现的UserDetailsService.loadUserByUsername(username)方法,下面会展示代码
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 { //对查询出来的用户信息进行预校验,主要检测的就是UserDetails实体中的isAccountNonExpired是否过期,isAccountNonLocked是否被锁定, isEnabled是否可用,只要有一个不满足就抛出异常
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); } //预检通过,进行后置检查,这里主要检查UserDeatils中的isCredentialNonExpired, 即检查密码是否过期,同样由我们自定义的UserDetailsService逻辑的来判断,是否过期 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.retrieveUser()从数据库中获取用户信息
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try { //通常我们使用security 都会自己实现UserDetailService进行配置,这里就用到了,它会通过我们自定义的逻辑来找寻用户并返回用户信息
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);
}
}
AbstractUserDetailsAuthenticationProvider的内部类实现的check方法()
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
@Override
public void check(UserDetails user) { //校验是否被锁定
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is locked");
throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
} //校验是否可用
if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
} //校验是否过期
if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account has expired");
throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
}
DaoAauthenticationProvider.additionalAuthenticationChecks()对凭证进行附加信息的校验,主要是校验密码
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { //如果凭证中的密码==null则直接抛出异常
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(); //调用security环境中的PasswordEncoder将凭证中的密码与数据库查询出的密码进行匹配,匹配不成功则抛出异常, 这个PasswordEncoder通常我们在使用security的 时候都会替换成自定义的Encoder,根据自己项目的需求进行自定义的实现其中的逻辑
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"));
}
}
AbstractAuthenticationProcessingFilter.successfulAuthentication()方法:认证通过后一些收尾处理
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException { //创建一个新的security上下文
SecurityContext context = SecurityContextHolder.createEmptyContext(); //将凭证填充进上下文
context.setAuthentication(authResult); //将上下文保存到本地线程变量中
SecurityContextHolder.setContext(context);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
} //security 开启了remember me功能的话的处理
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
} //security中配置了successForwardUrl的话,这里登录成功后就会跳转到指定url上
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
这篇文章是spring security源码解析的第一章,主要是解析了下security 是如何处理登录流程的,主要就是来看UsernamePasswordAuthenticationFilter的内部处理逻辑,通过源码我们也发现,默认情况下只有你的请求的POST方式的 /login,security才会认为这是登录请求,才会让该请求走UsernamePasswordAuthenticationFilter的处理逻辑,当然这个路径也是可以配置的,如下代码,配置了loginProcessingUrl的路径之后,再次登录,security就会以该路径当做登录请求。
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.logout()
.and()
.formLogin() //该路径也是UserNamePasswordAuthenticationFilter用来识别当前请求是不是登录请求,是的话才进行登录处理
.loginProcessingUrl("/loginMy") /...略../
}
关于UsernamePasswordAuthenticationFilter的内容暂时告一段落,下篇内容将继续通过源码解析剩下的过滤器SecurityContextPersistenceFilter security上下文持久化的过滤器等等…。
由于笔者水平有限,有些地方可能讲解的有错误,希望大家能够帮忙指出,共同进步。