序
本文主要来聊聊spring security oauth2的password方式的认证
/oauth/token
这个主要见上一篇文章,讲了是怎么拦截处理/oauth/token的,其中有个点还需要强调一下,就是支持password授权模式的话,还需要额外支持用户登录认证。
password模式
这个模式需要额外处理,首先经过filter认证通过,然后进入endpoint
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/provider/endpoint/TokenEndpoint.java
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
这里从getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest)这里会根据不同认证方式获取token,其中password比较特别,需要走用户账号密码认证,这里匹配的granter是ResourceOwnerPasswordTokenGranter
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/provider/password/ResourceOwnerPasswordTokenGranter.java
public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "password";
private final AuthenticationManager authenticationManager;
public ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}
protected ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
String username = parameters.get("username");
String password = parameters.get("password");
// Protect from downstream leaks of password
parameters.remove("password");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
}
catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
}
catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
可以看到这里获取用户名密码,然后进行认证。这里有个问题,之前不是已经设置了userDetailsService为ClientDetailsUserDetailsService了么,这里是如何还支持用户的password认证的。
之前这里指定了userDetailsService为clientDetailsUserDetailsService
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.java
@Override
public void init(HttpSecurity http) throws Exception {
registerDefaultAuthenticationEntryPoint(http);
if (passwordEncoder != null) {
ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(clientDetailsService());
clientDetailsUserDetailsService.setPasswordEncoder(passwordEncoder());
http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(clientDetailsUserDetailsService)
.passwordEncoder(passwordEncoder());
}
else {
http.userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService()));
}
http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable()
.httpBasic().realmName(realm);
}
AuthorizationServerSecurityConfigurer与AuthorizationServerEndpointsConfigurer
答案就在这个细节中,相对于TokenEndpoint来说,之前配置的是AuthorizationServerSecurityConfigurer,这个相当于pre认证,而AuthorizationServerEndpointsConfigurer这个相当于after认证。
之前讲述password模式的时候,特意提到的需要额外的配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//todo 这里额外指定了/oauth/token的password模式要使用的userDetailsService
endpoints.authenticationManager(authenticationManager);
endpoints.userDetailsService(userDetailsService);
endpoints.tokenStore(tokenStore());
}
对,就是这里指定了进入TokenEndpoint之后使用的authenticationManager和userDetailsService
这里也就额外体现了authenticationManager和userDetailsService分离的好处。
AuthorizationServerEndpointsConfigurer
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerEndpointsConfigurer.java
public AuthorizationServerEndpointsConfigurer userDetailsService(UserDetailsService userDetailsService) {
if (userDetailsService != null) {
this.userDetailsService = userDetailsService;
this.userDetailsServiceOverride = true;
}
return this;
}
设置了userDetailsService之后,userDetailsServiceOverride这个就为true,之后在外部的配置
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configuration/AuthorizationServerSecurityConfiguration.java
@Override
protected void configure(HttpSecurity http) throws Exception {
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
configure(configurer);
http.apply(configurer);
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}
// @formatter:off
http
.authorizeRequests()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.and()
.requestMatchers()
.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
// @formatter:on
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
这里额外判断了如果没有被覆盖的话,才设置userDetailsService
其他
之前AuthorizationServerSecurityConfigurer为什么不需要额外指定authenticationManager呢,因为内部的配置帮你配置好了
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.java
private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) {
ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter(
frameworkEndpointHandlerMapping().getServletPath("/oauth/token"));
clientCredentialsTokenEndpointFilter
.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
authenticationEntryPoint.setTypeName("Form");
authenticationEntryPoint.setRealmName(realm);
clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
clientCredentialsTokenEndpointFilter = postProcess(clientCredentialsTokenEndpointFilter);
http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
return clientCredentialsTokenEndpointFilter;
}
看这里从http.getSharedObject(AuthenticationManager.class)获取然后指定了。
小结
- 请求/oauth/token的,如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter
- 请求/oauth/token的,如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证
- client detail认证(走AuthorizationServerSecurityConfigurer的配置)成功之后,如果是password模式,要走用户账号密码认证
- 走password的就是用AuthorizationServerEndpointsConfigurer中配置的userDetailsService来进行认证
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。