SpringSecurity原理解析
Posted weixin_42412601
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity原理解析相关的知识,希望对你有一定的参考价值。
目录
1、认证流程
总体的认证流程如下:
登录的请求首先会被拦截器UsernamePasswordAuthenticationFilter
拦截,第一次请求肯定是未认证,未认证通过对象AuthenticationManager
,来委托AuthenticationProvider
,去关联UserDetailService
,去查数据库,判断用户是否是数据库中存在的用户,当认证通过后,把数据封装到UserDetail
中,再然后把认证 的信息,封装到Authentication
中
源码查看
UsernamePasswordAuthenticationFilter
认证过滤器,继承自AbstractAuthenticationProcessingFilter
。
它的doFilter
方法,其实是在父类AbstractAuthenticationProcessingFilter
中,它只是重写了一些方法而已。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
1、判断当前请求是不是post请求
if (!this.requiresAuthentication(request, response))
chain.doFilter(request, response);
else
if (this.logger.isDebugEnabled())
this.logger.debug("Request is to process authentication");
Authentication authResult;
try
//2、调用子类的方法进行身份认证,认证成功之后,把认证信息封装到对象里面去
authResult = this.attemptAuthentication(request, response);
if (authResult == null)
return;
//3、认证成功之后,用session存储相关信息。session策略处理
this.sessionStrategy.onAuthentication(authResult, request, response);
catch (InternalAuthenticationServiceException var8)
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
//4、认证失败,做认证失败的处理
this.unsuccessfulAuthentication(request, response, var8);
return;
catch (AuthenticationException var9)
//4、认证失败,做认证失败的处理
this.unsuccessfulAuthentication(request, response, var9);
return;
//5、认证成功后的处理
if (this.continueChainBeforeSuccessfulAuthentication)
//执行下一个过滤器
chain.doFilter(request, response);
//调用认证成功后的方法
this.successfulAuthentication(request, response, chain, authResult);
1、判断当前请求是不是post请求
这个requiresAuthenticationRequestMatcher其实最终是来源于子类UsernamePasswordAuthenticationFilter的构造方法
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response)
return this.requiresAuthenticationRequestMatcher.matches(request);
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher)
Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
UsernamePasswordAuthenticationFilter的构造方法
public UsernamePasswordAuthenticationFilter()
super(new AntPathRequestMatcher("/login", "POST"));
如果我们自定义一个认证过滤器,只需要继承自UsernamePasswordAuthenticationFilter
,然后在构造方法中,调用setRequiresAuthenticationRequestMatcher
就能覆盖UsernamePasswordAuthenticationFilter
设置的默认的登录路径。
2、调用子类的方法进行身份认证,认证成功之后,把认证信息封装到对象里面去
UsernamePasswordAuthenticationFilter
:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException
//判断是否是post请求
if (this.postOnly && !request.getMethod().equals("POST"))
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
else
//从请求的queryString中获取用户名和密码
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null)
username = "";
if (password == null)
password = "";
username = username.trim();
//将用户名和密码封装到UsernamePasswordAuthenticationToken中
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//再将request请求,封装到UsernamePasswordAuthenticationToken对象中
this.setDetails(request, authRequest);
//将未认证的信息调用authenticate方法进行认证
return this.getAuthenticationManager().authenticate(authRequest);
//从请求的queryString中获取用户名和密码
protected String obtainUsername(HttpServletRequest request)
return request.getParameter(this.usernameParameter);
2.1、查看UsernamePasswordAuthenticationToken
该类是Authentication
的实现类
2.2、调用authenticate方法进行身份认证
ProviderManager
是 AuthenticationManager
接口的实现类,该接口是认证相关的核心接口,也是认证的入口。在实际开发中,我们可能有多种不同的认证方式,例如:用户名+密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是AuthenticationManager
。在该接口的常用实现类 ProviderManager
内部会维护一个List<AuthenticationProvider>
列表,存放多种认证方式,实际上这是委托者模式(Delegate)
的应用。每种认证方式对应着一个 AuthenticationProvider
,AuthenticationManager
根据认证方式的不同(根据传入的 Authentication
类型判断)委托对应的 AuthenticationProvider
进行用户认证。
ProviderManager
的authenticate
方法的认证过程。
public Authentication authenticate(Authentication authentication) throws AuthenticationException
//获取传入的Authentication 类型的UsernamePasswordAuthenticationToken.class
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
//获取认证方式列表List<AuthenticationProvider>的迭代器
Iterator var8 = this.getProviders().iterator();
//循环迭代
while(var8.hasNext())
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
//判断当前的AuthenticationProvider是否支持UsernamePasswordAuthenticationToken.class类型的Authentication
if (provider.supports(toTest))
if (debug)
logger.debug("Authentication attempt using " + provider.getClass().getName());
try
//重点!!!!!!!!!!!!!!!!!!!!!!!!!!!
//成功找到适配当前认证方式的AuthenticationProvider,此处为DaoAuthenticationProvider
//如果认证成功,会返回一个标记已认证的Authentication对象
result = provider.authenticate(authentication);
if (result != null)
//认证成功,将传入的Authentication对象中的details信息拷贝到已认证的Authentication对象中
this.copyDetails(authentication, result);
break;
catch (InternalAuthenticationServiceException | AccountStatusException var13)
this.prepareException(var13, authentication);
throw var13;
catch (AuthenticationException var14)
lastException = var14;
if (result == null && this.parent != null)
try
//认证失败,使用父类型的AuthenticationManager进行验证
result = parentResult = this.parent.authenticate(authentication);
catch (ProviderNotFoundException var11)
catch (AuthenticationException var12)
parentException = var12;
lastException = var12;
if (result != null)
//认证成功之后,去除result的敏感信息,要求相关类实现CredentialsContainer接口
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer)
//去除过程就是调用CredentialsContainer接口的eraseCredentials方法
((CredentialsContainer)result).eraseCredentials();
//发布认证成功的事件
if (parentResult == null)
this.eventPublisher.publishAuthenticationSuccess(result);
return result;
else
//认证失败之后,抛出失败的异常信息
if (lastException == null)
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]toTest.getName(), "No AuthenticationProvider found for 0"));
if (parentException == null)
this.prepareException((AuthenticationException)lastException, authentication);
throw lastException;
看重点:DaoAuthenticationProvider
的authenticate
方法,实际上是父类AbstractUserDetailsAuthenticationProvider
的authenticate
方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () ->
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
);
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
//尝试从缓存中获取user
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null)
cacheWasUsed = false;
try
//重点!!!!!
//缓存中没有获取到用户,直接去数据库中检索用户
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
catch (UsernameNotFoundException var6)
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions)
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
throw var6;
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
try
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
catch (AuthenticationException var7)
if (!cacheWasUsed)
throw var7;
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed)
//把获取到的用户放到缓存中
this.userCache.putUserInCache(user);
Object principalToReturn = user;
if (this.forcePrincipalAsString)
principalToReturn = user.getUsername();
return this.createSuccessAuthentication(principalToReturn, authentication, user);
看重点:发现retrieveUser
方法被子类DaoAuthenticationProvider
,重写了
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException
this.prepareTimingAttackProtection();
try
//获取一个UserDetailsService,并执行它的loadUserByUsername方法,来获取一个用户
//查询数据库获取用户的逻辑,就写在loadUserByUsername中
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null)
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
else
return loadedUser;
catch (UsernameNotFoundException var4)
this.mitigateAgainstTimingAttack(authentication);
throw var4;
catch (InternalAuthenticationServiceException var5)
throw var5;
catch (Exception var6)
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
认证过程最重要的部分就解析完了
3、认证成功/失败处理
回到AbstractAuthenticationProcessingFilter
,查看successfulAuthentication
和unsuccessfulAuthentication
方法
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException
if (this.logger.isDebugEnabled())
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
//将认证成功的用户信息对象Authentication封装进SecurityContext对象中
//SecurityContextHolder是对ThreadLocal的一个封装
SecurityContextHolder.getContext().setAuthentication(authResult);
//remenberMe的处理
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null)
//发布认证成功的事件
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
//调用认证成功处理器
this.successHandler.onAuthenticationSuccess(request, response, authResult);
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException
//清除该线程在SecurityContextHolder中对应的SecurityContext对象
SecurityContextHolder.clearContext();
if (this.logger.isDebugEnabled())
this.logger.debug("Authentication request failed: " + failed.toString(), failed);
this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
//rememberMe的处理
this.rememberMeServices.loginFail(request, response);
//调用认证失败处理器
this.failureHandler.onAuthenticationFailure(request, response, failed);
2、授权流程
上一个部分通过源码的方式介绍了认证流程,下面介绍权限访问流程,主要是对ExceptionTranslationFilter
过滤器和 FilterSecurityInterceptor
过滤器进行介绍。
2.1、 ExceptionTranslationFilter 过滤器
该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。具体源码如下:
2.2、 FilterSecurityInterceptor过滤器
FilterSecurityInterceptor
是过滤器链的最后一个过滤器,该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器。
ExceptionTranslationFilter
进行捕获和处理。具体源码如下:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
public void invoke(FilterInvocation fi) throws IOException, ServletException
if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest)
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
else
if (fi.getRequest() != null && this.observeOncePerRequest)
fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
//根据资源权限配置来判断当前请求是否有权限访问对应的资源
//如果不能访问则抛出异常
InterceptorStatusToken token = super.beforeInvocation(fi);
try
//访问相关资源,通过Springmvc的核心组件DispatchServlet进行访问
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
finally
//待请求完成后会在 finallyInvocation() 中将原来的 SecurityContext 重新设置给SecurityContextHolder。
super.finallyInvocation(token);
// 正常请求结束,最后也会执行(afterInvocation 内部会调用finallyInvocation )
super.afterInvocation(token, (Object)null);
需要注意,Spring Security
的过滤器链是配置在 SpringMVC
的核心组件DispatcherServlet
运行之前。也就是说,请求通过 Spring Security
的所有过滤器,不意味着能够正常访问资源,该请求还需要通过 SpringMVC
的拦截器链。
看看beforeInvocation
:
protected InterceptorStatusToken beforeInvocation(Object object)
Assert.notNull(object, "Object was null");
boolean debug = this.logger.isDebugEnabled();
if (!this.getSecureObjectClass().isAssignableFrom(object.getClass()))
throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass());
else
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
if (attributes != null && !attributes.isEmpty())
if (debug)
this.logger.debug("Secure object: " + object + "; Attributes: " + attributes);
if (SecurityContextHolder.getContext().getAuthentication() == null)
this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);
//判断是否需要进行身份认证
Authentication authenticated = this.authenticateIfRequired();
try
//使用获取到的ConfigAttribute ,继续调用访问控制器AccessDecisionManager对当前请求进行鉴权。
//无论鉴权通过或是不通后,Spring Security 框架均使用了观察者模式,来通知其它Bean,当前请求的鉴权结果。
//如果鉴权不通过,则会抛出 AccessDeniedException 异常,即访问受限,然后会被 ExceptionTranslationFilter 捕获,最终解析后调转到对应的鉴权失败页面
//如果鉴权通过,AbstractSecurityInterceptor 通常会继续请求
this.accessDecisionManager.decide(authenticated, object, attributes);
catch (AccessDeniedException var7)
this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7));
throw var7;
if (debug)
this.logger.debug("Authorization successful");
if (this.publishAuthorizationSuccess)
this.publishEvent(new AuthorizedEvent(object, attributes, authenticated));
//通过 RunAsManager 在现有 Authentication 基础上构建一个新的Authentication
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
if (runAs == null)
if (debug)
this.logger.debug("RunAsManager did not change Authentication object");
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
else
if (debug)
this.logger.debug("Switching to RunAs Authentication: " + runAs);
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
//如果新的 Authentication 不为空则将产生一个新的 SecurityContext,并把新产生的Authentication 存放在其中
//这样在请求受保护资源时从 SecurityContext中 获取到的 Authentication 就是新产生的 Authentication。
SecurityContextHolder.getContext().setAuthentication(runAs);
return new InterceptorStatusToken(origCtx, true, attributes, object);
// rejectPublicInvocations 属性,默认为 false。此属性含义为拒绝公共请求
else if (this.rejectPublicInvocations)
throw new IllegalArgumentException("Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. This indicates a configuration error because the rejectPublicInvocations property is set to 'true'");
else
if (debug)
this.logger.debug("Public object - authentication not attempted");
this.publishEvent(new PublicInvocationEvent(object));
return null;
authenticateIfRequired
:判断是否需要进行身份认证
private Authentication authenticateIfRequired()
//从SecurityContextHolder获取authentication
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//判断是否已经认证过了。
//还记得UsernamePasswordAuthenticationToken的构造方法有设置是否认证过吗
if (authentication.isAuthenticated() && !this.alwaysReauthenticate)
if (this.logger.isDebugEnabled())
this.logger.debug("Previously Authenticated: " + authentication);
//认证过了,直接返回authentication
return authentication;
else
//没有认证过,调用相应的ProviderManager去认证
authentication = this.authenticationManager.authenticate(authentication);
if (this.logger.isDebugEnabled以上是关于SpringSecurity原理解析的主要内容,如果未能解决你的问题,请参考以下文章
Spring Security 解析 —— 基于JWT的单点登陆(SSO)开发及原理解析