解决SpringSecurity访问控制与SpringSecurityOAuth2资源服务冲突,分割OAuth2接口、普通接口的认证鉴权逻辑
首先我们知道,在OAuth2的设计理念中,服务有着 Resource Server 和 Authorization Server 两种定位。
那么这里有个问题,SpringSecutiryOAuth2的设计默认你的认证服务、资源服务、Client均为不同的应用,如果你想将其混合在一起使用,比如将 Resource Server & Authorization Server 安排在同一个程序上,原生流程可能会产生一些意料之外、且难以处理的bug。
根据我的使用来看,认证服务和资源服务放一起其实没什么问题,有办法兼容。
但是如果你的资源服务要和一个基于SpringSecurity的 web 应用结合的话,这时就会出问题了。
如下是一个很标准的 Resource Server 配置,具体的来说它做了两件事:1、设置本资源服务的资源ID,作为标识作用。2、让所有以 /open
开头的路径都被拦截,需要认证后才能访问
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private static final String CURRENT_RESOURCE_ID = SecurityConstant.OAUTH_RESOURCE; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(CURRENT_RESOURCE_ID).stateless(true); } @Override public void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() .antMatchers("/open/**").authenticated();//访问控制,必须认证过后才可以访问 } }
配置完后,实现了效果么?很完美的实现了效果。
然而这仅是对于一个纯Resource Sever来说,按我的项目来看,这里的Resource Server 本身也是一个可供外界访问的 Web 服务(有一套已经基于SpringSecurity完成的认证鉴权流程),OAuth2 这一套东西是作为一个 feature 新加上去的后来者,此时就出现bug了。
表现为:
SpringSecutiryOAuth2内置拦截器检查全部请求,导致并不想被OAuth2逻辑侵入的接口仍会被置于上述代码的访问控制之下,从而使原本正常访问的接口无法访问(因为需要Resource Server的鉴权通过)。
要实现的是什么?
是要将 OAuth2 相关接口的认证鉴权 与 Web 服务的认证鉴权在逻辑上分割,互不影响。
我秉持着能框架原生拓展实现,就绝不自定义侵入代码的设计理念,研究了好几天,把SpringSecurity的安全配置逻辑又重新复习了一边,也把SpringSecutiryOAuth2里相关源码都看了。
遗憾的是,我没有寻找到一个可以借助框架提供的扩展接口实现两者兼容的方法。
只能说能力不足,或者其实能简单实现,只是我漏掉了一些拓展,反正我确实绕不过去 @EnableResourceServer
,具体是 ResourceServerConfiguration
这个配置中的一些逻辑。
没办法,业务逻辑还是需要实现,不过这时我也只能另辟蹊径,既然你自动配置的OAuth2访问控制没法子和SpringSecurity的访问控制兼容,那我就对着你源码照猫画虎一顿抄,基于已存在的系统自个实现你那一套拦截鉴权也可以。
SpringSecutiryOAuth2的拦截源码看起来很简单,说白了都是基于Servlet拦截器的,我们找到那个最终对请求拦截的拦截器就行。
配置栈如下所示
@EnableResourceServer
@Import
–>ResourceServerConfiguration
ResourceServerConfiguration#configure
ResourceServerSecurityConfigurer#configure
- add filter:
org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter
public final class ResourceServerSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { //…… @Override public void configure(HttpSecurity http) throws Exception { //用于访问控制(鉴权)的认证管理器 AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http); //这里就是创建、填充拦截器 resourcesServerFilter = new OAuth2AuthenticationProcessingFilter(); resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint); resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager); if (eventPublisher != null) { resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher); } if (tokenExtractor != null) { resourcesServerFilter.setTokenExtractor(tokenExtractor); } resourcesServerFilter = postProcess(resourcesServerFilter); resourcesServerFilter.setStateless(stateless); // @formatter:off //配置注解拦截机制、设置拦截器位置顺序、拒绝策略、访问端点 http .authorizeRequests().expressionHandler(expressionHandler) .and() .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class) .exceptionHandling() .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint); // @formatter:on } //…… }
在链路最后,组装一个 OAuth2AuthenticationProcessingFilter 拦截器,配置到整个SpringSecurity拦截链中,OAuth2相关配置就完成了。
OAuth2AuthenticationProcessingFilter#doFilter
是实际检查请求的逻辑,源码精简如下所示。
//从请求体中抽取Token(access token) Authentication authentication = tokenExtractor.extract(request); //客户端携带了令牌,先把令牌详情设置到请求里。这里的参数与后续buildDetails的Token检查有关 request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal()); //创建新的身份验证详细信息实例,填充进认证主体 AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication; needsDetails.setDetails(authenticationDetailsSource.buildDetails(request)); //提交一个文章的认证主体,使用AuthenticationManager进行认证,可能会抛出 OAuth2 认证相关异常(认证失败) authenticate = (OAuth2Authentication) authenticationManager.authenticate(authentication); //认证完后发布认证成功的事件、将认证主体设置到SpringSecurity的上下文中 eventPublisher.publishAuthenticationSuccess(authResult); SecurityContextHolder.getContext().setAuthentication(authResult);
我们若要将 Resource Server 集成到现有已存在SpringSecurity的服务中,只需要自行配置一个拦截器,并实现上述代码的逻辑,就能够在应用的安全相关启动配置(SpringSecurity配置类)将拦截器组装完毕后,通过 configure(HttpSecurity http) 方法使用 http.addFilterBefore 把自定义的拦截器置入。
在拦截器中加入接口路径的判定,就可以完美实现效果。
即将 OAuth2 相关接口的认证鉴权 与 Web 服务的认证鉴权,完全的在逻辑上分割开来。
我的拦截器代码,仅参考用,删除了部分敏感操作,以及注释。
/** * OAuth2 Resource server 授权过滤器 * 所有的OAuth2请求都会经过此类 * 认证检查,设置上下文 * * @see org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter#doFilter */ public class OAuth2AuthenticationProcessingFilter implements Filter { private final Logger logger = LoggerFactory.getLogger(getClass()); private final RequestMatcher requestMatcher; private final TokenExtractor tokenExtractor = new BearerTokenExtractor(); private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource(); private SecurityExceptionTerminator securityExceptionTerminator = new SecurityExceptionTerminator(); private final TokenStore tokenStore; private final ClientDetailsService clientDetailsService; private final AuthenticationManager authenticationManager; public OAuth2AuthenticationProcessingFilter(String path, TokenStore tokenStore, ClientDetailsService clientDetailsService) { this.requestMatcher = new AntPathRequestMatcher(path); this.tokenStore = tokenStore; this.clientDetailsService = clientDetailsService; authenticationManager = oauthAuthenticationManager(); } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) res; if (!requestMatcher.matches(request)) { chain.doFilter(request, response); return; } Authentication authentication = tokenExtractor.extract(request); if (authentication == null) { authenticationFailure(request, response, "No token"); return; } OAuth2Authentication authenticate; try { request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal()); AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication; needsDetails.setDetails(authenticationDetailsSource.buildDetails(request)); authenticate = (OAuth2Authentication) authenticationManager.authenticate(authentication); } catch (RuntimeException e) { authenticationFailure(request, response, e.getMessage()); return; } SecurityContextHolder.getContext().setAuthentication(authenticate); chain.doFilter(request, response); } private AuthenticationManager oauthAuthenticationManager() { OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager(); oauthAuthenticationManager.setResourceId(SecurityConstant.OAUTH_RESOURCE); oauthAuthenticationManager.setTokenServices(tokenServices()); oauthAuthenticationManager.setClientDetailsService(clientDetailsService); return oauthAuthenticationManager; } private ResourceServerTokenServices tokenServices() { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore); tokenServices.setSupportRefreshToken(true); tokenServices.setClientDetailsService(clientDetailsService); return tokenServices; } public void init(FilterConfig filterConfig) throws ServletException { } public void destroy() { } private void authenticationFailure(HttpServletRequest request, HttpServletResponse response, String message) throws IOException { securityExceptionTerminator.handle(request, response, R.fail(new SecurityAuthException(message))); } }