1.情况说明
在搭建认证服务器,完成Spring Security Oauth2的四种模式的获取token的测试后,预示着我们已经将认证服务器搭建成功,紧接着我们搭建了一个用于测试的资源服务。相关的配置和测试接口如下。
1.1.资源服务配置
- 配置当前资源服务的信息;
- 配置当前资源服务验证token的方式。
@Configuration
@EnableResourceServer
//激活方法上的PreAuthorize注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//公钥
private static final String RESOURCE_ID = "eden-life";
/**
* 资源服务的安全配置
*
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId(RESOURCE_ID)
.tokenServices(tokenServices())
.stateless(true);
}
/***
* 配置资源服务的令牌验证服务
* @return
*/
@Bean
public ResourceServerTokenServices tokenServices() {
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setCheckTokenEndpointUrl("http://localhost:9949/oauth/check_token");
tokenServices.setClientId("eden");
tokenServices.setClientSecret("eden");
return tokenServices;
}
}
这里采用RemoteTokenService请求远端认证服务器的方式进行token的校验。因为我们资源服务和认证服务并不是在同一个服务里,而是分开的;我们也没有使用JWT来生成token,token只是随机的UUID字符串,没有包含用户信息等。
如果在一起,那我们可以使用DefaultTokenService的token服务方式。
1.2.资源服务的测试接口
我们提供两个测试接口,第一个是需要经过鉴权才可以访问的;第二个没有设置访问权限。
@RestController
@RequestMapping
public class TestController {
@GetMapping("/life/test/test1/{str}")
@PreAuthorize("hasAnyAuthority('admin')")
public String test(@PathVariable String str) {
return str;
}
@GetMapping("/life2/test/test2/{str}")
public String test2(@PathVariable String str) {
return str;
}
}
1.3.问题
按照正常理解,上面两个接口应该分别如此访问:
- 第一个接口,请求头需要携带token,且用户的权限里应该包含admin 权限;
- 第二个接口,可以直接访问,无需任何授权。
我们来测试一下。
2.测试
2.1.接口一
和猜测的一样,我们携带token顺利请求到了资源接口。
2.2.接口二
是不是有些意外?我们被Oauth2拦截了。
2.2.1.增加安全访问的限制策略
这里增加的配置的意思是,当前资源服务只有与/life/**路径相匹配的请求才会由Oauth2的过滤器链进行处理,其他请求路径不由Oauth2的过滤器链进行处理。
/**
* Http安全配置,对每个到达系统的http请求链接进行校验
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http
//限制资源服务直接管/life/**匹配的路径
.requestMatchers().antMatchers("/life/**")
.and()
.authorizeRequests()
.antMatchers("/life/**").authenticated()
.anyRequest().permitAll();
}
再次进行测试:
因为经过我们上一步的配置,Oauth2的过滤器链并不处理/life2的请求,所以这次使用的过滤器链其实是Security的过滤器链,在这个过滤器链中,抛出了AccessDeniedException。所以这一次, 我们是被Security拦截了。既然如此,那我们就增加Security的配置。
正常配置资源服务一般是需要Security配置的,我们只是为了引出来这个知识点,所以在一开始并未做这个配置。
2.2.2.增加Security配置
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class LifeWebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置http安全拦截机制
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
// /life/**的请求必须认证通过
.antMatchers("/life/**").authenticated()
.anyRequest().permitAll();
}
}
再次测试,我们发现已经可以访问到资源接口了。
为什么加了一个Security的配置就可以访问资源接口了呢?我们就来分析这个。
3.Debug分析
我们配置security和Oauth2的配置时,分别继承了一个Adapter,即WebSecurityConfigurerAdapter与ResourceServerConfigurerAdapter。下面借用一下一篇博文中的片段。
他们其实本质就是Adapter,只是二者所属的功能模块不同,而且二者的加载顺序是不一样的。
ResourceServerConfigurerAdapter是优先于WebSecurityConfigurerAdapter(这个特点要记住后面会用到,当然了这个顺序也是可以修改的!这个默认加载顺序情况下先加载的Adapter在处理相同路径情况下会覆盖后续加载的路径,造成后加载的失效!),那么我们这里WebSecurityConfigurerAdapter和ResourceServerConfigurerAdapter都存在那么我们系统中就会有两个Adapter,我们每声明一个*Adapter类,都会产生一个filterChain。
一个request(匹配url)只能被一个filterChain处理,这就解释了为什么二者Adapter同时在的时候处理相同请求路径时,后者默认为什么会失效的原因。我们可以在FilterChainProxy中的getFilters(HttpServletRequest request)方法中可以看到有哪些filter chain,并处理哪些url。
当发出接口二的请求时,我们可以在FilterChainProxy中看到有两个过滤器链。第一个是Oauth2的,第二个是security的,并且第一个可以拦截处理的路径为/life/**,第二个是拦截所有请求。Oauth2的过滤器链是在前面的。
我们的请求路径是/life2,经过匹配,第一个过滤器链肯定是不符合条件的,因此最终执行的是Security的过滤器链。
由于Security配置了对/life/**之外的请求全部放行,我们也就能顺利请求到资源接口。
4.Oauth2的过滤器链
对于Oauth2来说,他的过滤器链中最重要的过滤器是OAuth2AuthenticationProcessingFilter。他的主要操作我们可以看一下,doFilter方法中一进来就是要获取请求头中的的token,但是请求头中并没有,继续放行。
直至最终到了FilterSecurityIntercepter,抛出了AccessDeniedException。
ExceptionTranslationFilter捕获AccessDeniedException,并最做了最终的响应处理。
如果请求头中没有token,走Oauth2的过滤器链是根本走不通的。
那么什么情况下,才会不走OAuth2的过滤器链呢?也就是在请求和过滤器链不能match的时候。
5.结论
- 资源服务要配置需要保护的、明确的、合理范围的资源路径,不写或者写/**都可能造成问题。
如下图圈出的位置:
因为Oauth2的过滤器链的顺序在前,所以如果你不在ResourceServerConfig中设置当前资源服务所保护的路径,那Oauth2就会对所有资源都进行拦截,但有些资源确实是不需要让这个资源服务进行处理的,所以会造成异常的授权判断。
2.当资源服务的过滤器链都不能处理时,才会轮到Security的过滤器链,当然得是能match上才行。
所以,如果你发现自己的资源服务的接口控制不符合预期,或者感觉配置的Security配置好像没有生效似的,那么很有可能就是因为没有用到Security的过滤器链。建议你将Security的安全访问策略的配置挪到资源服务的配置上去,或者两个config都配置上。