6

前言

我正在开发OAuth两台服务器之间的安全应用程序。我有一个OAuth Server和一个Resource Server。有Resource Server一个.war部署,其中包含4 APIs.

单一职责

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

我做了什么:

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

在资源服务器端:

我的不同 API 的网址是:

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

下面的代码路由任何请求,如果他们/API/anythingspring 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{}

我需要帮助:

问题在于对 single.warmultiple API. 问题是 spring 配置是在包级别创建的,因为所有的APIs.war具有相同的clientIDclientSecret.

我的 OAuth 服务器如何知道,API正在访问哪些具体内容以及API需要hitCount扣除哪些内容。

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

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

4

1 回答 1

6

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

您所要做的就是:

资源服务器上的配置

而不是使用RemoteTokenServicemake acustom 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

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);
}
}
于 2018-03-22T09:35:47.313 回答