TodoItem
OAuth2标准研究 todo
AuthorizationServerConfigurerAdapter vsWebSecurityConfigurerAdapter todo
判断是否超时的逻辑 todo
CORS todo
JSR-250 todo
Basic Knowledge
很好的基础教程
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
120 = {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 | 0 = {OrderedGatewayFilter@13342} "OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@25a2c4dc}, order=-2147483648}" |
最终默认情况下是通过DefaultTokenServices里面的createAccessToken方法调用到RedisTokenStore的storeAccessToken方法对token一系列的详细信息存储到redis里面。
Spring Security默认的过滤器栈
1 | 0 = {WebAsyncManagerIntegrationFilter@12983} |
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
20catch (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
3tokens = {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
6security:
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
17private 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,这样子比较灵活。