0%

Spring系列之SpringSecurity

TodoItem

OAuth2标准研究 todo

AuthorizationServerConfigurerAdapter vsWebSecurityConfigurerAdapter todo

判断是否超时的逻辑 todo

CORS todo

JSR-250 todo

Basic Knowledge

很好的基础教程

Security with Spring

token值在哪里生成?怎么生成?

默认鉴权生成token的url是”/oauth/token”,在TokenEndpoint里面生成。
SpringSecurity默认的token生成url是:http://hostname:port/oauth/token
但是要进入到真正的TokenEndpoint里面还需要经过一道道关卡:

1
2
3
4
5
6
7
8
9
10
11
12
0 = {WebAsyncManagerIntegrationFilter@13114} 
1 = {SecurityContextPersistenceFilter@13113}
2 = {HeaderWriterFilter@13112}
3 = {LogoutFilter@13111}
4 = {ClientCredentialsTokenEndpointFilter@13110}
5 = {BasicAuthenticationFilter@13108}
6 = {RequestCacheAwareFilter@13248}
7 = {SecurityContextHolderAwareRequestFilter@13247}
8 = {AnonymousAuthenticationFilter@13246}
9 = {SessionManagementFilter@13245}
10 = {ExceptionTranslationFilter@13244}
11 = {FilterSecurityInterceptor@13243}
1
2
3
4
5
6
7
8
9
10
11
12
13
0 = {OrderedGatewayFilter@13342} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@25a2c4dc}, order=-2147483648}"
1 = {OrderedGatewayFilter@13343} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@62dfe152}, order=-2147482648}"
2 = {OrderedGatewayFilter@13344} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=me.fengorz.kiwi.gateway.filter.GenericRequestGlobalFilter@430f0c63}, order=-1000}"
3 = {OrderedGatewayFilter@13345} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@1e75af65}, order=-1}"
4 = {OrderedGatewayFilter@13346} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@2bee1c13}, order=0}"
5 = {OrderedGatewayFilter@13347} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.GatewayMetricsFilter@71098fb3}, order=0}"
6 = {OrderedGatewayFilter@13936} "OrderedGatewayFilter{delegate=me.fengorz.kiwi.gateway.filter.ValidateCodeGatewayFilter$$Lambda$854/120561697@1f06c463, order=1}"
7 = {OrderedGatewayFilter@14156} "OrderedGatewayFilter{delegate=me.fengorz.kiwi.gateway.filter.PasswordDecoderGatewayFilter$$Lambda$855/473170143@13f576a8, order=2}"
8 = {OrderedGatewayFilter@13349} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@29d81c22}, order=10000}"
9 = {OrderedGatewayFilter@13350} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@25a52a60}, order=10100}"
10 = {OrderedGatewayFilter@13351} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@1859b996}, order=2147483646}"
11 = {OrderedGatewayFilter@13352} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@15549dd7}, order=2147483647}"
12 = {OrderedGatewayFilter@13353} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@18d396eb}, order=2147483647}"

最终默认情况下是通过DefaultTokenServices里面的createAccessToken方法调用到RedisTokenStore的storeAccessToken方法对token一系列的详细信息存储到redis里面。

Spring Security默认的过滤器栈

1
2
3
4
5
6
7
8
9
10
11
12
0 = {WebAsyncManagerIntegrationFilter@12983} 
1 = {SecurityContextPersistenceFilter@12982}
2 = {HeaderWriterFilter@12981}
3 = {LogoutFilter@12980}
4 = {ClientCredentialsTokenEndpointFilter@12979}
5 = {BasicAuthenticationFilter@12977}
6 = {RequestCacheAwareFilter@13387}
7 = {SecurityContextHolderAwareRequestFilter@13386}
8 = {AnonymousAuthenticationFilter@13385}
9 = {SessionManagementFilter@13384}
10 = {ExceptionTranslationFilter@13383}
11 = {FilterSecurityInterceptor@13382}

AuthorizationServerSecurityConfigurer

allowFormAuthenticationForClients():
在BasicAuthenticationFilter之前添加clientCredentialsTokenEndpointFilter。

Problem Solution

maven依赖出现了不同版本的TokenEndpoint

原因是Spring自己的依赖冲突了,在父工程根目录的dependencyManagement里面添加

1
2
3
4
5
6
<!--稳定版本,替代spring security bom内置-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${security.oauth.version}</version>
</dependency>

权限校验的配置不能冲突

比如我在某个配置类配置了:

这样会导致全局的权限放开失效:

1
2
3
4
# 直接放行URL
ignore:
urls:
- /EnhancerTokenEndpoint/**

全局的安全配置一般放在common-security模块:

Spring Security默认不会打印debug日志

可通过配置类打开:

1
2
3
4
@Override
public void configure(WebSecurity web) throws Exception {
web.debug(true);
}

但是这种配置类打印的error日志不全,很多时候一些深层次的报错是不会打印出来的,因为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();

if (debug) {
this.logger.debug("Authentication request for failed: " + failed);
}

this.rememberMeServices.loginFail(request, response);

onUnsuccessfulAuthentication(request, response, failed);

if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, failed);
}

return;
}

类似这样的代码都有判断if(debug),这种事因为spring security默认用的是logback-classic,需要在classpath加上logback-spring.xml日志打印配置文件。

通过postman调用接口token验证不通过

我有二个微服务应用,一个是可以正常验证通过,另一个不能,这种问题只能跟源码分析了,最开始发现问题的端倪是:
接口请求之后,SpringSecurity会自动将严重的token转发给http://localhost:9991/auth/oauth/token在内部做验证,通过的话再继续走业务接口。
http://localhost:9991/auth/oauth/token验证接口需要经过一些列的过滤器,上面有提到。
在BasicAuthenticationFilter的doFilterInternal方法中对:

1
String header = request.getHeader("Authorization");

这个Authorization进行解密,异常的应用解密出来是:

1
2
3
tokens = {String[2]@15312} 
0 = "null"
1 = "5951061b-856f-47cf-8f1b-dc34f975438c"

这里null就出现问题了,因为正常的那个不会是null,所以要研究一下转发到http://localhost:9991/auth/oauth/token之前塞进Header里面的Authorization是怎么来的?
于是只能先从SpringSecurity的过滤器栈中每个过滤器跟起,最终发现是在OAuth2AuthenticationProcessingFilter的doFilter方法的这一行:

1
Authentication authResult = authenticationManager.authenticate(authentication);

发现了这里的authentication里面的getCredentials()返回是null,这才导致了上面转发到http://localhost:9991/auth/oauth/token验证token的时候出现0 = “null”,继续跟踪getCredentials()的来源,然后发现credentials是依赖于RemoteTokenServices的clientId属性,getPrincipal()依赖的是clientSecret属性,接着跟一下RemoteTokenServices是在哪里被注入到Spring的,发现其注入如下:

1
2
3
4
5
6
7
8
         @Bean
public RemoteTokenServices remoteTokenServices() {
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl(this.resource.getTokenInfoUri());
services.setClientId(this.resource.getClientId());
services.setClientSecret(this.resource.getClientSecret());
return services;
}

this.resource,属性如下:

1
2
3
4
5
6
7
8
9
10
11
@ConfigurationProperties(prefix = "security.oauth2.resource")
public class ResourceServerProperties implements BeanFactoryAware, InitializingBean {

@JsonIgnore
private final String clientId;

@JsonIgnore
private final String clientSecret;

...
}

这就和明确了,yml或者properties没有配置这二个对应的属性,于是配置上,这里采用了jasypt的加密方式:

1
2
3
4
5
6
security:
oauth2:
client:
client-id: ENC(wORgugqWfXlIuzbal/3pjXTNXij/RSpo)
client-secret: ENC(rMd1buB3iI+si+W99eB+QFa3QburIEmY)
scope: server

本来以为这个就正常了,结果报了新的错误:

1
Caused by: java.lang.IllegalArgumentException: Authorities must be either a String or a Collection

debug了一下,发现上面的client-id和client-secret是注入成功的,那么异常应该是出现在其他地方,继续跟进。
最终发现,是在EnhancerUserAuthenticationConverter这个token认证转换器中的extractAuthentication方法报错,报错原因是因为admin用户没有赋权,在这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
Object authorities = map.get(AUTHORITIES);

List<String> userNames = this.filterIgnorePropertiesConfig.getUserNames();
if (CollUtil.contains(userNames, map.get(SecurityConstants.DETAILS_USERNAME))) {
authorities = CommonConstants.EMPTY;
}

if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.collectionToCommaDelimitedString((Collection<?>) authorities));
}
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}

这样如果直接写死代码忽略admin用户的话代码就太硬了,于是通过yml配置映射到配置独享filterIgnorePropertiesConfig,这样子比较灵活。