TodoItem OAuth2标准研究 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}
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,这样子比较灵活。