【问题标题】:Can i append some information in oauth/check_token endpoint and retrieve it at authorization server?我可以在 oauth/check_token 端点中附加一些信息并在授权服务器上检索它吗?
【发布时间】:2018-07-26 04:18:10
【问题描述】:

前言

我正在开发一个OAuth 应用程序以确保两台服务器之间的安全性。我有一个OAuth Server 和一个Resource ServerResource Server 部署了一个包含4 APIs.war

单一职责

  1. OAuth server 必须验证由同一 .warAPI(4 个中的 1 个)传递的 access token
  2. OAuth server 必须为特定的accessToken 为特定的API 保留一个hit count。如果hit count 超过配置的hitsOAuth server 将抛出 403: Forbidden
  3. .war 中的每个 API 必须首先验证来自 OAuth serveraccessToken,如果验证通过,则继续提供响应。

我做了什么:

如果.war 有一个API,那么我可以简单地使两个服务器使用webHook 进行通信,下面是执行此操作的代码。

在资源服务器端:

我的不同 API 的网址是:

  • localhost:8080/API/API1
  • localhost:8080/API/API2

如果有/API/anything,下面的代码会将任何请求路由到spring security filters

<http pattern="/API/**" create-session="never" authentication-manager-ref="authenticationManager" entry-point-ref="oauthAuthenticationEntryPoint" xmlns="http://www.springframework.org/schema/security">
        <anonymous enabled="false" />        
        <intercept-url pattern="/places/**" method="GET" access="IS_AUTHENTICATED_FULLY" />
        <custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
        <access-denied-handler ref="oauthAccessDeniedHandler" />
</http>

我使用了远程令牌服务并定义了webHook 将请求路由到OAuth server

<bean id="tokenServices"  class="org.springframework.security.oauth2.provider.token.RemoteTokenServices">
    <property name="checkTokenEndpointUrl" value="http://localhost:8181/OUTPOST/oauth/check_token"/>
    <property name="clientId" value="atlas"/>
    <property name="clientSecret" value="atlas"/>
</bean>

身份验证服务器配置

@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private static String REALM="OUTPOST_API";

    @Autowired
    private ClientDetailsService clientService;

    @Autowired
    public AuthorizationServerConfig(AuthenticationManager authenticationManager,RedisConnectionFactory redisConnectionFactory) {
        this.authenticationManager = authenticationManager;
        this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
    }

//  @Autowired
//  @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    private TokenStore redisTokenStore;

    @Autowired
    private UserApprovalHandler userApprovalHandler;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Override

    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {

        security.tokenKeyAccess("isAuthenticated()")
                .checkTokenAccess("isAuthenticated()").
        realm(REALM+"/client");

    }

    @Override

    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients

                .inMemory()
                .withClient("cl1")
                .secret("pwd")
                .authorizedGrantTypes("password", "client_credentials", "refresh_token")
                .authorities("ROLE_CLIENT", "ROLE_ADMIN")
                .scopes("read", "write", "trust")/*
                .resourceIds("sample-oauth")*/              
                .accessTokenValiditySeconds(1000)               
                .refreshTokenValiditySeconds(5000)
                .and()
                .withClient("atlas")
                .secret("atlas");



    }

    @Bean
    @Autowired
    public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
        this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
         return this.redisTokenStore;
    }

    @Bean
    public WebResponseExceptionTranslator loggingExceptionTranslator() {
        return new DefaultWebResponseExceptionTranslator() {
            @Override
            public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
                // This is the line that prints the stack trace to the log. You can customise this to format the trace etc if you like
                e.printStackTrace();

                // Carry on handling the exception
                ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
                HttpHeaders headers = new HttpHeaders();
                headers.setAll(responseEntity.getHeaders().toSingleValueMap());
                OAuth2Exception excBody = responseEntity.getBody();
                return new ResponseEntity<>(excBody, headers, responseEntity.getStatusCode());
            }
        };
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.tokenStore(redisTokenStore).userApprovalHandler(userApprovalHandler)
                .authenticationManager(authenticationManager)
                .exceptionTranslator(loggingExceptionTranslator());
    }

    public void setRedisConnectionFactory(RedisConnectionFactory redisConnectionFactory) {
        this.redisConnectionFactory = redisConnectionFactory;
    }



        @Bean
        public TokenStoreUserApprovalHandler userApprovalHandler(){
            TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
            handler.setTokenStore(redisTokenStore);
            handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientService));
            handler.setClientDetailsService(clientService);
            return handler;
        }

        @Bean
        @Autowired
        public ApprovalStore approvalStore() throws Exception {
            TokenApprovalStore store = new TokenApprovalStore();
            store.setTokenStore(redisTokenStore);
            return store;
        }

        @Bean
        @Primary
        @Autowired
        public DefaultTokenServices tokenServices() {
            DefaultTokenServices tokenServices = new DefaultTokenServices();
            tokenServices.setSupportRefreshToken(true);
            tokenServices.setTokenStore(redisTokenStore);
            return tokenServices;
        }

    }

    @Component
    class MyOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint{}

我需要什么帮助:

问题在于对单个 .warmultiple API 的支持。问题是 spring 配置是在包级别创建的,因为.war 中的所有APIs 都具有相同的clientIDclientSecret

我的 OAuth 服务器如何知道,哪些API 正在被访问,哪些API 需要扣除hitCount

可能的解决方案? 我正在考虑自定义RemoteTokenService 并在webHoot URL 添加请求参数,然后在OAuth 服务器上使用过滤器来获取传递的tag(如果我可以这样称呼它)

这甚至可能吗?有没有比这更好的方法,不涉及所有这些变通方法?

【问题讨论】:

  • 为任何用户管理剩余的 API 命中是授权服务器的工作。身份验证服务器检查剩余的命中,然后授权请求。资源服务器将更多地关注 API,它的工作而不是管理点击。

标签: spring-boot spring-security spring-security-oauth2 spring-oauth2 auth-request


【解决方案1】:

Eureka !!我终于找到了解决这个问题的方法。

你所要做的就是:

资源服务器配置

不要使用RemoteTokenService,而是使用custom remote token service,它会在生成的请求中附加一些数据(查询参数)。

public class CustomRemoteTokenService implements ResourceServerTokenServices {

protected final Log logger = LogFactory.getLog(getClass());

private RestOperations restTemplate;

private String checkTokenEndpointUrl;

private String clientId;

private String clientSecret;

private String tokenName = "token";

private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();

@Autowired
public CustomRemoteTokenService() {
    restTemplate = new RestTemplate();
    ((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
        @Override
        // Ignore 400
        public void handleError(ClientHttpResponse response) throws IOException {
            if (response.getRawStatusCode() != 400) {
                super.handleError(response);
            }
        }
    });
}

public void setRestTemplate(RestOperations restTemplate) {
    this.restTemplate = restTemplate;
}

public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
    this.checkTokenEndpointUrl = checkTokenEndpointUrl;
}

public void setClientId(String clientId) {
    this.clientId = clientId;
}

public void setClientSecret(String clientSecret) {
    this.clientSecret = clientSecret;
}

public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
    this.tokenConverter = accessTokenConverter;
}

public void setTokenName(String tokenName) {
    this.tokenName = tokenName;
}

@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

    /*
     * This code needs to be more dynamic. Every time an API is added we have to add its entry in the if check for now.
     * Should be changed later.
     */
    HttpServletRequest request = Context.getCurrentInstance().getRequest();         
    MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
    String uri = request.getRequestURI();

    formData.add(tokenName, accessToken);       

    if(request != null) {
        if(uri.contains("API1")) {
            formData.add("api", "1");
        }else if(uri.contains("API2")) {
            formData.add("api", "2");
        } 
    }

    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
    Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);

    if (map.containsKey("error")) {
        logger.debug("check_token returned error: " + map.get("error"));
        throw new InvalidTokenException(accessToken);
    }



    Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
    return tokenConverter.extractAuthentication(map);
}

@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
    throw new UnsupportedOperationException("Not supported: read access token");
}

private String getAuthorizationHeader(String clientId, String clientSecret) {
    String creds = String.format("%s:%s", clientId, clientSecret);
    try {
        return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
    }
    catch (UnsupportedEncodingException e) {
        throw new IllegalStateException("Could not convert String");
    }
}

private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
    if (headers.getContentType() == null) {
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    }
    @SuppressWarnings("rawtypes")
    Map map = restTemplate.exchange(path, HttpMethod.POST,
            new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
    @SuppressWarnings("unchecked")
    Map<String, Object> result = map;
    return result;
}

}

通过实现ResourceServerTokenServices可以修改resource server发送的请求到auth server进行认证和授权。

身份验证服务器上的配置

覆盖弹簧安全控制器。我的意思是创建一个custom controller,以便oauth/check_token 的请求由您的自定义控制器而不是弹簧定义的控制器处理。

@RestController
public class CustomCheckTokenEndpoint {

private ResourceServerTokenServices resourceServerTokenServices;

private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();

protected final Log logger = LogFactory.getLog(getClass());

private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();

@Autowired
KeyHitManager keyHitManager;

public CustomCheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
    this.resourceServerTokenServices = resourceServerTokenServices;
}

/**
 * @param exceptionTranslator
 *            the exception translator to set
 */
public void setExceptionTranslator(WebResponseExceptionTranslator 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, ?> customCheckToken(@RequestParam("token") String value, @RequestParam("api") int api) {

    OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
    if (token == null) {
        throw new InvalidTokenException("Token was not recognised");
    }

    if (token.isExpired()) {
        throw new InvalidTokenException("Token has expired");
    }

    OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());

    Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication);

    String clientId = (String) response.get("client_id");
    if (!keyHitManager.isHitAvailble(api,clientId)) {
        throw new InvalidTokenException(
                "Services for this key has been suspended due to daily/hourly transactions limit");
    }

    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);
}
}

【讨论】:

  • 您好,感谢您的解决方案,但我无法在 CustomCheckTokenEndpoint 的构造函数中初始化 ResourceServerTokenServices。你能帮忙吗?
  • @AshishMalhotra 为 TokenService 创建一个 bean。我在其他一些配置文件中添加了 DefaultTokenServices。
  • @AshishMalhotra 如果对您有帮助,请支持此答案。
  • 你是如何获得上下文来初始化这一行的:HttpServletRequest request = Context.getCurrentInstance().getRequest();?使用您的解决方案时我没有参考上下文
  • @LukeDavidson 如果您实现 ResourceServerTokenServices,您将从父类获取上下文。已经三年多了。我现在没有这个代码。对不起。
猜你喜欢
  • 1970-01-01
  • 2020-12-12
  • 1970-01-01
  • 2011-04-13
  • 1970-01-01
  • 2013-10-05
  • 2015-05-25
  • 1970-01-01
  • 2015-02-09
相关资源
最近更新 更多