Spring Security OAuth2 源码解析 (一)

有的老铁可能还没没怎么了解OAuth2,没关系给你一个链接去先去看看 

     →http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

OAuth 2.0 定义了四种授权方式:

  1. 授权码模式(authorization code)
  2. 简化模式(implicit)
  3. 密码模式(resource owner password credentials)
  4. 客户端模式(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类去继承这样我们就可以重写里面的方法了)

  1. AuthorizationCodeTokenGranter : 授权码模式
  2. ClientCredentialsTokenGranter : 客户端模式
  3. ImplicitTokenGranter : 简化模式
  4. 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很尴尬了),但肯定不是最后一篇!

当然错误肯定会有,希望看官大佬不要吝啬指教~

    原文作者:Spring Cloud
    原文地址: https://blog.csdn.net/hz1994214/article/details/82020202
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞