基于spring security的oauth2搭建

模式

password模式

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>oauth2-demo</groupId>
    <artifactId>oauth2-demo</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <springSecurityOauth>2.3.3.RELEASE</springSecurityOauth>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
       <!-- <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 -->
<!--        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>${springSecurityOauth}</version>
        </dependency>-->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>

        <!-- redis中 不用可以去除-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- mysql连接 -->
        <!--<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>-->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

权限服务器配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    RedisConnectionFactory redisConnectionFactory;

    @Bean
	public RedisClientDetailsService redisClientDetailsService() {
		RedisClientDetailsService r = new RedisClientDetailsService();
		return r;
	}
    
    @Autowired
    private TokenStore getRedisTokenStore() {
        return new MyRedisTokenStore(redisConnectionFactory);
    }
    @Autowired
	private UserDetailsService userDetailsService;
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//        clients.inMemory()
//                .withClient("client_1")
//                .resourceIds(DEMO_RESOURCE_ID)
//                .authorizedGrantTypes("client_credentials", "refresh_token")
//                .scopes("all")
//                .authorities("client")
//                .secret("{bcrypt}" + new BCryptPasswordEncoder().encode("123456"));
        
        clients.withClientDetails(redisClientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(getRedisTokenStore())   // 使用自定义Redis token,新版的redis去除了set方法
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        //允许表单认证
        oauthServer.allowFormAuthenticationForClients();
    }

}

1.配置客户端验证和用户验证实现类
2.配置tokenstore(我们使用redis作为token存储介质)

自定义TokenStore(redis)

可以使用spring security自带的RedisTokenStore,但是由于redis版本原因,会执行报错,重写后在set前加stringCommands。
该类中包含token验证、权限判断等,无需再次改动。

资源服务器配置

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
    	http.authorizeRequests()
    	.antMatchers("/ignore").permitAll()
    	.antMatchers("/user").hasAuthority("ADMIN")
    	.antMatchers("/user2").hasAuthority("ADMIN2")
    	.anyRequest().authenticated();
    }
}

permitAll():表示匿名访问
hasAuthority(“ADMIN”):表示需要admin权限

WEB安全配置

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled =true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private UserDetailsService userDetailsService;
	@Autowired
	private PasswordEncoder passwordEncoder;
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	//http.csrf().disable();
    	// 匹配所有请求都不需要权限控制
        // http.authorizeRequests().anyRequest().access("permitAll");
    	//匹配所有的请求都需要USER权限
        //http.authorizeRequests().anyRequest().hasRole("USER");
        //http.authorizeRequests().antMatchers("/api/**").authenticated();
    }
    
  // 使用 用户登录的加密策略
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
 
    @Override
    @Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

}

用户验证

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

    	if (!username.equals("ld")) {
    		throw new AuthenticationCredentialsNotFoundException("用户不存在");
    	} 
		UserDetails build = User.withUsername(username).password("$2a$10$/xdCNqcVOtPTxiM1BARhkOWJGHFC2t8nnySPpXtMvB5U3qGOejGj6")
				// 这里不能roles和authorities一起使用,会覆盖类中list,要一起使用在ROLE判断前面加ROLE_
		.authorities("ROLE_LD","ADMIN").build();
        return build;
    }
}

示例中写死用户名、密码、权限,实际中需要查库。
.authorities(“ROLE_LD”,“ADMIN”),这里ROLE_LD,表示用户拥有LD角色,不写ROLE_开头,表示直接拥有某权限。

客户端验证

public class RedisClientDetailsService implements ClientDetailsService {

	@Override
	public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
		BaseClientDetails b = new BaseClientDetails();
		// 这里写正确的clientID和secret
		// 从数据库查询后封装BaseClientDetails对象,后面会通过DaoAuthenticationProvider来比较传入的值与此对象的值
		b.setClientId(clientId);
		b.setClientSecret("$2a$10$06msMGYRH8nrm4iVnKFNKOoddB8wOwymVhbUzw/d3ZixD7Nq8ot72");
		Collection<String> authorizedGrantTypes = new HashSet<String>();
		authorizedGrantTypes.add("password");
		authorizedGrantTypes.add("refresh_token");
		b.setAuthorizedGrantTypes(authorizedGrantTypes );
		Collection<String> scope = new HashSet<>();
		scope.add("select");
		b.setScope(scope);
		return b;
	}
}

本示例中写死客户端信息(clientId、secret、scope、权限等),实际中根据业务,可以查询数据库或者缓存,匹配传入的clientId查询数据返回客户端对象。

调试

  1. 获取token

访问接口

http://localhost:9090/oauth/token?username=ld&password=123456&grant_type=password&scope=select&client_id=client_2&client_secret=webApp

核心处理类TokenEndpoint、AuthorizationEndpoint(源码不分析了,可以参照其他博主的文章),
先进入我们自定义的clientDetailService,由于我们测试使用的password模式,后面会进入自定义UserDetailService。
此接口返回数据格式:

{
    "access_token": "e9fad28a-21ec-4ff7-b78b-7ff2a47c0ef5",
    "token_type": "bearer",
    "refresh_token": "fa7bdc1e-c89e-4464-b801-7883e9797c84",
    "expires_in": 42880,
    "scope": "select"
}

access_token:用户token,访问资源需要携带此token
refresh_token:用于调用刷新token,需要携带此token

  1. 调用业务接口

http://localhost:9090/role

请求头

Authorization=Bearer e9fad28a-21ec-4ff7-b78b-7ff2a47c0ef5

@RequestMapping("/role")
	@PreAuthorize("hasRole('LD')")
	public String role() {
		return "role";
	}

@PreAuthorize(“hasRole(‘LD’)”)
调用自定义redisTokenStore来验证权限

  1. 刷新token

http://localhost:9090/oauth/token?refresh_token=fa7bdc1e-c89e-4464-b801-7883e9797c84&grant_type=refresh_token&client_id=client_2&client_secret=webApp

可以重置expires_in过期时间

源码分析

1. 客户端验证

http://localhost:9090/oauth/token?username=ld&password=123456&grant_type=password&scope=select&client_id=client_2&client_secret=webApp

拦截器
在这里插入图片描述
在FilterChainProxy的doFilter中开始

private List<SecurityFilterChain> filterChains;

在这里插入图片描述
我们关注 ClientCredentialsTokenEndpointFilter这个类

public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
    
    

继承了AbstractAuthenticationProcessingFilter ,首先会先到父类的doFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
    
    

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
    
    
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
    
    
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		try {
    
    
			authResult = attemptAuthentication(request, response); // 1
			if (authResult == null) {
    
    
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
    
    
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
    
    
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
    
    
			chain.doFilter(request, response);
		}

		successfulAuthentication(request, response, chain, authResult);
	}
authResult = attemptAuthentication(request, response); // 1
抽象方法,ClientCredentialsTokenEndpointFilter实现如下图
@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException {
    
    
		//  必须为post请求
		if (allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
    
    
			throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[] {
    
     "POST" });
		}

		String clientId = request.getParameter("client_id");
		String clientSecret = request.getParameter("client_secret");

		// If the request is already authenticated we can assume that this
		// filter is not needed
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication != null && authentication.isAuthenticated()) {
    
    
			return authentication;
		}

		if (clientId == null) {
    
    
			throw new BadCredentialsException("No client credentials presented");
		}

		if (clientSecret == null) {
    
    
			clientSecret = "";
		}

		clientId = clientId.trim();
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
				clientSecret);

		return this.getAuthenticationManager().authenticate(authRequest);

	}

this.getAuthenticationManager()默认类ProviderManager

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    
    
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
    
    
			if (!provider.supports(toTest)) {
    
    
				continue;
			}

			if (debug) {
    
    
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
    
    
				result = provider.authenticate(authentication);

				if (result != null) {
    
    
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
    
    
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
    
    
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
    
    
				lastException = e;
			}
		}

		if (result == null && parent != null) {
    
    
			// Allow the parent to try.
			try {
    
    
				result = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
    
    
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
    
    
				lastException = e;
			}
		}

		if (result != null) {
    
    
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
    
    
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
    
    
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] {
    
     toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		prepareException(lastException, authentication);

		throw lastException;
	}

result = provider.authenticate(authentication);
默认provider为DaoAuthenticationProvider,继承AbstractUserDetailsAuthenticationProvider,先进入父类authenticate:

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);
	}

会先从缓存里面取用户信息(如果设置了缓存),如果未设置,默认为NullUserCache,没有任何实现逻辑

public class NullUserCache implements UserCache {
    
    
	// ~ Methods
	// ========================================================================================================

	public UserDetails getUserFromCache(String username) {
    
    
		return null;
	}

	public void putUserInCache(UserDetails user) {
    
    
	}

	public void removeUserFromCache(String username) {
    
    
	}
}

主要看user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
DaoAuthenticationProvider.retrieveUser():

protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
    
    
		prepareTimingAttackProtection();
		try {
    
    
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
    
    
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
    
    
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
    
    
			throw ex;
		}
		catch (Exception ex) {
    
    
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

客户端模式this.getUserDetailsService()返回ClientDetailsUserDetailsService

public class ClientDetailsUserDetailsService implements UserDetailsService {
    
    

	private final ClientDetailsService clientDetailsService;
	private String emptyPassword = "";
	
	public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {
    
    
		this.clientDetailsService = clientDetailsService;
	}
	
	/**
	 * @param passwordEncoder the password encoder to set
	 */
	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
    
    
		this.emptyPassword = passwordEncoder.encode("");
	}

	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
		ClientDetails clientDetails;
		try {
    
    
			clientDetails = clientDetailsService.loadClientByClientId(username);
		} catch (NoSuchClientException e) {
    
    
			throw new UsernameNotFoundException(e.getMessage(), e);
		}
		String clientSecret = clientDetails.getClientSecret();
		if (clientSecret== null || clientSecret.trim().length()==0) {
    
    
			clientSecret = emptyPassword;
		}
		return new User(username, clientSecret, clientDetails.getAuthorities());
	}

}

clientDetailsService是之前配置的RedisClientDetailsService,
其中loadUserByUsername,调用自定义客户端验证,返回ClientDetails对象。

回到AbstractUserDetailsAuthenticationProvider的authenticate(Authentication authentication)中preAuthenticationChecks.check(user),简单验证一下user对象是否被锁定、被禁用等。
下一步additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
由子类DaoAuthenticationProvider调用

protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
    
    
		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.matches(presentedPassword, userDetails.getPassword())) {
    
    
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}

在这里匹配传入的client_secret和查询到的ClientDetails对象中的密码是否相同(加密后),匹配成功返回,整个客户端验证就结束了。

2. 获取token

@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);
public class CompositeTokenGranter implements TokenGranter {
    
    

	private final List<TokenGranter> tokenGranters;

	public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
    
    
		this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
	}
	
	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
    
    
		for (TokenGranter granter : tokenGranters) {
    
    
			OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
			if (grant!=null) {
    
    
				return grant;
			}
		}
		return null;
	}
	
	public void addTokenGranter(TokenGranter tokenGranter) {
    
    
		if (tokenGranter == null) {
    
    
			throw new IllegalArgumentException("Token granter is null");
		}
		tokenGranters.add(tokenGranter);
	}

}

这里的tokenGranters为List,维护了授权的五种方式
1.授权码模式
2.刷新token
3.简化模式
4.客户端模式
5.密码模式
在这里插入图片描述
grant方法,根据传入的grantType,找到对应的授权处理器,这里是password模式,由ResourceOwnerPasswordTokenGranter处理,父类AbstractTokenGranter来处理

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
    
    

		if (!this.grantType.equals(grantType)) {
    
    
			return null;
		}
		
		String clientId = tokenRequest.getClientId();
		ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
		validateGrantType(grantType, client);
		
		logger.debug("Getting access token for: " + clientId);

		return getAccessToken(client, tokenRequest);

	}

这里又验证一次客户端(目前仅仅一次调用,一共验证了三次客户端,应该是每个环节可能会在其他地方调用)

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
    
    
		return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
	}

getOAuth2Authentication由ResourceOwnerPasswordTokenGranter类实现

@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);
	}

authenticationManager.authenticate(userAuth);这里的authenticationManager最终由ProviderManager来处理,与上面的客户端验证使用同一套逻辑判断,上面使用的是clientId作为username,调用的是ClientDetailService,这里传入的username为用户传入的username,调用UserDetailService。
返回结果后,进行创建token的逻辑,DefaultTokenServices

@Transactional
	public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
    
    

		OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
		OAuth2RefreshToken refreshToken = null;
		if (existingAccessToken != null) {
    
    
			if (existingAccessToken.isExpired()) {
    
    
				if (existingAccessToken.getRefreshToken() != null) {
    
    
					refreshToken = existingAccessToken.getRefreshToken();
					tokenStore.removeRefreshToken(refreshToken);
				}
				tokenStore.removeAccessToken(existingAccessToken);
			}
			else {
    
    
				tokenStore.storeAccessToken(existingAccessToken, authentication);
				return existingAccessToken;
			}
		}
		if (refreshToken == null) {
    
    
			refreshToken = createRefreshToken(authentication);
		}
		else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
    
    
			ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
			if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
    
    
				refreshToken = createRefreshToken(authentication);
			}
		}

		OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
		tokenStore.storeAccessToken(accessToken, authentication);
		refreshToken = accessToken.getRefreshToken();
		if (refreshToken != null) {
    
    
			tokenStore.storeRefreshToken(refreshToken, authentication);
		}
		return accessToken;

	}

tokenStore.getAccessToken(),自定义tokenStore去获取OAuth2AccessToken,这里实例中从redis中去获取(先MD5加密后去redis匹配key),如果返回为空,tokenStore.storeRefreshToken(),将新生成的token放入缓存;如果不为空,返回token。整个获取token流程就完成了。

3. 访问拦截

  1. 不带token,访问需要授权认证的资源
    FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 311
private static class VirtualFilterChain implements FilterChain {
    
    
		private final FilterChain originalChain;
		private final List<Filter> additionalFilters;
		private final FirewalledRequest firewalledRequest;
		private final int size;
		private int currentPosition = 0;

		private VirtualFilterChain(FirewalledRequest firewalledRequest,
				FilterChain chain, List<Filter> additionalFilters) {
    
    
			this.originalChain = chain;
			this.additionalFilters = additionalFilters;
			this.size = additionalFilters.size();
			this.firewalledRequest = firewalledRequest;
		}

		@Override
		public void doFilter(ServletRequest request, ServletResponse response)
				throws IOException, ServletException {
    
    

同样,也是根据List依次执行过滤器链,currentPosition来记录目前过滤器索引位。
返回结果
{
“error”: “unauthorized”,
“error_description”: “Full authentication is required to access this resource”
}

  1. 不带token,访问无需验证的资源
    拦截过程同上

  2. 携带token,访问需要验证的资源
    OAuth2AuthenticationProcessingFilter,调用到最后的readAccessToken,从缓存中获取

  3. 携带token,访问@PreAuthorize的资源,通过OAuth2AuthenticationProcessingFilter拦截处理,调用到TokenStore的readAuthentication,取出缓存中OAuth2Authentication对象,最后再在ExpressionUtils的evaluateAsBoolean进行判断是否包含此接口访问权限。

猜你喜欢

转载自blog.csdn.net/qq_36382225/article/details/100985803