0%

Spring security的认证和鉴权

简介

Spring security 总体分为认证和鉴权两部分,认证我们可以理解为对账号密码的验证,鉴权为用户能不能访问资源,我们可以通过实现Spring security的接口来实现自定义的认证和鉴权。Spring security的本质是一连串的拦截器,我们可以拦截器链中加入自定义的拦截器来实现自己的逻辑.

认证部分

下面介绍认证的核心接口和概念

Authentication

此接口负者存储要认证的具体信息,主要是将认证的账号密码还有权限等信息存放在其中,比如前端传入账号和密码给后端验证,那么需要将账号和密码封装进实现Authentication接口的认证类中,然后将认证信息传给Spring security拦截器链,由Spring security调用认证和鉴权方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface Authentication extends Principal, Serializable {

//此账号具有哪些角色或者权限
Collection<? extends GrantedAuthority> getAuthorities();

//认证信息,可以理解为密码
Object getCredentials();

Object getDetails();

//认证主体,如账号
Object getPrincipal();

//这个Authentication是否已经认证过
boolean isAuthenticated();

//设置Authentication是否已经认证过
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
AuthenticationManager

在框架中AuthenticationManager负责认证,一般我们使用Spring security自带实现类ProviderManager

1
2
3
4

public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}

ProviderManager的authenticate(Authentication var1)方法的实现,我们可以看到起主要逻辑是循环调用List providers 中的AuthenticationProvider下的authenticate(Authentication authentication)方法,如果其中有一个AuthenticationProvider认证成功则返回,那么这里认证的逻辑就已经大概清晰,Spring Security 依托ProviderManager的实现去完成认证,而ProviderManager的主要实现类是ProviderManager,ProviderManager中又有一个AuthenticationProvider对象的集合,其负责具体的验证逻辑,如果有一个认证通过,则之前存在Authentication中的信息就认证成功,我们要做的就是实现自己的AuthenticationProvider认证,将其加入到ProviderManager的认证集合List providers中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

private List<AuthenticationProvider> providers;

//...省略其余代码

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;
boolean debug = logger.isDebugEnabled();
Iterator var8 = this.getProviders().iterator();

while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}

try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (InternalAuthenticationServiceException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}

if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var11) {
;
} catch (AuthenticationException var12) {
parentException = var12;
lastException = var12;
}
}

if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((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;
}
}
AuthenticationProvider

上文提到了AuthenticationProvider是负责具体认证的地方,它的代码如下authenticate方法负责接收一个Authentication类,如果认证成功则返回一个Authentication对象,supports(Class<?> var1)负责判定这个AuthenticationProvider可以鉴定什么类型的Authentication,我们可以实现自己的Authentication,和AuthenticationProvider,从而使我们自定义的AuthenticationProvider只去鉴定特定的Authentication

1
2
3
4
5
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;

boolean supports(Class<?> var1);
}
UserDetailsService

UserDetailsService是Spring security抽象的接口其方法如下,其作用是根据参数获取一个用户属性UserDetails,UserDetails为Spring secur为用户做的一个抽象。

1
2
3
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

在网上我们看到的很多教程叫我们只要自定义实现自己的 UserDetailsService就可以完成认证,这是因为Spring security默认使用的Authentication 是UsernamePasswordAuthenticationToken,而默认对UsernamePasswordAuthenticationToken进行认证的又是AuthenticationProvider 是DaoAuthenticationProvider,下面我们查看DaoAuthenticationProvider源码发现以下代码,其中this.getUserDetailsService().loadUserByUsername就是调用UserDetailsService去获取UserDetailsService对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();

try {
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);
}
}

那retrieveUser又具体做了什么呢,我们查看DaoAuthenticationProvider父类AbstractUserDetailsAuthenticationProvider中的代码,其中以下最重要的一段代码this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);这段代码的实现在DaoAuthenticationProvider中,且public boolean supports(Class<?> authentication) 表明了其只验证实现了UsernamePasswordAuthenticationToken的Authentication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

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;
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);
}



public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

DaoAuthenticationProvider中我们找到additionalAuthenticationChecks方法,这里的逻辑就很清晰了,将UserDetails中的值和authentication中的值做比对,如果不匹配抛出AuthenticationException异常由spring security去处理异常

1
2
3
4
5
6
7
8
9
10
11
12
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
拦截器

上文提到我们登陆的时候需要把我们登陆的信息封装成Authentication对象,再由Spring security框架去认证,前文还说到Spring Security的本质就是一连串的拦截器,那我们实现的思路就是这样,我们自定义一个拦截器,拦截登陆的信息,将账号密码组成Authentication对象,再交由Spring security对象去认证即可,之后以我们可以不自定义上述的类就可以使用是因为Spring security中默认加入了一个UsernamePasswordAuthenticationFilter拦截器,且提供了默认实现的DaoAuthenticationProvider.我们查看UsernamePasswordAuthenticationFilter的源码,发现其只是将前传送进来的账号密码组成一个UsernamePasswordAuthenticationToken ,我们注意到this.getAuthenticationManager().authenticate(authRequest)这段代码,这段代码就是调用ProviderManager去完成对Authentication的认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}

下面我们再查看UsernamePasswordAuthenticationToken的父类AbstractAuthenticationProcessingFilter,发现其调用了UsernamePasswordAuthenticationToken的attemptAuthentication方法,并且将UsernamePasswordAuthenticationToken放入SecurityContextHolder中的SecurityContext里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
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 {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}

this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}

if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}

this.successfulAuthentication(request, response, chain, authResult);
}
}


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);
}

SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}

this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

默认的SecurityContext是由ThredLocal生成,也就是说在同一个线程下我们都可以通过SecurityContextHolder.getContext().getAuthentication()获取到Authentication或者通过 SecurityContextHolder.getContext().setAuthentication()设置Authentication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();

ThreadLocalSecurityContextHolderStrategy() {
}

public void clearContext() {
contextHolder.remove();
}

public SecurityContext getContext() {
SecurityContext ctx = (SecurityContext)contextHolder.get();
if (ctx == null) {
ctx = this.createEmptyContext();
contextHolder.set(ctx);
}

return ctx;
}

public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}

public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}

在本人的自行实现的拦截器中并没有调用this.getAuthenticationManager().authenticate(authRequest)方法,因为本人只实现了OncePerRequestFilter类,并没有实现Spring security提供的AbstractAuthenticationProcessingFilter抽象类,但是仍然可以实现认证,因为在后面的鉴权拦截器中我实现了AbstractSecurityInterceptor类该中有一段代码Authentication authenticated = this.authenticateIfRequired();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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 {
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));
}

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());
SecurityContextHolder.getContext().setAuthentication(runAs);
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
} 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;
}
}
}

进入 this.authenticateIfRequired();我们发现Authentication的isAuthenticated状态时false时在这里将会完成再次认证,所以我们自定义认证完Authentication后必须将Authentication的isAuthenticated的状态设置为true,避免后续拦截器再次去认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Previously Authenticated: " + authentication);
}

return authentication;
} else {
authentication = this.authenticationManager.authenticate(authentication);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Successfully Authenticated: " + authentication);
}

SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
}
认证异常处理

在认证过程中出现认证失败,不管是我们自定义的认证类和Spring security 自带的实现类都抛出AuthenticationException异常,这个异常可以统一由Spring security捕获,从而处理认证异常

认证配置

以下配置中关于认证的我们只需要注意几点

  • public void configure(AuthenticationManagerBuilder authenticationManagerBuilder)
    此方法下往ProviderManager中配置了AuthenticationProvider认证器,并且设置了UserDetailService类和设置了认证时密码不加密

  • public void configure(HttpSecurity http)
    此方法配置了认证的具体逻辑,http.addFilterBefore(validateFilter, UsernamePasswordAuthenticationFilter.class)将我们自定义实现的拦截器加入到UsernamePasswordAuthenticationFilter之前,UsernamePasswordAuthenticationFilter是Spring security中默认最后一个负责认证的拦截器,加入的拦截器负责组装前台提交的账号和密码,将其转换成Authentication。
    .exceptionHandling().authenticationEntryPoint负责配置认证异常,所有认证抛出的AuthenticationException及其实现都会在这里配置的AuthenticationEntryPointd 的实现类中处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115


/**
* @author Liush
* @description
* @date 2019/11/12 16:06
**/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


@Autowired
private CustomLoginSuccessHandler customLoginSuccessHandler;

@Autowired
private CustomLoginFailHandler customLoginFailHandler;

@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;


@Autowired
private JWTUtil jwtUtil;


@Autowired
private SecurityProperties securityProperties;


@Autowired
private DbUserDetailsService dbUserDetailsService;

@Autowired
private PasswordProvider passwordProvider;

@Autowired
private TokenProvider tokenProvider;

@Autowired
private CustomEntryPointHandler customEntryPointHandler;


@Autowired
private IdentityUserServiceI identityUserService;

@Autowired
private RoleAccessDeniedHandler roleAccessDeniedHandler;

@Autowired
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(dbUserDetailsService).
passwordEncoder(NoOpPasswordEncoder.getInstance());
authenticationManagerBuilder.authenticationProvider(passwordProvider);
authenticationManagerBuilder.authenticationProvider(tokenProvider);


}


@Bean
public RoleAccessSecurityInterceptor roleAccessSecurityInterceptor(){
List<AccessDecisionVoter<? extends Object>> voters=new ArrayList<>();
voters.add(new RoleAccessDecisionVoter());
AccessDecisionManager accessDecisionManager=new RoleDecisionManager(voters);
RoleMetadataSource roleMetadataSource=new RoleMetadataSource(jwtUtil,identityUserService);
return new RoleAccessSecurityInterceptor(securityProperties.getWhiteUrls(),accessDecisionManager,roleMetadataSource);

}




@Override
public void configure(HttpSecurity http) throws Exception {
ValidateFilter validateFilter = new ValidateFilter(securityProperties.getLogin_url(),securityProperties.getWhiteUrls(), jwtUtil, customEntryPointHandler);
http.addFilterBefore(validateFilter, UsernamePasswordAuthenticationFilter.class)
//设置认证异常处理器
.exceptionHandling().authenticationEntryPoint(customEntryPointHandler)
//设置鉴权异常处理器
.accessDeniedHandler(roleAccessDeniedHandler)
.and()
.cors()
.and()
.csrf().disable();

whiteUrlConfig(http);
//设置鉴权拦截器
http.addFilterAfter(roleAccessSecurityInterceptor(),FilterSecurityInterceptor.class);
}

/**
* 白名单配置,调用permitAll方法,此url下的连接可以进入security拦截器,但是不鉴权
*/
private void whiteUrlConfig(HttpSecurity http) throws Exception {
List<String> whiteUrls= WhiteUrlUtil.createWhiteUrls(securityProperties.getWhiteUrls());
for (String url:whiteUrls){
http.authorizeRequests().antMatchers(url).permitAll();

}
http.authorizeRequests().anyRequest().authenticated();

}




/* @Override
public void configure(HttpSecurity http) throws Exception{
http.authorizeRequests().anyRequest().permitAll();


}*/


}

鉴权部分

鉴权就是对用户可以做什么进行判断

FilterInvocation

API中给的解释是Holds objects associated with a HTTP filter.意思就是其是保存HTTP过滤器的地方,我们必须在鉴权拦截器中生成FilterInvocation对象,并将FilterInvocation对象传入拦截器链,从而获取http中的信息,因为鉴权很多是和url做关联的,比如什么角色能访问什么url

SecurityMetadataSource

API中的解释是:提供ConfigAttribute的类实现。

1
2
3
4
5
6
7
public interface SecurityMetadataSource extends AopInfrastructureBean {
Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;

Collection<ConfigAttribute> getAllConfigAttributes();

boolean supports(Class<?> var1);
}

下面是本人对于这个方法的实现,入参Object var1,是一个FilterInvocation,通过拿到request中的信息,查询到该次请求需要哪些权限(一般是直接获取请求的url,然后去查询这次url访问需要哪些权限),将查询到的需要的权限封装成Collection ,供后续代用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
HttpServletRequest request = ((FilterInvocation) object).getRequest();
String powerId=request.getHeader("powerId");
//查询powerId下需要的角色
List<RoleDTO> roles=identityUserService.findRoleByPowerId(powerId);
if(roles==null || roles.isEmpty()){
throw new AccessDeniedException("该权限id不存在");
}
List<ConfigAttribute> configAttributes=new ArrayList<>();

for(RoleDTO role:roles){
configAttributes.add(new RoleConfigAttribute(role.getAuthority()));
}
return configAttributes;
}
ConfigAttribute

API中的解释:存储与安全系统相关的配置属性

1
2
3
public interface ConfigAttribute extends Serializable {
String getAttribute();
}
AccessDecisionManager

API中的解释:做出最终访问控制(授权)决定。

1
2
3
4
5
6
7
public interface AccessDecisionManager {
void decide(Authentication var1, Object var2, Collection<ConfigAttribute> var3) throws AccessDeniedException, InsufficientAuthenticationException;
//判断决策管理器时候支持此属性的验证,关于这两个类的实现可参考Spiring security的默认实现
boolean supports(ConfigAttribute var1);

boolean supports(Class<?> var1);
}

本人实现,获取AbstractAccessDecisionManager中AccessDecisionVoter,进行投票如果有一个AccessDecisionVoter投票为1说明有权限,如果鉴权失败抛出AccessDeniedException异常,由SpringSecurity统一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class RoleDecisionManager extends AbstractAccessDecisionManager {

protected RoleDecisionManager(List<AccessDecisionVoter<?>> decisionVoters) {
super(decisionVoters);
}

@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
int deny = -1; //如果等于-1验证失败,如果等于1验证成功
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
//通过验证
case AccessDecisionVoter.ACCESS_GRANTED:{
deny=1;
break;
}
//验证失败
case AccessDecisionVoter.ACCESS_DENIED:
continue;

default:
}
}

if (deny !=1) {
throw new AccessDeniedException("没有权限调用此功能");
}

// 如果所有投票者都弃权的话
//checkAllowIfAllAbstainDecisions();
}



/**
* 被AbstractSecurityInterceptor调用,遍历ConfigAttribute集合,筛选出不支持的attribute
*/
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

/**
* 被AbstractSecurityInterceptor调用,验证AccessDecisionManager是否支持这个安全对象的类型。
*/
@Override
public boolean supports(Class<?> clazz) {
return true;
}


}
AccessDecisionVoter

API中的解释:负责对授权决策进行投票 ,int vote(Authentication var1, S var2, Collection var3)如果有权限返回1,没有权限返回-1

1
2
3
4
5
6
7
8
9
10
11
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;

boolean supports(ConfigAttribute var1);

boolean supports(Class<?> var1);

int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}
AbstractSecurityInterceptor

API中的解释:为安全对象实现安全拦截的抽象类

整体认证逻辑

首先我们实现自己的AbstractSecurityInterceptor,在拦截器中生成FilterInvocation对象,调用AbstractSecurityInterceptor的super.beforeInvocation(roleFilterInvocation)完成鉴权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

/**
* @author Liush
* @description 鉴权拦截器
* @date 2019/11/17 19:39
**/
public class RoleAccessSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

private String whiteUrls;
private FilterInvocationSecurityMetadataSource securityMetadataSource;

public RoleAccessSecurityInterceptor(String whiteUrls, AccessDecisionManager decisionManager,FilterInvocationSecurityMetadataSource securityMetadataSource) {
if(StringUtils.isEmpty(whiteUrls)){
whiteUrls= "";
}
this.whiteUrls = whiteUrls;
super.setAccessDecisionManager(decisionManager);
this.securityMetadataSource=securityMetadataSource;
}




@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

//查看是否白名单,如果是的话不认证
if(WhiteUrlUtil.isWhite((HttpServletRequest) servletRequest,whiteUrls)){
filterChain.doFilter(servletRequest,servletResponse);
return;
}

FilterInvocation roleFilterInvocation=new FilterInvocation(servletRequest,servletResponse,filterChain);
InterceptorStatusToken token=super.beforeInvocation(roleFilterInvocation);
try{
roleFilterInvocation.getChain().doFilter(servletRequest,servletResponse);
}finally {
super.afterInvocation(token,null);
}


}






@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}

@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return securityMetadataSource;
}


}

我们进入super.beforeInvocation(roleFilterInvocation)方法,
Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object);此方法调用SecurityMetadataSource来获取,该次调用需要获取什么权限
this.authenticateIfRequired()上面文章提到如果之前如果Authenticate的isAuthenticated为false的话会在这里再次鉴权
this.accessDecisionManager.decide(authenticated, object, attributes);调用AccessDecisionManager进行鉴权投票,如果没有抛出AccessDeniedException异常则鉴权成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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 {
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));
}

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());
SecurityContextHolder.getContext().setAuthentication(runAs);
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
} 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;
}
}
}

注意

在WebSecurityConfigurerAdapter配置类下配置的白名单
http.authorizeRequests().antMatchers(url).permitAll(),这里配置的url仍然会进入拦截器,所以我在进入拦截器前先会去查找白名单,如果是白名单直接跳过拦截器