从Spring Security的过滤器链中,我们已经发现位于最后的FilterSecurityInterceptor是用来进行权限认证的,这一节将详细分析Spring Security是如何进行权限认证的。
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
Filter {
private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
...
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
...
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
//第一次调用此请求时,请执行安全检查
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
}
这段代码的主要逻辑就是调用了父类的beforeInvocation、finallyInvocation和afterInvocation方法。接下来看一下父类做了什么。
AbstractSecurityInterceptor是一个实现了对受保护对象的访问进行拦截的抽象类,先来看一下它的源码:
public abstract class AbstractSecurityInterceptor implements InitializingBean,
ApplicationEventPublisherAware, MessageSourceAware {
...
//对返回值进行修改
private AfterInvocationManager afterInvocationManager;
//改变鉴权过后的Authentication
private RunAsManager runAsManager = new NullRunAsManager();
...
protected InterceptorStatusToken beforeInvocation(Object object) {
...
//获取配置的权限信息
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
...
//身份认证是否完成
Authentication authenticated = authenticateIfRequired();
try {
//资源权限认证
this.accessDecisionManager.decide(authenticated, object, attributes);
}
...
// Attempt to run as a different user
//修改保存在SecurityContext中的Authentication
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
...
}
//请求完毕后进行清理
protected void finallyInvocation(InterceptorStatusToken token) {
if (token != null && token.isContextHolderRefreshRequired()) {
...
SecurityContextHolder.setContext(token.getSecurityContext());
}
}
protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
finallyInvocation(token); // continue to clean in this method for passivity
if (afterInvocationManager != null) {
// Attempt after invocation handling
try {
//重点
returnedObject = afterInvocationManager.decide(token.getSecurityContext()
.getAuthentication(), token.getSecureObject(), token
.getAttributes(), returnedObject);
}
...
}
return returnedObject;
}
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
//判断身份认证是否完成
if (authentication.isAuthenticated() && !alwaysReauthenticate) {
return authentication;
}
//身份认证
authentication = authenticationManager.authenticate(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
...
}
其中有几个比较重要的参数,ConfigAttribute,RunAsManager,AfterInvocationManager。接下来简单分析一下。
AbstractSecurityInterceptor的beforeInvocation()方法内部在进行鉴权的时候使用的是注入的AccessDecisionManager的decide()方法进行的。decide()方法是需要接收一个受保护对象对应的ConfigAttribute集合的。一个ConfigAttribute可能只是一个简单的角色名称,具体将视AccessDecisionManager的实现者而定。AbstractSecurityInterceptor将使用一个SecurityMetadataSource对象来获取与受保护对象关联的ConfigAttribute集合。ConfigAttribute将通过注解的形式定义在受保护的方法上,或者通过access属性定义在受保护的URL上。例如我们常见的
就表示将ConfigAttribute ROLE_A和ROLE_B应用在所有的URL请求上。对于默认的AccessDecisionManager的实现,上述配置意味着用户所拥有的权限中只要拥有一个GrantedAuthority与这两个ConfigAttribute中的一个进行匹配则允许进行访问。当然,严格的来说ConfigAttribute只是一个简单的配置属性而已,具体的解释将由AccessDecisionManager来决定。
那么ConfigAttribute是从哪里配置的呢?
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
而obtainSecurityMetadataSource是个抽象方法。
public abstract SecurityMetadataSource obtainSecurityMetadataSource();
它的实现在FilterSecurityInterceptor
中
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
this.securityMetadataSource = newSource;
}
那么securityMetadataSource是什么时候被set进去的呢,这段逻辑在AbstractSecurityInterceptor
中
public void configure(H http) throws Exception {
FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
if (metadataSource == null) {
return;
}
//这里调用
FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
http, metadataSource, http.getSharedObject(AuthenticationManager.class));
if (filterSecurityInterceptorOncePerRequest != null) {
securityInterceptor
.setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);
}
securityInterceptor = postProcess(securityInterceptor);
http.addFilter(securityInterceptor);
http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
}
private FilterSecurityInterceptor createFilterSecurityInterceptor(H http,FilterInvocationSecurityMetadataSource metadataSource,AuthenticationManager authenticationManager) throws Exception {
FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor();
//这里将securityMetadataSource设置到子类中
securityInterceptor.setSecurityMetadataSource(metadataSource);
securityInterceptor.setAccessDecisionManager(getAccessDecisionManager(http));
securityInterceptor.setAuthenticationManager(authenticationManager);
securityInterceptor.afterPropertiesSet();
return securityInterceptor;
}
最后重点来了,configure方法来源于自己的配置。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/error")
.permitAll()
.and()
.logout()
.permitAll();
}
}
具体怎么转换的这里就不深究了。
在某些情况下你可能会想替换保存在SecurityContext中的Authentication。这由AccessDecisionManager调用RunAsManager来处理。在AbstractSecurityInterceptor的beforeInvocation()方法体中,在AccessDecisionManager鉴权成功后,将通过RunAsManager在现有Authentication基础上构建一个新的Authentication,如果新的Authentication不为空则将产生一个新的SecurityContext,并把新产生的Authentication存放在其中。
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
这样在请求受保护资源时从SecurityContext中获取到的Authentication就是新产生的Authentication。待请求完成后会在finallyInvocation()中将原来的SecurityContext重新设置给SecurityContextHolder。AbstractSecurityInterceptor默认持有的是一个对RunAsManager进行空实现的NullRunAsManager。
final class NullRunAsManager implements RunAsManager {
public Authentication buildRunAs(Authentication authentication, Object object,
Collection<ConfigAttribute> config) {
return null;
}
public boolean supports(ConfigAttribute attribute) {
return false;
}
public boolean supports(Class<?> clazz) {
return true;
}
}
此外,Spring Security对RunAsManager还有一个非空实现类RunAsManagerImpl.
public class RunAsManagerImpl implements RunAsManager, InitializingBean {
private String key;
private String rolePrefix = "ROLE_";
...
public Authentication buildRunAs(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
List<GrantedAuthority> newAuthorities = new ArrayList<>();
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
GrantedAuthority extraAuthority = new SimpleGrantedAuthority(
getRolePrefix() + attribute.getAttribute());
newAuthorities.add(extraAuthority);
}
}
if (newAuthorities.size() == 0) {
return null;
}
// Add existing authorities
newAuthorities.addAll(authentication.getAuthorities());
return new RunAsUserToken(this.key, authentication.getPrincipal(),
authentication.getCredentials(), newAuthorities,
authentication.getClass());
}
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute() != null
&& attribute.getAttribute().startsWith("RUN_AS_");
}
}
其在构造新的Authentication时是这样的逻辑:如果受保护对象对应的ConfigAttribute中拥有以“RUN_AS_”开头的配置属性,则在该属性前加上“ROLE_”,然后再把它作为一个GrantedAuthority赋给将要创建的Authentication(如ConfigAttribute中拥有一个“RUN_AS_ADMIN”的属性,则将构建一个“ROLE_RUN_AS_ADMIN”的GrantedAuthority),最后再利用原Authentication的principal、权限等信息构建一个新的Authentication进行返回;如果不存在任何以“RUN_AS_”开头的ConfigAttribute,则直接返回null。
在请求受保护的对象完成以后,可以通过afterInvocation()方法对返回值进行修改。AbstractSecurityInterceptor把对返回值进行修改的控制权交给其所持有的AfterInvocationManager,以根据需要实际修改对象。和AuthenticationProvider相似,AfterInvocationManager接口的实现类AfterInvocationProviderManager维护了一个AfterInvocationProvider列表,依次委托给AfterInvocationProvider接口的实现类进行修改返回值。
public class AfterInvocationProviderManager implements AfterInvocationManager,
InitializingBean {
private List<AfterInvocationProvider> providers;
...
public Object decide(Authentication authentication, Object object,
Collection<ConfigAttribute> config, Object returnedObject)
throws AccessDeniedException {
Object result = returnedObject;
//循环调用decide方法
for (AfterInvocationProvider provider : providers) {
result = provider.decide(authentication, object, config, result);
}
return result;
}
...
}
需要注意的是AfterInvocationManager需要在受保护对象成功被访问后才能执行。
AfterInvocationProvider提供了一个实现类PostInvocationAdviceProvider:
public class PostInvocationAdviceProvider implements AfterInvocationProvider {
public Object decide(Authentication authentication, Object object,
Collection<ConfigAttribute> config, Object returnedObject)
throws AccessDeniedException {
PostInvocationAttribute pia = findPostInvocationAttribute(config);
if (pia == null) {
return returnedObject;
}
return postAdvice.after(authentication, (MethodInvocation) object, pia,
returnedObject);
}
}
这里就不往下深究了,如果遇到需要修改返回值的情况在回来补上。总的来说,AfterInvocationManager可以选择对返回值进行修改、不修改或抛出异常(如:后置权限鉴定不通过)。
AbstractSecurityInterceptor提供了两个常用实现类,FilterSecurityInterceptor和MethodSecurityInterceptor,MethodSecurityInterceptor将用于调用受保护的方法,而FilterSecurityInterceptor将用于受保护的Web请求。它们在处理受保护对象的请求时都具有一致的逻辑,具体的逻辑如下。
FilterSecurityInterceptor上面已经分析过,接下来看一下MethodSecurityInterceptor部分源码:
public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements
MethodInterceptor {
public Object invoke(MethodInvocation mi) throws Throwable {
InterceptorStatusToken token = super.beforeInvocation(mi);
Object result;
try {
result = mi.proceed();
}
finally {
super.finallyInvocation(token);
}
return super.afterInvocation(token, result);
}
}
比较简单,就是运用了Spring Aop思想。
以下是Spring Security官方文档提供的一张关于AbstractSecurityInterceptor相关关系的图。
接下来就是重中之重的AccessDecisionManager
AccessDecisionManager负责鉴定用户是否有访问对应资源(方法或URL)的权限。AccessDecisionManager是一个接口,其中只定义了三个方法,其定义如下。
public interface AccessDecisionManager {
/**
* 通过传递的参数来决定用户是否有访问对应受保护对象的权限
*
* @param authentication 当前正在请求受包含对象的Authentication
* @param object 受保护对象,其可以是一个MethodInvocation、JoinPoint或FilterInvocation。
* @param configAttributes 与正在请求的受保护对象相关联的配置属性
**/
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)throws AccessDeniedException, InsufficientAuthenticationException;
//表示当前AccessDecisionManager是否支持对应的ConfigAttribute
boolean supports(ConfigAttribute attribute);
//表示当前AccessDecisionManager是否支持对应的受保护对象类型
boolean supports(Class<?> clazz);
}
decide()方法用于决定authentication是否符合受保护对象要求的configAttributes。supports(ConfigAttribute attribute)方法是用来判断AccessDecisionManager是否能够处理对应的ConfigAttribute的。supports(Class> clazz)方法用于判断配置的AccessDecisionManager是否支持对应的受保护对象类型。
Spring Security已经内置了几个基于投票的AccessDecisionManager,当然如果需要你也可以实现自己的AccessDecisionManager。以下是Spring Security官方文档提供的一个图,其展示了与基于投票的AccessDecisionManager实现相关的类。
使用这种方式,一系列的AccessDecisionVoter将会被AccessDecisionManager用来对Authentication是否有权访问受保护对象进行投票,然后再根据投票结果来决定是否要抛出AccessDeniedException。AccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。
public interface AccessDecisionVoter<S> {
intACCESS_GRANTED = 1;
intACCESS_ABSTAIN = 0;
intACCESS_DENIED = -1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}
vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。ACCESS_GRANTED表示同意,ACCESS_DENIED表示返回,ACCESS_ABSTAIN表示弃权。如果一个AccessDecisionVoter不能判定当前Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。
Spring Security内置了三个基于投票的AccessDecisionManager实现类,它们分别是AffirmativeBased、ConsensusBased和UnanimousBased。
AffirmativeBased的逻辑是这样的:
ConsensusBased的逻辑是这样的:
UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。UnanimousBased的逻辑具体来说是这样的:
AccessDecisionVoter是一个接口,主要定义了三个方法:
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
AccessDecisionVoter提供了很多实现类,这里只拿出比较常用的进行分析。
public class RoleVoter implements AccessDecisionVoter<Object> {
private String rolePrefix = "ROLE_";
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null)
&& attribute.getAttribute().startsWith(getRolePrefix())) {
return true;
}
else {
return false;
}
}
public boolean supports(Class<?> clazz) {
return true;
}
public int vote(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
if(authentication == null) {
return ACCESS_DENIED;
}
int result = ACCESS_ABSTAIN;
Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
// Attempt to find a matching granted authority
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
Collection<? extends GrantedAuthority> extractAuthorities(
Authentication authentication) {
return authentication.getAuthorities();
}
}
RoleVoter是Spring Security内置的一个AccessDecisionVoter,其会将ConfigAttribute简单的看作是一个角色名称,在投票的时如果拥有该角色即投赞成票。如果ConfigAttribute是以“ROLE_”开头的,则将使用RoleVoter进行投票。当用户拥有的权限中有一个或多个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute时其将投赞成票;如果用户拥有的权限中没有一个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute,则RoleVoter将投反对票;如果受保护对象配置的ConfigAttribute中没有以“ROLE_”开头的,则RoleVoter将弃权。
AuthenticatedVoter也是Spring Security内置的一个AccessDecisionVoter实现。其主要用来区分匿名用户、通过Remember-Me认证的用户和完全认证的用户。完全认证的用户是指由系统提供的登录入口进行成功登录认证的用户。
public class AuthenticatedVoter implements AccessDecisionVoter<Object> {
public static final String IS_AUTHENTICATED_FULLY = "IS_AUTHENTICATED_FULLY";
public static final String IS_AUTHENTICATED_REMEMBERED = "IS_AUTHENTICATED_REMEMBERED";
public static final String IS_AUTHENTICATED_ANONYMOUSLY = "IS_AUTHENTICATED_ANONYMOUSLY";
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null)
&& (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute())
|| IS_AUTHENTICATED_REMEMBERED.equals(attribute.getAttribute()) || IS_AUTHENTICATED_ANONYMOUSLY
.equals(attribute.getAttribute()))) {
return true;
}
else {
return false;
}
}
public boolean supports(Class<?> clazz) {
return true;
}
public int vote(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
int result = ACCESS_ABSTAIN;
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
if (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute())) {
if (isFullyAuthenticated(authentication)) {
return ACCESS_GRANTED;
}
}
if (IS_AUTHENTICATED_REMEMBERED.equals(attribute.getAttribute())) {
if (authenticationTrustResolver.isRememberMe(authentication)
|| isFullyAuthenticated(authentication)) {
return ACCESS_GRANTED;
}
}
if (IS_AUTHENTICATED_ANONYMOUSLY.equals(attribute.getAttribute())) {
if (authenticationTrustResolver.isAnonymous(authentication)
|| isFullyAuthenticated(authentication)
|| authenticationTrustResolver.isRememberMe(authentication)) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
}
AuthenticatedVoter可以处理的ConfigAttribute有IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED和IS_AUTHENTICATED_ANONYMOUSLY。如果ConfigAttribute不在这三者范围之内,则AuthenticatedVoter将弃权。否则将视ConfigAttribute而定,如果ConfigAttribute为IS_AUTHENTICATED_ANONYMOUSLY,则不管用户是匿名的还是已经认证的都将投赞成票;如果是IS_AUTHENTICATED_REMEMBERED则仅当用户是由Remember-Me自动登录,或者是通过登录入口进行登录认证时才会投赞成票,否则将投反对票;而当ConfigAttribute为IS_AUTHENTICATED_FULLY时仅当用户是通过登录入口进行登录的才会投赞成票,否则将投反对票。
AuthenticatedVoter是通过AuthenticationTrustResolver的isAnonymous()方法和isRememberMe()方法来判断SecurityContextHolder持有的Authentication是否为AnonymousAuthenticationToken或RememberMeAuthenticationToken的,即是否为IS_AUTHENTICATED_ANONYMOUSLY和IS_AUTHENTICATED_REMEMBERED。
当然,用户也可以通过实现AccessDecisionVoter来实现自己的投票逻辑。
到这里有关Spring Security认证和鉴权都介绍完了,前前后后写了3个星期,还是比较有收获的。
参考 https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#RunAsManager