我正在使用一组受oAuth2保护的服务。当前它是这样工作的:客户端使用其用户名和密码登录。我用这些交换令牌。我将令牌保存在会话中,并在每次调用服务时提交。它可以工作,但是问题是我完全手动执行此操作,而没有使用很多Spring Security oAuth2支持。外观如下:
<!-- Configure Authentication mechanism --> <authentication-manager alias="authenticationManager"> <authentication-provider ref="oAuth2AuthenticationProvider"/> </authentication-manager> <beans:bean id="oAuth2AuthenticationProvider" class="my.custom.Oauth2AuthenticationProvider"> <beans:constructor-arg name="accessTokenUri" value="http://x.x.x.x/oauth/token"/> <beans:constructor-arg name="clientId" value="myClientId"/> <beans:constructor-arg name="clientSecret" value="myClientSecret"/> <beans:constructor-arg name="scope"> <beans:list> <beans:value>myScope</beans:value> </beans:list> </beans:constructor-arg> </beans:bean> <beans:bean id="resourceOwnerPasswordAccessTokenProvider" class="org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider"/>
如您所见,我自己创建了身份验证提供程序。它正在接受该标准,UsernamePasswordAuthenticationToken但正在产生自己的扩展,该扩展也保留了实际内容OAuth2AccessToken,因此将其保留在安全性上下文中。
UsernamePasswordAuthenticationToken
OAuth2AccessToken
public class Oauth2AuthenticationProvider implements AuthenticationProvider { @Autowired private ResourceOwnerPasswordAccessTokenProvider provider; private String accessTokenUri; private String clientId; private String clientSecret; private List<String> scope; public Oauth2AuthenticationProvider(String accessTokenUri, String clientId, String clientSecret, List<String> scope) { this.accessTokenUri = accessTokenUri; this.clientId = clientId; this.clientSecret = clientSecret; this.scope = scope; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = authentication.getCredentials().toString(); OAuth2AccessToken token = obtainToken(username, password); return handleLogonSuccess(authentication, token); } private OAuth2AccessToken obtainToken(String username, String password) { ResourceOwnerPasswordResourceDetails passwordResourceDetails = new ResourceOwnerPasswordResourceDetails(); passwordResourceDetails.setUsername(username); passwordResourceDetails.setPassword(password); passwordResourceDetails.setClientId(clientId); passwordResourceDetails.setClientSecret(clientSecret); passwordResourceDetails.setScope(scope); passwordResourceDetails.setAccessTokenUri(accessTokenUri); DefaultAccessTokenRequest defaultAccessTokenRequest = new DefaultAccessTokenRequest(); OAuth2AccessToken token; try { token = provider.obtainAccessToken(passwordResourceDetails, defaultAccessTokenRequest); } catch (OAuth2AccessDeniedException accessDeniedException) { throw new BadCredentialsException("Invalid credentials", accessDeniedException); } return token; } public OAuth2AccessToken refreshToken(OAuth2AuthenticationToken authentication) { OAuth2AccessToken token = authentication.getoAuth2AccessToken(); OAuth2RefreshToken refreshToken = token.getRefreshToken(); BaseOAuth2ProtectedResourceDetails resourceDetails = new BaseOAuth2ProtectedResourceDetails(); resourceDetails.setClientId(clientId); resourceDetails.setClientSecret(clientSecret); resourceDetails.setScope(scope); resourceDetails.setAccessTokenUri(accessTokenUri); OAuth2AccessToken newToken = provider.refreshAccessToken(resourceDetails, refreshToken, new DefaultAccessTokenRequest()); authentication.setoAuth2AccessToken(newToken); return newToken; } public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } private Authentication handleLogonSuccess(Authentication authentication, OAuth2AccessToken token) { MyCustomOAuth2AuthenticationToken successAuthenticationToken = new MyCustomOAuth2AuthenticationToken(user, authentication.getCredentials(), calculateAuthorities(authentication), token); return successAuthenticationToken; } public list<GrantedAuthority> calculateAuthorities(Authentication authentication) { //my custom logic that assigns the correct role. e.g. ROLE_USER }
}
如您所见,它基本上可以确保令牌保留在安全范围内,从这里我可以在每次调用后端服务之前简单地手动将其提取出来。同样,我将在每次调用前检查令牌的新鲜度。这很好用,但是我确定我可以在XML中使用Spring的oauth命名空间(我没有在使用Java config),以多配置少代码的方式实现相同目的。我发现的大多数示例都包括我不关心并且只会让我感到困惑的oAuth服务器实现。
谁能帮我这个忙吗?
通过浏览Spring Security OAuth来源以及在线找到的其他解决方案的一些片段,我已经混搭了一个类似的解决方案。我正在使用Java Config,但也许可以帮助您映射到xml配置,如下所示:
@Configuration @EnableOAuth2Client public class RestClientConfig { @Value("${http.client.maxPoolSize}") private Integer maxPoolSize; @Value("${oauth2.resourceId}") private String resourceId; @Value("${oauth2.clientId}") private String clientId; @Value("${oauth2.clientSecret}") private String clientSecret; @Value("${oauth2.accessTokenUri}") private String accessTokenUri; @Autowired private OAuth2ClientContext oauth2ClientContext; @Bean public ClientHttpRequestFactory httpRequestFactory() { return new HttpComponentsClientHttpRequestFactory(httpClient()); } @Bean public HttpClient httpClient() { PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(maxPoolSize); // This client is for internal connections so only one route is expected connectionManager.setDefaultMaxPerRoute(maxPoolSize); return HttpClientBuilder.create().setConnectionManager(connectionManager).build(); } @Bean public OAuth2ProtectedResourceDetails oauth2ProtectedResourceDetails() { ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails(); details.setId(resourceId); details.setClientId(clientId); details.setClientSecret(clientSecret); details.setAccessTokenUri(accessTokenUri); return details; } @Bean public AccessTokenProvider accessTokenProvider() { ResourceOwnerPasswordAccessTokenProvider tokenProvider = new ResourceOwnerPasswordAccessTokenProvider(); tokenProvider.setRequestFactory(httpRequestFactory()); return new AccessTokenProviderChain( Arrays.<AccessTokenProvider> asList(tokenProvider) ); } @Bean public OAuth2RestTemplate restTemplate() { OAuth2RestTemplate template = new OAuth2RestTemplate(oauth2ProtectedResourceDetails(), oauth2ClientContext); template.setRequestFactory(httpRequestFactory()); template.setAccessTokenProvider(accessTokenProvider()); return template; } }
我发现的重要一点是,即使对于单个提供程序,也需要使用AccessTokenProviderChain,否则自动令牌刷新(在身份验证之后)将无法工作。
要在第一个请求上设置用户凭据,您需要:
@Autowired private OAuth2RestTemplate restTemplate; restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("username", username); restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("password", password);
然后,您可以使用RestTemplate方法正常发出请求,例如:
String url = "http://localhost:{port}/api/users/search/findByUsername?username={username}"; ResponseEntity<User> responseEntity = restTemplate.getForEntity( url, User.class, 8081, username);
如果要跟踪网络上的请求,可以将apache http客户端上的日志级别设置为DEBUG,例如使用Spring Boot:
logging.level.org.apache.http = DEBUG