前言
我正在开发一个OAuth
应用程序,用于两台服务器之间的安全性。我有一个OAuth Server
和一个Resource Server
.Resource Server
部署了一个包含4 APIs
的.war
。
单一责任
OAuth server
必须验证同一.war
的API
(4 个中的 1 个)传递的access token
。OAuth server
必须为特定accessToken
的特定API
保留hit count
。如果hit count
超过配置的hits
则OAuth server
将引发403:禁止访问。.war
中的每个API
都必须首先验证来自OAuth server
的accessToken
,如果已验证,则继续提供响应。
我做了什么:
如果一个.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{}
我需要帮助:
问题在于对单.war
和multiple API
的支持。问题是 spring 配置是在包级别创建的,因此.war
中的所有APIs
都具有相同的clientID
和clientSecret
。
我的OAuth服务器如何知道正在访问哪些特定API
以及需要扣除哪些API
hitCount
。
可能的解决方案?我想自定义RemoteTokenService
并在webHoot URL
添加一个请求参数,然后在服务器上使用过滤器OAuth
以获取传递的tag
(如果我可以这么称呼它)
这可能吗?有没有比这更好的方法,不涉及所有这些解决方法?
尤里卡!!我终于找到了解决这个问题的方法。
您所要做的就是:
资源服务器上的配置
而不是使用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
进行身份验证和授权的请求。
身份验证服务器上的配置
覆盖 Spring 安全控制器。我所说的覆盖环的意思是进行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);
}
}