看下源码修下 SpringSecurityOAuth2 的bug,解决令牌检查端点未实现 OAuth2 规范带来的坑: Resour...

栏目: IT技术 · 发布时间: 6年前

内容简介:最近在自己搭一个使用目的是为了在网关层做所有的鉴权操作。  其实一切都还好,ajax 登陆、OAuth2密码模式的 token 获取、token刷新等 都有序进行中。认证的整个流程都没发现问题,可是一到鉴权的阶段就不对劲了。

最近在自己搭一个使用 SpringSecutiryOAuth2 的认证服务器, 这里的接口基于 SpringMVC, 而资源服务器是 SpringCloudGateway 建立的网关层,实现是 WebFlux。

目的是为了在网关层做所有的鉴权操作。  其实一切都还好,ajax 登陆、OAuth2密码模式的 token 获取、token刷新等 都有序进行中。

认证的整个流程都没发现问题,可是一到鉴权的阶段就不对劲了。

主要就是令牌内省失败,正常的令牌没问题,但是令牌如果一过期/或者是错误的令牌, 直接就报错了。这我就觉得很不对劲。

问题是我想了想 无论是 AuthorizationServer 还是 ResourceServer , 我都没有对具体认证流程作出改动。 仅仅实现了 SpringSecurity 提供的拓展点。比如 Token存储、Client存储、token 的附加信息、权限查询 之类的。

那这就不应该啊?? 我这用的你默认的实现,怎么还能有问题呢?

而又由于网关层 也就是OAuth2 ResourceServer 他是一个 WebFlux 搭建的web服务,  这个东西调试是真的不好调,里面大量的 lambda 和异步看的我头都要炸了。

不过又还能怎么办呢? ResourceServer/ AuthorizationServer 源码看看看看他丫的。 Debug日志开他丫的。

可是看他这个 WebFlux 下的鉴权源码真的很痛苦。 所以我具体详细的 Debug 流程就不细说了。

ResourceServer introspect

首先就是看 ResourceServer 的令牌内省(introspect)  也就是检查令牌的机制流程

具体触发时机为: 一个客户端试图来请求 ResourceServer 受保护的资源时、若是携带了 Authorization 请求头( Bearer ) 时则会触发令牌内省

最终我找到了处理token 鉴权具体类,就是它: NimbusReactiveOpaqueTokenIntrospector

这里贴一段 WebFlux 作为资源服务器处理 token 鉴权的流程源码。

Gateway的过滤器会提取出 Bearer Token 然后调用此方法。每个流程都写了注释, 还是很清晰的。

@Override
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
 
    return Mono.just(token)
            .flatMap(this::makeRequest)//携带token请求 AuthorizationServer
            .flatMap(this::adaptToNimbusResponse)//检查Http响应正确性 (看是不是200)
            .map(this::parseNimbusResponse)//封装Http响应为Token内省响应
            .map(this::castToNimbusSuccess)//检查Token内省响应正确性
            .doOnNext(response -> validate(token, response))//效验返回值 (active == true?)
            .map(this::convertClaimsSet)//解析返回值中携带的信息,封装成认证对象
            .onErrorMap(e -> !(e instanceof OAuth2IntrospectionException), this::onError);
}

NimbusReactiveOpaqueTokenIntrospector 这个类里面所有的源码我都看了一边、并没有发现有什么问题。

只是对于异常处理我有点不满, 因为如果出现了异常,我作为请求资源服务的客户端看到的响应是一片空白的, 具体错误信息都放在了Response Header 里,这个我觉得不太好。到时候把他覆盖掉给他改了。

然后源码没看出什么花来,那就打断点看看,

结果在检查Http响应正确性的方法里也就是 adaptToNimbusResponse() 中发现了不对。

这是这个方法的源码

private Mono<HTTPResponse> adaptToNimbusResponse(ClientResponse responseEntity) {
 HTTPResponse response = new HTTPResponse(responseEntity.rawStatusCode());
 response.setHeader(HttpHeaders.CONTENT_TYPE, responseEntity.headers().contentType().get().toString());
 if (response.getStatusCode() != HTTPResponse.SC_OK) {
 return responseEntity.bodyToFlux(DataBuffer.class)
 .map(DataBufferUtils::release)
 .then(Mono.error(new OAuth2IntrospectionException(
 "Introspection endpoint responded with " + response.getStatusCode())));
 }
 return responseEntity.bodyToMono(String.class)
 .doOnNext(response::setContent)
 .map(body -> response);
}

在断点时,请求完成了,结果判定走进了这个 if 块。也就是请求错误。不是200响应。

所以整个流程就到了这里中断。是这个响应的 HttpStatus 是400 (Bad request) 让我有点奇怪。 为什么会是400?

因为我看到后面的处理 validate() 效验返回值逻辑,正常来说请求应该是返回200,并且带上一个 active为false 的 Response body 才对啊。

AuthorizationServer CheckToken Endpoint

于是我立马就决定去看认证服务的 check_token 端点是怎么写的。

这是 SpringSecutiryOAuth2 默认的令牌检查端点的源码,   checkToken() 方法中我打了详细的注释

/**
 * Controller which decodes access tokens for clients who are not able to do so (or where opaque token values are used).
 * 
 * @author Luke Taylor
 * @author Joel D'sa
 */
@FrameworkEndpoint
public class CheckTokenEndpoint {
 
 private ResourceServerTokenServices resourceServerTokenServices;
 
 private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
 
 protected final Log logger = LogFactory.getLog(getClass());
 
 private WebResponseExceptionTranslator<OAuth2Exception> exceptionTranslator = new DefaultWebResponseExceptionTranslator();
 
 public CheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
 this.resourceServerTokenServices = resourceServerTokenServices;
 }
 
 /**
 * @param exceptionTranslator the exception translator to set
 */
 public void setExceptionTranslator(WebResponseExceptionTranslator<OAuth2Exception> exceptionTranslator) {
 this.exceptionTranslator = exceptionTranslator;
 }
 
 /**
 * @param accessTokenConverter the accessTokenConverter to set
 */
 public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
 this.accessTokenConverter = accessTokenConverter;
 }
 
 @RequestMapping(value = "/oauth/check_token")
 @ResponseBody
 public Map<String, ?> checkToken(@RequestParam("token") String value) {
 
            //读取验证的 token 字符串, 封装成OAuth2AccessToken
 OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
 if (token == null) {
                    // 如果找不到这个 token (非法、无效) 就直接报错
 throw new InvalidTokenException("Token was not recognised");
 }
 
 if (token.isExpired()) {
                    //token 存在,但是过期了, 也直接报错
 throw new InvalidTokenException("Token has expired");
 }
 
            //解析token表示的用户信息,提取出其认证
 OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
 
            //从认证信息中提取相应字段(过期时间、用户名之类的),封装成响应
 Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);
 
            //active=true 表示这个是一个有效的 token
 // gh-1070
 response.put("active", true); // Always true if token exists and not expired
 
 return response;
 }
 
 @ExceptionHandler(InvalidTokenException.class)
 public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
 logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
 // This isn't an oauth resource, so we don't want to send an
 // unauthorized code here. The client has already authenticated
 // successfully with basic auth and should just
 // get back the invalid token error.
 @SuppressWarnings("serial")
 InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
 @Override
 public int getHttpErrorCode() {
 return 400;
 }
 };
 return exceptionTranslator.translate(e400);
 }
 
}

然后看到这里我就惊了。 他这里边的逻辑显示:  token 如果发现不对,或者是 token 正确但是过期了, 就直接抛一个异常。

然后看下面的异常处理(@ExceptionHandler 注解的方法) 内部的注释,这说的是人话么

因为这不是oauth资源,因此我们不想在此处发送未经授权的代码。 ”  我懂你的意思了, 知道你不想返回403状态造成资源服务器误解,

问题是你也不能够直接怼个 400 错误请求回去啊???

看下源码修下 SpringSecurityOAuth2 的bug,解决令牌检查端点未实现 OAuth2 规范带来的坑: Resour...

先不说你返回啥400, 你光返回的不是200 就很有问题了。按照道理来说,这个接口只要进来了(即客户端身份验证已经通过了), 那么出去的响应肯定得是 200 才行

因为我看了OAuth2的文档,这是 OAuth2 令牌内省的规范。

https://www.oauth.com/oauth2-servers/token-introspection-endpoint/

里面很清楚的说到了,在下面这些情况下,都不应该返回错误响应,端点仅返回无效标志

  • 请求的令牌不存在或无效
  • 令牌已过期
  • 令牌已发出给与发出此请求的客户端不同的客户端

如果说出现了令牌过期,那么返回值应该是这样子的

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

{
  "active": false
}

问题解决方案

还能咋解决。 重写

这是我重写令牌检查端点后的代码:

/**
 * 覆盖掉默认的令牌检查端点 {@link CheckTokenEndpoint}
 * 提供标准的 check token response
 * https://www.oauth.com/oauth2-servers/token-introspection-endpoint/
 */
@FrameworkEndpoint
class IntrospectEndpoint {
 
    @Resource(type = DefaultTokenServices.class)
    @Lazy
    private ResourceServerTokenServices resourceServerTokenServices;
 
    private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
 
    private WebResponseExceptionTranslator<OAuth2Exception> exceptionTranslator = new RoWebResponseExceptionTranslator();
 
 
    @PostMapping("/oauth/introspect")
    @ResponseBody
    public Map<String, Object> introspect(@RequestParam("token") String value) {
 
        OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
 
        if (token == null) {
            return Map.of("active", false, "msg", "Token was not recognised");
        }
 
        if (token.isExpired()) {
            var builder = ImmutableMap.<String, Object>builder();
            builder.put("active", false).put("msg", "Token has expired");
 
            if (Objects.nonNull(token.getExpiration())) {
                long exp = token.getExpiration().getTime() / 1000;
                builder.put("exp", exp);
            }
 
            return builder.build();
        }
 
        OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
 
        Map<String, Object> response = (Map<String, Object>) accessTokenConverter.convertAccessToken(token, authentication);
 
        // gh-1070
        response.put("active", true);    // Always true if token exists and not expired
 
        return response;
    }
 
 
    @ExceptionHandler(OAuth2Exception.class)
    public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
        return exceptionTranslator.translate(e);
    }
}

重写令牌检查端点之后, 需要在认证服务器的 AuthorizationServerEndpointsConfigurer 配置中将端点映射路径修改, 即从原来的 /oauth/check_token 映射为自定义的端点路径。覆盖掉原先的实现。

最后的疑问

为啥你这 SpringSecurityOAuth2 WebFlux 检查令牌的流程是 按照规矩 来的  ??

为啥你这 SpringSecurityOAuth2 WebMVC 的检查令牌端点 不是按照规范实现 的??

你知道你这样搞, 资源服务和认证服务 接口对不上么??? 我佛了


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Agile Web Development with Rails 4

Agile Web Development with Rails 4

Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2013-10-11 / USD 43.95

Ruby on Rails helps you produce high-quality, beautiful-looking web applications quickly. You concentrate on creating the application, and Rails takes care of the details. Tens of thousands of deve......一起来看看 《Agile Web Development with Rails 4》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具