有的老铁可能还没没怎么了解OAuth2,没关系给你一个链接去先去看看
→http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
OAuth 2.0 定义了四种授权方式:
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
重点讲一讲密码模式(也就是我目前碰到的)
首先我们可以找到位于org.springframework.security.oauth2.provider.endpoint这里面的TokenEnEndpoint类有两个端点(都一样);在里面我们看到很熟悉的@ReRequestMaping(Principal principal,@RequestParam Map<String,String> parameters),里面有俩个参数.解释下:
- principal :这个其实是在Filter阶段就已经认证好的客户端信息(有兴趣的可以去debug一下);
- parameters:这个就是前端传递过去的参数啦;
@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);
}
(认证环节)重点看:这段代码
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
- getTokenGranter():这个方法主要就是为了拿到tokenGranter,从这个类的set方法中我们可以看到tokenGranter值其实是从AuthorizationServerEndpointsConfiguration这个类中被赋值的(实例化了TokenEndpoint),我们可以通过继承AuthorizationServerConfigurerAdapterz这个适配器来自定义配置AuthorizationServerEndpointsConfigurer内的属性,从而配置认证授权端点配置
- grant():听名字就知道大概的意思,接下来的操作都在里面(这里的grant()调用的是在AuthorizationServerEndpointsConfigurer类中的方法,可以仔细去看看,意思就是说默认使用CompositeTokenGranter实现先去遍历)来段代码看看
private TokenGranter tokenGranter() { if (tokenGranter == null) { tokenGranter = new TokenGranter() { private CompositeTokenGranter delegate; @Override public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { if (delegate == null) { delegate = new CompositeTokenGranter(getDefaultTokenGranters()); } return delegate.grant(grantType, tokenRequest); } }; } return tokenGranter; }
这是CompositeTokenGranter中的代码
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { for (TokenGranter granter : tokenGranters) { OAuth2AccessToken grant = granter.grant(grantType, tokenRequest); if (grant!=null) { return grant; } } return null; }
TokenGranter接口
grant()方法就在这个接口中的方法;他被AbstractTokenGranter抽象类实现,而AbstractTokenGranter又有分别被下面4个继承(我们也可以自定义一个XxxTokenGranter类去继承这样我们就可以重写里面的方法了)
- AuthorizationCodeTokenGranter : 授权码模式
- ClientCredentialsTokenGranter : 客户端模式
- ImplicitTokenGranter : 简化模式
- ResourceOwnerPasswordTokenGranter : 密码模式
TokenGranter还有被一个叫CompositeTokenGranter的类实现,会根据你前端传递的参数grant_type来判断到底进哪个模式
而这个模式也是可以自定义的 ,也就是上面图中的tokenGranter()方法回默认用CompositeTokenGranter实现先去遍历(4中模式),
这里的默认也就是说在AuthorizationServerEndpointsConfigurer类中未配置TokenGranter这个属性,如果要配置的其实也很简单
看下面:
AuthorizationServerConfigurer 和 AuthorizationServerConfigurerAdapter
我们要做的只是写一个配置类来继承AuthorizationServerConfigurerAdapter这个适配器,重写三个configure方法
/**
* @author Dave Syer
*
*/
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
其中 endpoints.tokenGranter()就是配置让我们自定义的配置tokenGranter的,建议配置CompositeTokenGranter这个.
因为只有子类ClientCredentialsTokenGranter重写了grant(),其他子类重用父类grant()方法;其实在这里我们也可以自己去继承AbstractTokenGranter类来自定义一个XxGranter;然后在自定义的认证配置里面(也就是继承AuthorizationServerConfigurerAdapter适配器)的endpoints端点配置中去搞他.
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private Environment env;
@Autowired
private UserService userService;
@Autowired
private CaptchaService captchaService;
@Autowired
private TokenStore tokenStore;
@Autowired
private TokenEnhancerChain tokenEnhancerChain;
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
endpoints.tokenGranter(tokenGranter(endpoints))
.userDetailsService(userService)
.reuseRefreshTokens(true);
}
private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer configurer) {
AuthorizationServerTokenServices tokenService = configurer.getTokenServices();
OAuth2RequestFactory requestFactory = configurer.getOAuth2RequestFactory();
ClientDetailsService clientDetailsService = configurer.getClientDetailsService();
List<TokenGranter> tokenGranters = new ArrayList<>();
tokenGranters.add(new ClientCredentialsTokenGranter(tokenService, clientDetailsService, requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenService, clientDetailsService, requestFactory));
//UsernamePasswordGranter就是我们需要的自定义的XXGranter继承AbstractTokenGranter类
UsernamePasswordGranter tokenGranter =
new UsernamePasswordGranter(authenticationManager,
tokenService, clientDetailsService,
requestFactory, captchaService,
userService);
tokenGranters.add(tokenGranter);
return new CompositeTokenGranter(tokenGranters);
}
}
不管是不是自定义Granter,认证还是流程还是必须走AuthenticationManager接口的authenticate方法滴!然后就是走ProvideManager中的authenticate方法去遍历providers(认证提供者好多个)其中的authenticate方法
关于UserDetail 和 UserDetailsService
首先你的User类你的必须实现UserDetail接口和里面的参数, UserDetailsService中的loadUserByUsername()方法在认证(authenticate方法)过程中是肯定要用到.一般来说我们用UserService去继承UserDetailsService然后在他是实现类里面UserServiceImpl去实现loadUserByUsername()方法.
关于authenticate方法,也就是真正的认证了
其实跟前面的TokenGranter也有点类似, 1.大总管(AuthenticationManager) 2.小总管(ProviderManager) 3…..然后后面…..
在ProviderManager中的authenticate方法中会遍历List<AuthenticationProvider> providers这个认证供应者(百度一下),AuthenticationProvider是一个接口,所有每个provider的实现中又有一个authenticate方法,好了进入这里就是真正的认证了.
在这里最好自己打个断点看看往哪走,在进入AbstractUserDetailsAuthenticationProvider实现下面放代码
package org.springframework.security.authentication.dao;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.util.Assert;
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
}
这里应该基本上没啥问题,仔细看一下就明白了. 其中retrieveUser()点进去看就能明白我们之前继承UserDetailService实现的loadUserByUsername()就会被在这调用到, 还有一个就是校验密码的时候—-additionalAuthenticationChecks()方法点进去看
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
presentedPassword, salt)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
重点在密码验证这if(!passwordEncoder.isPasswordVlid(userDetails.getPassword(),presentedPassword.salt)){…………..},还有前面的UserDetailService的loadUserByUsernam()方法~~哎奇怪是不是感觉少个配置啊,就是下面这个东东
其实我们还需要一个继承WebSecurityConfigConfigurerAdpter的Security配置类
先上个图看看吧
package com.hz.coreconfig;
import com.hz.pojo.User;
import com.hz.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author hz
* @date 2018-8-22 16:10
*/
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* userDetailsService(userService):配置用户服务
* passwordEncoder(User.passwordEncoder):配置密码校验规则,在DaoAuthenticationProvider类中
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService)
.passwordEncoder(new BCryptPasswordEncoder(11));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
}
在.userDetailService()中配置的就是我们自己的LoadUserByUsername()方法, .passwordEncode()中配置的就是我们需要密码校验的格式;
好了基本上/oauth/token的流程就是这样了,我也是看了好几天源码,边看边debug才明白的.其实刚开始看真的很懵逼,没办法只能一步一步debug下去.这一字一句都是我自己亲自敲出来的,很开心这是我第一篇博客(其实光写这个篇我花了半天时间,生怕哪里写错了,然后被大佬指出来~~~那就很low很尴尬了),但肯定不是最后一篇!
当然错误肯定会有,希望看官大佬不要吝啬指教~