在 Spring Security 中具有密码授权的 oAuth2 客户端
Posted
技术标签:
【中文标题】在 Spring Security 中具有密码授权的 oAuth2 客户端【英文标题】:oAuth2 client with password grant in Spring Security 【发布时间】:2015-06-25 17:09:27 【问题描述】:我正在使用一组受 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
,从而将其保留在安全上下文中。
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 配置)以更多配置更少代码的方式实现相同的目的。我发现的大多数示例都包括我不关心的 oAuth 服务器实现,只是让我感到困惑。
谁能帮我解决这个问题?
【问题讨论】:
【参考方案1】:我通过浏览 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;
我发现一个重要的一点是,即使是单个 Provider,您也需要使用 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
【讨论】:
如果用户名和密码在 POST 正文中怎么办?作为 x-www-url-encoded @cosbor11 我同样问自己,发现我们可以在ResourceOwnerPasswordResourceDetails
实例详细信息上使用details.setClientAuthenticationScheme(AuthenticationScheme.form);
,这样身份验证数据通过HTTP 正文发送,而不是使用标头。 以上是关于在 Spring Security 中具有密码授权的 oAuth2 客户端的主要内容,如果未能解决你的问题,请参考以下文章
Spring Boot / Spring Security中的错误401未经授权
Spring Security OAuth2 Demo —— 密码模式(Password)
具有 CAS 身份验证和自定义授权的 Spring Security