当前位置:   article > 正文

十六 Oauth2.0实战--搭建资源服务器_security:oauth2:resource:

security:oauth2:resource:

Oauth2.0实战–搭建资源服务器

前言:
资源服务器搭建分为两种:
(一) 授权中心与资源服务在同一进程中部署
(二)授权中心与资源服务各自在不同进程中部署

本文主要讲解OAuth2中授权服务与资源服务分离这种情况,并且资源服务器远程调用授权服务器进行身份验证

资源服务搭建

每一个微服务都可以看着是一个资源服务器,这里将account-service微服务作为资源服务器
account-service微服务pom.xml文件引入oauth2.0

 <dependency>
    <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-oauth2</artifactId>
     <version>2.2.5.RELEASE</version>
     <exclusions>
         <exclusion>
             <artifactId>bcpkix-jdk15on</artifactId>
             <groupId>org.bouncycastle</groupId>
         </exclusion>
     </exclusions>
 </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

(1)授权服务校验地址配置(资源服务)
客户端访问资源服务必须要携带一个access_token,如果不携带则使用本地的资源服务安全策略进行验证,如果使用access_token则使用远程的tokenStore到授权服务器进行验证。所以资源客户端必须配置正确的授权服务器令牌校验信息:

#oauth2客户端
security:  
  oauth2:
    resource:
      filter-order: 3
      id: resource_server_id
      tokenInfoUri: http://127.0.0.1:7000/oauth/check_token
      preferTokenInfo: true
      #user-info-uri: http://127.0.0.1:7000/user/principal
      #prefer-token-info: false
    #客户端配置
    client:
      accessTokenUri: http://127.0.0.1:7000/oauth/token
      userAuthorizationUri: http://127.0.0.1:7000/oauth/authorize
      clientId: my_client_id
      clientSecret: my_client_secret

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

但实际上我只配置了以下内容:
在这里插入图片描述

因为我只要一配置客户端,服务我就起不起来,所以我就没有配置客户端client

这里,我配置的授权服务器token校验地址为http://10.1.62.33:8085/oauth/check_token,使用token进行校验(当然也可以使用用户信息进行校验,指定授权服务器获取用户认证信息地址并设置preferTokenInfo为false即可)

备注: /oauth/check_token 为oauth2.0自带的接口, 10.1.62.33:8085为认证服务器的部署地址

(2)授权服务器资源配置
资源服务器可以对本资源服务的所有资源的访问权限进行配置,包括配置资源服务自己的id、接口访问权限等,如我的配置如下所示:

package com.ifly.accountservice.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;

/**
 * 资源服务访问配置
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
     /*该资源服务器id必须在数据库记录中有配置,也就是对应token的用户必须该资源访问权限
     否则无权访问,对应的是oauth_client_details中resource_ids字段里的值
     */
    private static final String DEMO_RESOURCE_ID = "account-service";

    /**
     * 以代码形式配置资源服务器id,配置文件配置不生效
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        remoteTokenServices.setClientId("clientOne");
        remoteTokenServices.setClientSecret("123456");
        remoteTokenServices.setCheckTokenEndpointUrl("http://10.1.62.33:8085/oauth/check_token");
        resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
        resources.tokenServices(remoteTokenServices);
    }

    /**
     * 注意:从网关经过的所有url都进行过滤,情况分为如下两种:
     * 1、带access_token的参数url,过滤器会获取参数到授权中心去鉴权
     * 2、不带access_token的url,过滤器会获取本地‘资源服务’鉴权配置--即如下方法(或注解形式配置)
     * 注意“**”的使用, 使用不好可能导致权限控制失效!!!(如果url前面无单词如/oauth/...,但是匹配路径用** /oauth,就会导致权限控制失效)
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 其他匹配的[剩下的]任何请求都需要授权
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
        registry
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable()
                .httpBasic();
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

可以看到,我的资源服务除了登录之外,所有的接口都必须要登录认证后才能访问。

测试令牌的准备
认证我们依然使用的是资源服务进行认证。 当访问资源服务的时候,我们使用同一的授权服务认证之后返回的token进行资源的访问,最后资源服务会将当前token路由到授权服务配置地址进行token的验证,合法后则通过并允许访问对应的资源或接口,所以,为了测试,我直接使用postman通过用户名密码模式进行认证获取一个测试token:

请求地址:
http://localhost:8085/oauth/token?grant_type=password&client_id=clientApp&client_secret=123456&username=user_1&password=123456
在这里插入图片描述

资源服务接口资源准备:
访问资源服务器接口,不带token的情况下,不能访问资源服务器接口:
在这里插入图片描述
访问资源服务器接口,带token的情况下,能正常访问资源服务器接口:

2、流程解析
(1)通过token方式从授权服务获取用户信息
这种方式对应的资源服务配置如下:

#oauth2客户端
security:  
  oauth2:
    resource:
      filter-order: 3
      id: resource_server_id
      tokenInfoUri: http://127.0.0.1:7000/oauth/check_token
      preferTokenInfo: true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

preferTokenInfo设置为true并且指定了token授权服务器验证地址,这种方式会通过token获取认证信息。具体流程我稍后慢慢解析。

通过《oauth2认证流程》一文的认证流程分析可以知道,OAuth客户端认证请求处理的过滤器是OAuth2AuthenticationProcessingFilter,我是我们首先看该过滤器的处理过程:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.oauth2.provider.authentication;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.Assert;

public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
    private static final Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);
    private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
    private AuthenticationManager authenticationManager;
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
    private TokenExtractor tokenExtractor = new BearerTokenExtractor();
    private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
    private boolean stateless = true;

    public OAuth2AuthenticationProcessingFilter() {
    }

    public void setStateless(boolean stateless) {
        this.stateless = stateless;
    }

    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setTokenExtractor(TokenExtractor tokenExtractor) {
        this.tokenExtractor = tokenExtractor;
    }

    public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
        Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void afterPropertiesSet() {
        Assert.state(this.authenticationManager != null, "AuthenticationManager is required");
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        boolean debug = logger.isDebugEnabled();
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;

        try {
        //根据请求参数(包括请求查询参数或者头部参数)中的access_token远程授权服务换取认证信息
            Authentication authentication = this.tokenExtractor.extract(request);
            //认证信息无效
            if (authentication == null) {
                if (this.stateless && this.isAuthenticated()) {
                    if (debug) {
                        logger.debug("Clearing security context.");
                    }

                    SecurityContextHolder.clearContext();
                }

                if (debug) {
                    logger.debug("No token in request, will continue chain.");
                }
            } else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal()); //保存认证信息
                if (authentication instanceof AbstractAuthenticationToken) {
                    AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
                    needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request)); //创建客户端详情
                }
//提交给认证管理器进行认证
                Authentication authResult = this.authenticationManager.authenticate(authentication);
                if (debug) {
                    logger.debug("Authentication success: " + authResult);
                }

                   this.eventPublisher.publishAuthenticationSuccess(authResult);//发布认证成功事件
                SecurityContextHolder.getContext().setAuthentication(authResult);
            }
        } catch (OAuth2Exception var9) {
        	//清除认证信息
            SecurityContextHolder.clearContext();
            if (debug) {
                logger.debug("Authentication request failed: " + var9);
            }
			//发布认证失败事件
            this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
            //认证端点处理
            this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
            return;
        }

        chain.doFilter(request, response);
    }

    private boolean isAuthenticated() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication != null && !(authentication instanceof AnonymousAuthenticationToken);
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void destroy() {
    }

    private static final class NullEventPublisher implements AuthenticationEventPublisher {
        private NullEventPublisher() {
        }

        public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
        }

        public void publishAuthenticationSuccess(Authentication authentication) {
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151

认证信息由TokenExtractor的实现类BearerTokenExtractor类生成,具体代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.oauth2.provider.authentication;

import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

public class BearerTokenExtractor implements TokenExtractor {
    private static final Log logger = LogFactory.getLog(BearerTokenExtractor.class);

    public BearerTokenExtractor() {
    }

    public Authentication extract(HttpServletRequest request) {
        String tokenValue = this.extractToken(request);
        if (tokenValue != null) {
            PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
            return authentication;
        } else {
            return null;
        }
    }

    protected String extractToken(HttpServletRequest request) {
        //提取头部中的access_token参数
        String token = this.extractHeaderToken(request);
        if (token == null) {
            logger.debug("Token not found in headers. Trying request parameters.");
            //再次从查询参数中获取access_token
            token = request.getParameter("access_token");
            if (token == null) {
                logger.debug("Token not found in request parameters.  Not an OAuth2 request.");
            } else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer"); //保存token类型为Bearer
            }
        }

        return token;
    }
	
	//提前头部Authorization的token,格式:Bearer token,xx
    protected String extractHeaderToken(HttpServletRequest request) {
    		//获取授权字段Authorization
        Enumeration<String> headers = request.getHeaders("Authorization");

        String value;
        do {
            if (!headers.hasMoreElements()) {
                return null;
            }

            value = (String)headers.nextElement();
        } while(!value.toLowerCase().startsWith("Bearer".toLowerCase())); //如果是以Bearer开始

        String authHeaderValue = value.substring("Bearer".length()).trim();
        request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim());
        //提前Bearer之后的token
        int commaIndex = authHeaderValue.indexOf(44);
        if (commaIndex > 0) {
            authHeaderValue = authHeaderValue.substring(0, commaIndex);
        }

        return authHeaderValue;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

最后交给OAuth2AuthenticationManager认证管理器认证代码如下:

	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		if (authentication == null) {
			throw new InvalidTokenException("Invalid token (token not found)");
		}
		// 提取
		String token = (String) authentication.getPrincipal();
		// 通过tokenServices到授权服务查询对应的认证信息
		OAuth2Authentication auth = tokenServices.loadAuthentication(token);
		if (auth == null) {
			throw new InvalidTokenException("Invalid token: " + token);
		}
		// 获取可访问的资源服务器【这里我故意弄错为[gate_way_server],也就是没有id为resource_server资源服务访问权限】
		Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
		// 查询是否有对应资源服务的访问权限
		if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
			throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
		}

		checkClientDetails(auth);
		// 检查auth的详细信息是否相同
		if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
			OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
			// Guard against a cached copy of the same details
			// 不相同的情况下则更新解码后的信息
			if (!details.equals(auth.getDetails())) {
				// Preserve the authentication details from the one loaded by token services
				details.setDecodedDetails(auth.getDetails());
			}
		}
		auth.setDetails(authentication.getDetails());
		auth.setAuthenticated(true);
		// 最终返回OAuth2Authentication认证信息
		return auth;
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

可以看到该认证管理器最终使用tokenServices(实现类RemoteTokenServices)到授权服务查询认证信息,具体查询实现代码如下:

    @Override
	public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
		// token:8b9fa51d-d5e0-40df-b3d2-e35cf6848f6b
		MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
		formData.add(tokenName, accessToken);

		// 授权头字段: Authorization:"Basic bXlfY2xpZW50X2lkOm15X2NsaWVudF9zZWNyZXQ="
		HttpHeaders headers = new HttpHeaders();
		headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
		// 通过token和authorization头字段从配置的url中获取用户信息
		// url: http://127.0.0.1:7000/oauth/check_token
		Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
		// 如果发生错误则抛出无效token异常
		if (map.containsKey("error")) {
			if (logger.isDebugEnabled()) {
				logger.debug("check_token returned error: " + map.get("error"));
			}
			throw new InvalidTokenException(accessToken);
		}
		// 判断服务器是否返回active,返回active为true
		// gh-838
		if (map.containsKey("active") && !"true".equals(String.valueOf(map.get("active")))) {
			logger.debug("check_token returned active attribute: " + map.get("active"));
			throw new InvalidTokenException(accessToken);
		}
		// 通过map再次转换为认证信息(服务端通过token查询到认证信息,客户端逆向转回来)
		return tokenConverter.extractAuthentication(map);
	}

	// 生成授权头:Authorization:Basic base64(client_id:client_secret)
	private String getAuthorizationHeader(String clientId, String clientSecret) {

		if(clientId == null || clientSecret == null) {
			logger.warn("Null Client ID or Client Secret detected. Endpoint that requires authentication will reject request with 401 error.");
		}

		String creds = String.format("%s:%s", clientId, clientSecret);
		try {
			return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
		}
		catch (UnsupportedEncodingException e) {
			throw new IllegalStateException("Could not convert String");
		}
	}

	// post从授权服务获取用户认证信息
	private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
		// 增加头Content-Type:"application/x-www-form-urlencoded"
		if (headers.getContentType() == null) {
			headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		}
		// 从授权服务获取用户认证信息
		@SuppressWarnings("rawtypes")
		Map map = restTemplate.exchange(path, HttpMethod.POST,
				new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
		// 转化返回map
		@SuppressWarnings("unchecked")
		Map<String, Object> result = map;
		return result;
	}

	// tokenConverter逆向将map转为认证信息过程
	public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
		Map<String, String> parameters = new HashMap<String, String>();
		// 获取可访问scope列表
		Set<String> scope = extractScope(map);
		// 提取用户认证信息
		Authentication user = userTokenConverter.extractAuthentication(map);
		// 获取clientid
		String clientId = (String) map.get(clientIdAttribute);
		parameters.put(clientIdAttribute, clientId);
		if (includeGrantType && map.containsKey(GRANT_TYPE)) {
			parameters.put(GRANT_TYPE, (String) map.get(GRANT_TYPE));
		}
		// 获取可访问的资源服务器id
		Set<String> resourceIds = new LinkedHashSet<String>(map.containsKey(AUD) ? getAudience(map)
				: Collections.<String>emptySet());
		
		Collection<? extends GrantedAuthority> authorities = null;
		if (user==null && map.containsKey(AUTHORITIES)) {
			@SuppressWarnings("unchecked")
			String[] roles = ((Collection<String>)map.get(AUTHORITIES)).toArray(new String[0]);
			authorities = AuthorityUtils.createAuthorityList(roles);
		}
		// 创建认证请求信息
		OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null,
				null);
		return new OAuth2Authentication(request, user);
	}
	// 提取用户认证信息
	public Authentication extractAuthentication(Map<String, ?> map) {
		if (map.containsKey(USERNAME)) {
			Object principal = map.get(USERNAME);
			Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
			if (userDetailsService != null) {
				UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
				authorities = user.getAuthorities();
				principal = user;
			}
			// 创建用户名密码认证token
			return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
		}
		return null;
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105

那么问题来了,请求的地址url为“http://127.0.0.1:7000/oauth/check_token”的授权服务是如何返回用户认证信息的呢?通过《OAuth2认证流程解析》一文,我们可以直接定位到CheckTokenEndpoint端点,可以看到它的实现代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.oauth2.provider.endpoint;

import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@FrameworkEndpoint
public class CheckTokenEndpoint {
    private ResourceServerTokenServices resourceServerTokenServices;
    private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
    protected final Log logger = LogFactory.getLog(this.getClass());
    private WebResponseExceptionTranslator<OAuth2Exception> exceptionTranslator = new DefaultWebResponseExceptionTranslator();

    public CheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
        this.resourceServerTokenServices = resourceServerTokenServices;
    }

    public void setExceptionTranslator(WebResponseExceptionTranslator<OAuth2Exception> exceptionTranslator) {
        this.exceptionTranslator = exceptionTranslator;
    }

    public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
        this.accessTokenConverter = accessTokenConverter;
    }

    @RequestMapping({"/oauth/check_token"})
    @ResponseBody
    public Map<String, ?> checkToken(@RequestParam("token") String value) {
    //通过资源服务器token服务查询对应token
        OAuth2AccessToken token = this.resourceServerTokenServices.readAccessToken(value);
        if (token == null) {
            throw new InvalidTokenException("Token was not recognised");
        } else if (token.isExpired()) { //数据库token是否过期
            throw new InvalidTokenException("Token has expired");
        } else { //通过jdbctokenStore读取用户认证信息
            OAuth2Authentication authentication = this.resourceServerTokenServices.loadAuthentication(token.getValue());
            Map<String, Object> response = this.accessTokenConverter.convertAccessToken(token, authentication);//将认证信息转化为map
            response.put("active", true);
            return response;
        }
    }

    @ExceptionHandler({InvalidTokenException.class})
    public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
        this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
            public int getHttpErrorCode() {
                return 400;
            }
        };
        return this.exceptionTranslator.translate(e400);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

授权服务器resourceServerTokenServices的默认实现类是org.springframework.security.oauth2.provider.token.DefaultTokenServices
它的实现代码为:

	public OAuth2AccessToken readAccessToken(String accessToken) {
		return tokenStore.readAccessToken(accessToken);
	}

  • 1
  • 2
  • 3
  • 4

可以看到最终还是通过tokenStore读取token信息,这里我们注入的tokenStore为JdbcTokenStore所以会从数据库中查询对应的token和认证信息,最终见认证信息转为map后返回给客户端,我的token获取认证信息转为map后的结果如下:

{aud=[gate_way_server], exp=1596000111, user_name=admin, authorities=[ROLE_所有权限], client_id=my_client_id, scope=[user_info]}

  • 1
  • 2

可以看到查询到了包括如下重要信息:

可以访问的资源服务列表

  • scope范围
  • expire过期时间
  • 用户名
  • 权限数组
  • 客户端id
  • 客户端秘钥信息

以上信息很重要,是资源服务判断是否有权限访问的依据!!!

通过上述流程的解析,我们看到我们在资源服务与授权服务分离的情况下如何检验携带的token是否有对应访问权限的验证的整个过程。首先,oauth2会检查头部或查询参数是否携带access_token,没有则使用本地安全配置投票查看是否可以访问对应资源,如果存在则从授权服务指定地址获取认证信息,然后检查该token是否有改资源服务的访问权限;

以上就是通过token获取认证信息并授权访问的过程,这里只演示了认证部分,授权部分可以直接在资源服务的访问安全策略配置或者直接使用oauth2的注解形式进行控制;
(2)通过从授权服务通过用户名密码加密方式获取用户认证信息方式进行验证
这种方式其实是在访问资源服务的时候一并带上access_token(可以是请求头或查询参数),当请求达到后台之后,资源服务授权过滤器会从中获取对应的token,然后根据资源服务器配置的用户信息url传递至授权服务器获取用户认证信息。

注意:
这里的token是用户名密码加密后的token,加密方式是(base64(username:password)或),千万不要传错,我本以为是登录之后的token或是base64(client_id:client_secret), 没想到经过调试之后发现,我错了!它是用户名密码base64加密后的值!

这种方式对应的资源服务配置如下:

#oauth2客户端
security:  
  oauth2:
    resource:
      #这里的认证方式必须与授权服务的认证方式保持一致,Authorization头字段加密方式可以是basic或Bearer加密方式
      tokenType: basic
      filter-order: 3
      id: resource_server_id
      preferTokenInfo: false
      user-info-uri: http://127.0.0.1:7000/user/principal

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

preferTokenInfo设置为false并且指定了授权服务器用户信息获取地址user-info-uri,这种方式会通过token获取认证信息。具体流程我们接下来一一解析。

有了以上通过token方式验证方式解析之后,我们可以直接定位到OAuth2AuthenticationProcessingFilter

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

		final boolean debug = logger.isDebugEnabled();
		final HttpServletRequest request = (HttpServletRequest) req;
		final HttpServletResponse response = (HttpServletResponse) res;

		try {
			Authentication authentication = tokenExtractor.extract(request);
			if (authentication == null) {
				if (stateless && isAuthenticated()) {
					if (debug) {
						logger.debug("Clearing security context.");
					}
					SecurityContextHolder.clearContext();
				}
				if (debug) {
					logger.debug("No token in request, will continue chain.");
				}
			}
			else {
				request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
				if (authentication instanceof AbstractAuthenticationToken) {
					AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
					needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
				}
				Authentication authResult = authenticationManager.authenticate(authentication);
				if (debug) {
					logger.debug("Authentication success: " + authResult);
				}
				eventPublisher.publishAuthenticationSuccess(authResult);
				SecurityContextHolder.getContext().setAuthentication(authResult);
			}
		}
		catch (OAuth2Exception failed) {
			SecurityContextHolder.clearContext();
			if (debug) {
				logger.debug("Authentication request failed: " + failed);
			}
			eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
					new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
			authenticationEntryPoint.commence(request, response,
					new InsufficientAuthenticationException(failed.getMessage(), failed));
			return;
		}
		chain.doFilter(request, response);
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

最终调用认证管理器统一认证

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		if (authentication == null) {
			throw new InvalidTokenException("Invalid token (token not found)");
		}
		String token = (String) authentication.getPrincipal();
		OAuth2Authentication auth = tokenServices.loadAuthentication(token);
		if (auth == null) {
			throw new InvalidTokenException("Invalid token: " + token);
		}

		Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
		if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
			throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
		}

		checkClientDetails(auth);

		if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
			OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
			// Guard against a cached copy of the same details
			if (!details.equals(auth.getDetails())) {
				// Preserve the authentication details from the one loaded by token services
				details.setDecodedDetails(auth.getDetails());
			}
		}
		auth.setDetails(authentication.getDetails());
		auth.setAuthenticated(true);
		return auth;

	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

此时我们的tokenServices的实现为UserInfoTokenServices,它是从授权服务获取用户认证信息:

	@Override
	public OAuth2Authentication loadAuthentication(String accessToken)
			throws AuthenticationException, InvalidTokenException {
		Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
		if (map.containsKey("error")) {
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("userinfo returned error: " + map.get("error"));
			}
			throw new InvalidTokenException(accessToken);
		}
		return extractAuthentication(map);
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

它最终通过get方式从授权服务器获取用户的认证信息,地址为配置url“http://127.0.0.1:7000/user/principal”,token为访问的token,实现如下:

	@SuppressWarnings({ "unchecked" })
	private Map<String, Object> getMap(String path, String accessToken) {
		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Getting user info from: " + path);
		}
		try {
			// OAuth2 rest请求模板
			OAuth2RestOperations restTemplate = this.restTemplate;
			if (restTemplate == null) {
				BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
				resource.setClientId(this.clientId);
				restTemplate = new OAuth2RestTemplate(resource);
			}
			// 查询当前上下文是否存在OAuth2AccessToken(已登录)
			OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext()
					.getAccessToken();
			// 不存在或者发生变化
			if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
				// 创建默认的DefaultOAuth2AccessToken
				DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
						accessToken);
				token.setTokenType(this.tokenType);
				// 设置请求参数access_token
				restTemplate.getOAuth2ClientContext().setAccessToken(token);
			}
			// 携带token并调用授权服务获取用户信息(没有token是无法获取到对应的认证信息的)
			return restTemplate.getForEntity(path, Map.class).getBody();
		}
		catch (Exception ex) {
			this.logger.warn("Could not fetch user details: " + ex.getClass() + ", "
					+ ex.getMessage());
			return Collections.<String, Object>singletonMap("error",
					"Could not fetch user details");
		}
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

OAuth2RestTemplate 继承自RestTemplate,其实用法和参数也都差不多,用postForEntity发送post请求,getForEntity发送get请求。我们在授权服务提供对应的接口(与配置相同),例如我的授权服务给定的一个获取用户认证信息的接口为(http://127.0.0.1:7000/user/principal)

	@GetMapping(value = "/principal")
    public Principal me(Principal principal) {
        log.info("资源服务获取用户信息:" + principal);
        return principal;
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

测试:
我们可以通过携带token(注意,如上所说这里的token是base64(真实用户名:密码))加密后的值如(base64(admin:123))的直接使用浏览器访问授权返回结果:
在这里插入图片描述
同样,如果资源服务器通过从授权服务器获取用户认证信息的方式进行验证,那么,浏览器直接访问资源服务器,同样也需要携带一个access_token参数(头或查询参数)直接访问资源服务器的对应资源或接口即可,但是要注意,这里的access_token不是从授权服务器获取的token,而是配置类型为base64或Bearer加密的用户名和密码的值(如base64(admin:123456),注意用户名不是客户端id,密码不是客户端秘钥)。Get请求如下:

http://localhost:7001/test/hi?name=lixx&access_token=YWRtaW46MTIzNDU2 
  • 1

根据以上介绍,授权服务器提供了一个获取用户认证信息的接口,在访问该接口前客户端将获取的token放入到请求授权服务器的Authorization头字段中,在到达端点前,授权服务器经过BasicAuthenticationFilter的过滤器拦截并解析出用户名、密码,然后通过userDetails接口获取用户名密码进行比对验证,通过之后则最后到达端点/principal,最后返回用户认真信息(注,用户认证信息会自注入到端点的参数中),请求过程如上所示getForEntity所示。

当授权服务器返回用户认证信息之后,接下来资源服务会通过认证信息鉴定用户权限,通过之后到达资源服务器的请求端点并提供对应服务。

	@Override
	public OAuth2Authentication loadAuthentication(String accessToken)
			throws AuthenticationException, InvalidTokenException {
		Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
		if (map.containsKey("error")) {
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("userinfo returned error: " + map.get("error"));
			}
			throw new InvalidTokenException(accessToken);
		}
		return extractAuthentication(map);
	}

	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		if (authentication == null) {
			throw new InvalidTokenException("Invalid token (token not found)");
		}
		String token = (String) authentication.getPrincipal();
		// 远程从资源服务载入用户认证信息
		OAuth2Authentication auth = tokenServices.loadAuthentication(token);
		if (auth == null) {
			throw new InvalidTokenException("Invalid token: " + token);
		}
		// 获取用户可访问的资源id列表并坚定是否有该资源服务的访问权限
		Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
		if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
			throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
		}

		checkClientDetails(auth);

		if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
			OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
			// Guard against a cached copy of the same details
			if (!details.equals(auth.getDetails())) {
				// Preserve the authentication details from the one loaded by token services
				details.setDecodedDetails(auth.getDetails());
			}
		}
		// 返回认证信息
		auth.setDetails(authentication.getDetails());
		auth.setAuthenticated(true);
		return auth;

	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

认证成功之后,发布认证事件

				Authentication authResult = authenticationManager.authenticate(authentication);

				if (debug) {
					logger.debug("Authentication success: " + authResult);
				}

				eventPublisher.publishAuthenticationSuccess(authResult);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

最后达到我们的端点:

package com.easystudy.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;

/**@文件名称: TestController.java
 * @功能描述: TODO(用一句话描述该文件做什么)
 * @版权信息: www.easystudy.com
 * @技术交流: 961179337(QQ群)
 * @编写作者: lixx2048@163.com
 * @联系方式: 941415509(QQ)
 * @开发日期: 2020年7月27日
 * @历史版本: V1.0  
 */
@RestController
@RequestMapping("/test")
@Api(value = "OAuth2 Client测试接口文档", tags = "OAuth2 Client测试接口文档")
public class TestController {
	
	// oauth2注解
	/**
	 * @RequiresUser:subject.isRemembered()结果为true,subject.isAuthenticated() 
	 * @RequiresAuthentication:同于方法subject.isAuthenticated() 结果为true时
	 * @RequiresGuest:与@RequiresUser完全相反。
	 * @RequiresRoles("xx");有xx角色才可以访问方法
	 * @RequiresPermissions({"file:read", "write:aFile.txt"} ):同时含有file:read和write:aFile.txt的权限才能执行方法
	 */
	@GetMapping("/hi")
	@ApiOperation(value="打招呼1", notes="打招呼1")
	@ApiImplicitParams({ @ApiImplicitParam(paramType = "query", dataType = "String", name = "name", value = "名称", required = true) })
	public String hi(@RequestParam(name = "name", required = true) String name){
		return "hi " + name;
	}
	
	@GetMapping("/hello")
	@ApiOperation(value="打招呼2", notes="打招呼2")
	@ApiImplicitParams({ @ApiImplicitParam(paramType = "query", dataType = "String", name = "name", value = "名称", required = true) })
	public String hello(@RequestParam(name = "name", required = true) String name){
		return "hello " + name;
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

这里我访问的接口是/hi, 认证成功之后访问结果:
在这里插入图片描述
综上所述,资源服务器从授权服务器获取用户信息的方式有两种:

  • 通过授权服务器获取的token到授权服务器获取用户认证信息
  • 通过用户名密码编码后的token作为Authorization头字段从授权服务获取用户认证信息

三、OAuth2资源服务器id配置
我们看到资源服务器id在配置文件中配置失效,必须通过代码配置,为什么?? 关于这个问题,我们首先看看资源服务的加载过程,才能找到最终原因。

1、oauth2资源客户端配置加载流程
我们直接端点在我们的资源服务器配置的某个接口即可看到授权配置的加载整个过程,具体的启动流程我跟进了一下,如下所示:

ConfigurationClassEnhancer$BeanMethodInterceptor->  
WebSecurityConfiguration$$EnhancerBySpringCGLIB->  
WebSecurity.doBuild->  
WebSecurity.init->  
WebSecurity.performBuild->  
ResourceServerSecurityConfigurer.configure->  
ResourceServerSecurityConfigurer.oauthAuthenticationManager  

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

之所以加载对应配置,原因是引入了OAuth2 的autoConfigure自动配置类注解:

@Configuration
@ConditionalOnClass({ OAuth2AccessToken.class, WebMvcConfigurer.class })
@Import({ OAuth2AuthorizationServerConfiguration.class,
		OAuth2MethodSecurityConfiguration.class, OAuth2ResourceServerConfiguration.class,
		OAuth2RestOperationsConfiguration.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
public class OAuth2AutoConfiguration {

	private final OAuth2ClientProperties credentials;

	public OAuth2AutoConfiguration(OAuth2ClientProperties credentials) {
		this.credentials = credentials;
	}

	@Bean
	public ResourceServerProperties resourceServerProperties() {
		return new ResourceServerProperties(this.credentials.getClientId(),
				this.credentials.getClientSecret());
	}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

它导入了OAuth2AuthorizationServerConfiguration、OAuth2ResourceServerConfiguration、OAuth2RestOperationsConfiguration
配置类,并自己创建了ResourceServerProperties资源服务资源读取类,这里注意的是它是直接new出来的,而不是使用文件读取类注解读取的,虽然它包含读取数据类属性,也就是实际上没有发挥作用(而是构造起了作用):

@ConfigurationProperties(prefix = "security.oauth2.resource")
public class ResourceServerProperties implements BeanFactoryAware, InitializingBean {

	@JsonIgnore
	private final String clientId;

	@JsonIgnore
	private final String clientSecret;

	@JsonIgnore
	private ListableBeanFactory beanFactory;

	private String serviceId = "resource";

	/**
	 * Identifier of the resource.
	 */
	private String id;

	/**
	 * URI of the user endpoint.
	 */
	private String userInfoUri;

	/**
	 * URI of the token decoding endpoint.
	 */
	private String tokenInfoUri;

	/**
	 * Use the token info, can be set to false to use the user info.
	 */
	private boolean preferTokenInfo = true;

	/**
	 * The token type to send when using the userInfoUri.
	 */
	private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE;
	...
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

这就是说通过配置文件配置的id是无效的,那么是通过什么途径配置上去的?让我们具体看看加载的关键细节:

@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 创建资源服务安全配置类
		ResourceServerSecurityConfigurer resources = new ResourceServerSecurityConfigurer();
		// 创建资源服务token服务
		ResourceServerTokenServices services = resolveTokenServices();
		if (services != null) {
			resources.tokenServices(services);
		}
		else {
			if (tokenStore != null) {
				resources.tokenStore(tokenStore);
			}
			else if (endpoints != null) {
				resources.tokenStore(endpoints.getEndpointsConfigurer().getTokenStore());
			}
		}
		// 事件发布器
		if (eventPublisher != null) {
			resources.eventPublisher(eventPublisher);
		}
		// 加载自定义资源服务安全配置类---这就是关键
		for (ResourceServerConfigurer configurer : configurers) {
			configurer.configure(resources);
		}
		// @formatter:off
		http.authenticationProvider(new AnonymousAuthenticationProvider("default"))
		// N.B. exceptionHandling is duplicated in resources.configure() so that
		// it works
		.exceptionHandling()
				.accessDeniedHandler(resources.getAccessDeniedHandler()).and()
				.sessionManagement()
				.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
				.csrf().disable();
		// @formatter:on
		http.apply(resources);
		if (endpoints != null) {
			// Assume we are in an Authorization Server
			http.requestMatcher(new NotOAuthRequestMatcher(endpoints.oauth2EndpointHandlerMapping()));
		}
		for (ResourceServerConfigurer configurer : configurers) {
			// Delegates can add authorizeRequests() here
			configurer.configure(http);
		}
		if (configurers.isEmpty()) {
			// Add anyRequest() last as a fall back. Spring Security would
			// replace an existing anyRequest() matcher with this one, so to
			// avoid that we only add it if the user hasn't configured anything.
			http.authorizeRequests().anyRequest().authenticated();
		}
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

其中ResourceServerSecurityConfigurer默认的安全配置加载:

public final class ResourceServerSecurityConfigurer extends
		SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

	private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();

	private AccessDeniedHandler accessDeniedHandler = new OAuth2AccessDeniedHandler();

	private OAuth2AuthenticationProcessingFilter resourcesServerFilter;

	private AuthenticationManager authenticationManager;

	private AuthenticationEventPublisher eventPublisher = null;

	private ResourceServerTokenServices resourceTokenServices;

	private TokenStore tokenStore = new InMemoryTokenStore();
	// 默认的资源服务名称
	private String resourceId = "oauth2-resource";

	private SecurityExpressionHandler<FilterInvocation> expressionHandler = new OAuth2WebSecurityExpressionHandler();

	private TokenExtractor tokenExtractor;

	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;

	private boolean stateless = true;

	public ResourceServerSecurityConfigurer() {
		resourceId(resourceId);
	}

	private ClientDetailsService clientDetails() {
		return getBuilder().getSharedObject(ClientDetailsService.class);
	}

	public TokenStore getTokenStore() {
		return tokenStore;
	}

	/**
	 * Flag to indicate that only token-based authentication is allowed on these resources.
	 * @param stateless the flag value (default true)
	 * @return this (for fluent builder)
	 */
	public ResourceServerSecurityConfigurer stateless(boolean stateless) {
		this.stateless = stateless;
		return this;
	}

	public ResourceServerSecurityConfigurer authenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
		this.authenticationEntryPoint = authenticationEntryPoint;
		return this;
	}

	public ResourceServerSecurityConfigurer accessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
		this.accessDeniedHandler = accessDeniedHandler;
		return this;
	}

	public ResourceServerSecurityConfigurer tokenStore(TokenStore tokenStore) {
		Assert.state(tokenStore != null, "TokenStore cannot be null");
		this.tokenStore = tokenStore;
		return this;
	}

	public ResourceServerSecurityConfigurer eventPublisher(AuthenticationEventPublisher eventPublisher) {
		Assert.state(eventPublisher != null, "AuthenticationEventPublisher cannot be null");
		this.eventPublisher = eventPublisher;
		return this;
	}

	public ResourceServerSecurityConfigurer expressionHandler(
			SecurityExpressionHandler<FilterInvocation> expressionHandler) {
		Assert.state(expressionHandler != null, "SecurityExpressionHandler cannot be null");
		this.expressionHandler = expressionHandler;
		return this;
	}

	public ResourceServerSecurityConfigurer tokenExtractor(TokenExtractor tokenExtractor) {
		Assert.state(tokenExtractor != null, "TokenExtractor cannot be null");
		this.tokenExtractor = tokenExtractor;
		return this;
	}

	/**
	 * Sets a custom {@link AuthenticationDetailsSource} to use as a source
	 * of authentication details. The default is {@link OAuth2AuthenticationDetailsSource}.
	 *
	 * @param authenticationDetailsSource the custom {@link AuthenticationDetailsSource} to use
	 * @return {@link ResourceServerSecurityConfigurer} for additional customization
	 */
	public ResourceServerSecurityConfigurer authenticationDetailsSource(
			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
		Assert.state(authenticationDetailsSource != null, "AuthenticationDetailsSource cannot be null");
		this.authenticationDetailsSource = authenticationDetailsSource;
		return this;
	}

	public ResourceServerSecurityConfigurer authenticationManager(AuthenticationManager authenticationManager) {
		Assert.state(authenticationManager != null, "AuthenticationManager cannot be null");
		this.authenticationManager = authenticationManager;
		return this;
	}

	public ResourceServerSecurityConfigurer tokenServices(ResourceServerTokenServices tokenServices) {
		Assert.state(tokenServices != null, "ResourceServerTokenServices cannot be null");
		this.resourceTokenServices = tokenServices;
		return this;
	}

	@Override
	public void init(HttpSecurity http) throws Exception {
		registerDefaultAuthenticationEntryPoint(http);
	}

	@SuppressWarnings("unchecked")
	private void registerDefaultAuthenticationEntryPoint(HttpSecurity http) {
		ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = http
				.getConfigurer(ExceptionHandlingConfigurer.class);
		if (exceptionHandling == null) {
			return;
		}
		ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class);
		if (contentNegotiationStrategy == null) {
			contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
		}
		MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy,
				MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON,
				MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA,
				MediaType.TEXT_XML);
		preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
		exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher);
	}

	public ResourceServerSecurityConfigurer resourceId(String resourceId) {
		this.resourceId = resourceId;
		if (authenticationEntryPoint instanceof OAuth2AuthenticationEntryPoint) {
			((OAuth2AuthenticationEntryPoint) authenticationEntryPoint).setRealmName(resourceId);
		}
		return this;
	}
	// 配置加载认证管理器
	@Override
	public void configure(HttpSecurity http) throws Exception {
		// 创建认证管理器
		AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
		// 创建远程认证过滤器
		resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
		// 设置认证端点
		resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
		// 设置认证管理器
		resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
		// 事件发布器
		if (eventPublisher != null) {
			resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
		}
		// 令牌提取器
		if (tokenExtractor != null) {
			resourcesServerFilter.setTokenExtractor(tokenExtractor);
		}
		if (authenticationDetailsSource != null) {
			resourcesServerFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
		}
		resourcesServerFilter = postProcess(resourcesServerFilter);
		resourcesServerFilter.setStateless(stateless);

		// @formatter:off
		http
			.authorizeRequests().expressionHandler(expressionHandler)
		.and()
			.addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
			.exceptionHandling()
				.accessDeniedHandler(accessDeniedHandler)
				.authenticationEntryPoint(authenticationEntryPoint);
		// @formatter:on
	}

	private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
		OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
		if (authenticationManager != null) {
			if (authenticationManager instanceof OAuth2AuthenticationManager) {
				oauthAuthenticationManager = (OAuth2AuthenticationManager) authenticationManager;
			}
			else {
				return authenticationManager;
			}
		}
		// 设置资源服务id
		oauthAuthenticationManager.setResourceId(resourceId);
		// 设置token存储服务,此处为RemoteTokenServices
		oauthAuthenticationManager.setTokenServices(resourceTokenServices(http));
		oauthAuthenticationManager.setClientDetailsService(clientDetails());
		return oauthAuthenticationManager;
	}

	private ResourceServerTokenServices resourceTokenServices(HttpSecurity http) {
		tokenServices(http);
		return this.resourceTokenServices;
	}

	private ResourceServerTokenServices tokenServices(HttpSecurity http) {
		if (resourceTokenServices != null) {
			return resourceTokenServices;
		}
		DefaultTokenServices tokenServices = new DefaultTokenServices();
		tokenServices.setTokenStore(tokenStore());
		tokenServices.setSupportRefreshToken(true);
		tokenServices.setClientDetailsService(clientDetails());
		this.resourceTokenServices = tokenServices;
		return tokenServices;
	}

	private TokenStore tokenStore() {
		Assert.state(tokenStore != null, "TokenStore cannot be null");
		return this.tokenStore;
	}

	public AccessDeniedHandler getAccessDeniedHandler() {
		return this.accessDeniedHandler;
	}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223

在看看我们自己的资源服务配置:

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
	// 该资源服务器id必须在数据库记录中有配置,也就是对应token的用户必须该资源访问权限(密文:test_resource_secret)
	// 例如,我的数据库记录:
	// 'my_client_id','test_resource_id','$2a$10$I28j9B0T/roapkMEqfIHguARt0GgLyXwC/DOnFwPpXuQ0xTkrd632','user_info','authorization_code,refresh_token,implicit,password','http://localhost:7010/uaa/login','ROLE_ADMIN,ROLE_DEVICE,ROLE_VIDEO',3600,7200,'{\"systemInfo\":\"Atlas System\"}','true'
	// 通过授权模式或简化模式获取的token(对应用户为wx_takeout_client_id)具有访问资源服务器test_resource_id
	// 的权限,所以将该资源服务器id要与数据库的对应,否则无权访问
	// 注意:在不使用代码配置的情况下资源服务器id默认值为: oauth2-resource
	private static final String DEMO_RESOURCE_ID = "gate_way_server";

	/**
	 * @功能描述: 以代码形式配置资源服务器id,配置文件配置不生效
	 * @编写作者: lixx2048@163.com
	 * @开发日期: 2020年7月27日
	 * @历史版本: V1.0  
	 * @参数说明:
	 * @返  回  值:
	 */
	@Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
    }
	
	/**
	 * 注意:从网关经过的所有url都进行过滤,情况分为如下两种:
	 * 1、带access_token的参数url,过滤器会获取参数到授权中心去鉴权
	 * 2、不带access_token的url,过滤器会获取本地‘资源服务’鉴权配置--即如下方法(或注解形式配置)
	 * 注意“**”的使用, 使用不好可能导致权限控制失效!!!(如果url前面无单词如/oauth/...,但是匹配路径用** /oauth,就会导致权限控制失效)
	 */
    @Override
    public void configure(HttpSecurity http) throws Exception {
    	// 其他匹配的[剩下的]任何请求都需要授权
    	ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
		registry
			.anyRequest().authenticated()
			.and()
		    	.formLogin()
		    .and()
		    	.csrf().disable()
		    .httpBasic();	
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

默认资源服务名称为oauth2-resource:

// 默认的资源服务名称
	private String resourceId = "oauth2-resource";

  • 1
  • 2
  • 3

我们重写了对应方法,给定了自己的资源服务器id,这样就覆盖了oauth2-resource,达到自定义资源服务id的目的;

所以,通过以上代码,我们可以看到在不配置资源服务名称的情况下,资源服务默认名称为oauth2-resource,如果注册客户端信息的时候,没有指定资源服务的id或id不相等的情况下,用户登录后是无法访问该资源服务的,如我的client注册信息记录如下:

my_client_id	gate_way_server	$2a$10$9mmTWJd1pJ2OjWKG1G1pNuyUxIG6Lv8lic42VmBXYrVNG4ZB9FwL6	user_info	authorization_code,refresh_token,implicit,password,client_credentials	http://www.baidu.com	ROLE_ADMIN	7200	86400	{"systemInfo":"Atlas System"}	true

  • 1
  • 2

指定对应client_id为my_client_id的用户只能访问id为gate_way_server的资源服务,多个则以逗号隔开,所以我的资源服务id配置必须该为gate_way_server才能使用对应token访问!!

知道原因之后,为了能让yaml文件配置的id生效,我们可以自己读取该属性然后配置上去:

	// 自己读取属性值
	@Value("${security.oauth2.resource.id}")
	private String DEMO_RESOURCE_ID = "gate_way_server";

	/**
	 * @功能描述: 以代码形式配置资源服务器id,配置文件配置不生效
	 * @编写作者: lixx2048@163.com
	 * @开发日期: 2020年7月27日
	 * @历史版本: V1.0  
	 * @参数说明:
	 * @返  回  值:
	 */
	@Override
    public void configure(ResourceServerSecurityConfigurer resources) {
		// 通过配置方法配置资源服务器id
        resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

四、资源服务认证方式配置加载流程
我们通过以上代码知道,我们可以通过资源服务器的配置security.oauth2.resource.preferTokenInfo为true或false来控制资源服务器是以何种方式到授权服务器获取认证信息,它包括两种方式:

token令牌校验
userInfo用户信息校验
那么问题来了,它是如何通过配置方式控制不同的校验方式的?首先我带领大家看看启动的堆栈信息与第三节加载一样
在这里插入图片描述
我们依然关注资源服务配置类ResourceServerConfiguration的configure方法即可:

@Configuration
public class ResourceServerConfiguration extends WebSecurityConfigurerAdapter implements Ordered {

	private int order = 3;

	@Autowired(required = false)
	private TokenStore tokenStore;

	@Autowired(required = false)
	private AuthenticationEventPublisher eventPublisher;

	@Autowired(required = false)
	private Map<String, ResourceServerTokenServices> tokenServices;

	@Autowired
	private ApplicationContext context;

	private List<ResourceServerConfigurer> configurers = Collections.emptyList();

	@Autowired(required = false)
	private AuthorizationServerEndpointsConfiguration endpoints;

	@Override
	public int getOrder() {
		return order;
	}

	public void setOrder(int order) {
		this.order = order;
	}

	/**
	 * @param configurers the configurers to set
	 */
	@Autowired(required = false)
	public void setConfigurers(List<ResourceServerConfigurer> configurers) {
		this.configurers = configurers;
	}

	private static class NotOAuthRequestMatcher implements RequestMatcher {

		private FrameworkEndpointHandlerMapping mapping;

		public NotOAuthRequestMatcher(FrameworkEndpointHandlerMapping mapping) {
			this.mapping = mapping;
		}

		@Override
		public boolean matches(HttpServletRequest request) {
			String requestPath = getRequestPath(request);
			for (String path : mapping.getPaths()) {
				if (requestPath.startsWith(mapping.getPath(path))) {
					return false;
				}
			}
			return true;
		}

		private String getRequestPath(HttpServletRequest request) {
			String url = request.getServletPath();

			if (request.getPathInfo() != null) {
				url += request.getPathInfo();
			}

			return url;
		}

	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 创建资源服务安全配置类
		ResourceServerSecurityConfigurer resources = new ResourceServerSecurityConfigurer();
		// 根据不同的配置生成不同的资源服务token服务类
		ResourceServerTokenServices services = resolveTokenServices();
		if (services != null) {
			resources.tokenServices(services);
		}
		else {
			if (tokenStore != null) {
				resources.tokenStore(tokenStore);
			}
			else if (endpoints != null) {
				resources.tokenStore(endpoints.getEndpointsConfigurer().getTokenStore());
			}
		}
		// 设置事件发布器
		if (eventPublisher != null) {
			resources.eventPublisher(eventPublisher);
		}
		// 获取并填充资源服务器配置,这里就是我的资源服务器配置,可配置多个这里有且仅有一个
		// com.easystudy.config.ResourceServerConfiguration
		for (ResourceServerConfigurer configurer : configurers) {
			configurer.configure(resources);
		}
		// @formatter:off
		http.authenticationProvider(new AnonymousAuthenticationProvider("default"))
		// N.B. exceptionHandling is duplicated in resources.configure() so that
		// it works
		.exceptionHandling()
				.accessDeniedHandler(resources.getAccessDeniedHandler()).and()
				.sessionManagement()
				.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
				.csrf().disable();
		// 应用资源服务器配置
		// @formatter:on
		http.apply(resources);
		if (endpoints != null) {
			// Assume we are in an Authorization Server
			http.requestMatcher(new NotOAuthRequestMatcher(endpoints.oauth2EndpointHandlerMapping()));
		}
		for (ResourceServerConfigurer configurer : configurers) {
			// Delegates can add authorizeRequests() here
			configurer.configure(http);
		}
		if (configurers.isEmpty()) {
			// Add anyRequest() last as a fall back. Spring Security would
			// replace an existing anyRequest() matcher with this one, so to
			// avoid that we only add it if the user hasn't configured anything.
			http.authorizeRequests().anyRequest().authenticated();
		}
	}

	private ResourceServerTokenServices resolveTokenServices() {
		if (tokenServices == null || tokenServices.size() == 0) {
			return null;
		}
		if (tokenServices.size() == 1) {
			return tokenServices.values().iterator().next();
		}
		if (tokenServices.size() == 2) {
			// Maybe they are the ones provided natively
			Iterator<ResourceServerTokenServices> iter = tokenServices.values().iterator();
			ResourceServerTokenServices one = iter.next();
			ResourceServerTokenServices two = iter.next();
			if (elementsEqual(one, two)) {
				return one;
			}
		}
		return context.getBean(ResourceServerTokenServices.class);
	}

	private boolean elementsEqual(Object one, Object two) {
		// They might just be equal
		if (one == two) {
			return true;
		}
		Object targetOne = findTarget(one);
		Object targetTwo = findTarget(two);
		return targetOne == targetTwo;
	}

	private Object findTarget(Object item) {
		Object current = item;
		while (current instanceof Advised) {
			try {
				current = ((Advised) current).getTargetSource().getTarget();
			}
			catch (Exception e) {
				ReflectionUtils.rethrowRuntimeException(e);
			}
		}
		return current;
	}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168

这里的关键就是配置tokenStore的代码:

// 创建资源服务安全配置类
		ResourceServerSecurityConfigurer resources = new ResourceServerSecurityConfigurer();
		// 根据不同的配置生成不同的资源服务token服务类
		ResourceServerTokenServices services = resolveTokenServices();
		if (services != null) {
			resources.tokenServices(services);
		}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

其他具体的实现resolveTokenServices如下所示:

	private ResourceServerTokenServices resolveTokenServices() {
		// 本地是否加载,没有返回null
		if (tokenServices == null || tokenServices.size() == 0) {
			return null;
		}
		// 本地有且仅有一个则返回
		if (tokenServices.size() == 1) {
			return tokenServices.values().iterator().next();
		}
		// 本地有两个以上如果相等则返回任意一起
		if (tokenServices.size() == 2) {
			// Maybe they are the ones provided natively
			Iterator<ResourceServerTokenServices> iter = tokenServices.values().iterator();
			ResourceServerTokenServices one = iter.next();
			ResourceServerTokenServices two = iter.next();
			if (elementsEqual(one, two)) {
				return one;
			}
		}
		// 2个以上则获取自定义bean:类型为ResourceServerTokenServices
		return context.getBean(ResourceServerTokenServices.class);
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

它是从ResourceServerConfiguration自己的属性列表中获取的,经过调试tokenServices.size()正好为1(如UserInfoTokenServices),那么tokenServices是怎么来的? 我们看到tokenServices是Autowired自动装载进来的。这就有点难办了? 那配置文件的读取到时是哪个类?

其实想到这点,我们就可以知道,正式因为我们配置了资源服务器配置所以才会加载对应的配置文件,首先我们查看我们的资源服务器配置代码:

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
	private static final String DEMO_RESOURCE_ID = "gate_way_server";

	@Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
    	ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
		registry
			.anyRequest().authenticated()
			.and()
		    	.formLogin()
		    .and()
		    	.csrf().disable()
		    .httpBasic();	
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

它很简单,是继承了ResourceServerConfigurerAdapter资源服务器配置类适配器,那么可想而知,配置文件的加载肯定是在ResourceServerConfigurerAdapter中了再次查看发现它又实现了接口ResourceServerConfigurer,我们顺着这个思路可以直接定位到父类的各个实现子类:
在这里插入图片描述
其源码如下所示:

@ConfigurationProperties(prefix = "security.oauth2.resource")
public class ResourceServerProperties implements BeanFactoryAware, InitializingBean {

	@JsonIgnore
	private final String clientId;

	@JsonIgnore
	private final String clientSecret;

	@JsonIgnore
	private ListableBeanFactory beanFactory;

	private String serviceId = "resource";

	/**
	 * Identifier of the resource.
	 */
	private String id;

	/**
	 * URI of the user endpoint.
	 */
	private String userInfoUri;

	/**
	 * URI of the token decoding endpoint.
	 */
	private String tokenInfoUri;

	/**
	 * Use the token info, can be set to false to use the user info.
	 */
	private boolean preferTokenInfo = true;

	/**
	 * The token type to send when using the userInfoUri.
	 */
	private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE;

	private Jwt jwt = new Jwt();

	private Jwk jwk = new Jwk();

	public ResourceServerProperties() {
		this(null, null);
	}

	public ResourceServerProperties(String clientId, String clientSecret) {
		this.clientId = clientId;
		this.clientSecret = clientSecret;
	}

	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		this.beanFactory = (ListableBeanFactory) beanFactory;
	}

	public String getResourceId() {
		return this.id;
	}

	public String getServiceId() {
		return this.serviceId;
	}

	public void setServiceId(String serviceId) {
		this.serviceId = serviceId;
	}

	public String getId() {
		return this.id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getUserInfoUri() {
		return this.userInfoUri;
	}

	public void setUserInfoUri(String userInfoUri) {
		this.userInfoUri = userInfoUri;
	}

	public String getTokenInfoUri() {
		return this.tokenInfoUri;
	}

	public void setTokenInfoUri(String tokenInfoUri) {
		this.tokenInfoUri = tokenInfoUri;
	}

	public boolean isPreferTokenInfo() {
		return this.preferTokenInfo;
	}

	public void setPreferTokenInfo(boolean preferTokenInfo) {
		this.preferTokenInfo = preferTokenInfo;
	}

	public String getTokenType() {
		return this.tokenType;
	}

	public void setTokenType(String tokenType) {
		this.tokenType = tokenType;
	}

	public Jwt getJwt() {
		return this.jwt;
	}

	public void setJwt(Jwt jwt) {
		this.jwt = jwt;
	}

	public Jwk getJwk() {
		return this.jwk;
	}

	public void setJwk(Jwk jwk) {
		this.jwk = jwk;
	}

	public String getClientId() {
		return this.clientId;
	}

	public String getClientSecret() {
		return this.clientSecret;
	}

	public void afterPropertiesSet() {
		validate();
	}

	public void validate() {
		// 是否包含AuthorizationServerEndpointsConfiguration授权服务器端点配置类
		if (countBeans(AuthorizationServerEndpointsConfiguration.class) > 0) {
			// If we are an authorization server we don't need remote resource token
			// services
			return;
		}
		// 是否包含ResourceServerTokenServicesConfiguration
		if (countBeans(ResourceServerTokenServicesConfiguration.class) == 0) {
			// If we are not a resource server or an SSO client we don't need remote
			// resource token services
			return;
		}
		// 查看clientid是否配置
		if (!StringUtils.hasText(this.clientId)) {
			return;
		}
		// 校验其他资源服务器参数
		try {
			doValidate();
		}
		catch (BindException ex) {
			throw new IllegalStateException(ex);
		}
	}

	private int countBeans(Class<?> type) {
		return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, type,
				true, false).length;
	}

	private void doValidate() throws BindException {
		BindingResult errors = new BeanPropertyBindingResult(this,
				"resourceServerProperties");
		boolean jwtConfigPresent = StringUtils.hasText(this.jwt.getKeyUri())
				|| StringUtils.hasText(this.jwt.getKeyValue());
		boolean jwkConfigPresent = StringUtils.hasText(this.jwk.getKeySetUri());
		// 如果是jwt验证
		if (jwtConfigPresent && jwkConfigPresent) {
			errors.reject("ambiguous.keyUri",
					"Only one of jwt.keyUri (or jwt.keyValue) and jwk.keySetUri should"
							+ " be configured.");
		}
		// 普通验证
		if (!jwtConfigPresent && !jwkConfigPresent) {
			// 使用用户信息验证userInfoUri必填
			if (!StringUtils.hasText(this.userInfoUri)
					&& !StringUtils.hasText(this.tokenInfoUri)) {
				errors.rejectValue("tokenInfoUri", "missing.tokenInfoUri",
						"Missing tokenInfoUri and userInfoUri and there is no "
								+ "JWT verifier key");
			}
			// 使用token验证tokenInfoUri必填
			if (StringUtils.hasText(this.tokenInfoUri) && isPreferTokenInfo()) {
				if (!StringUtils.hasText(this.clientSecret)) {
					errors.rejectValue("clientSecret", "missing.clientSecret",
							"Missing client secret");
				}
			}
		}
		if (errors.hasErrors()) {
			throw new BindException(errors);
		}
	}

	public class Jwt {

		/**
		 * The verification key of the JWT token. Can either be a symmetric secret or
		 * PEM-encoded RSA public key. If the value is not available, you can set the URI
		 * instead.
		 */
		private String keyValue;

		/**
		 * The URI of the JWT token. Can be set if the value is not available and the key
		 * is public.
		 */
		private String keyUri;

		/**
		 * The location of the key store.
		 */
		private String keyStore;

		/**
		 * The key store's password
		 */
		private String keyStorePassword;

		/**
		 * The alias of the key from the key store
		 */
		private String keyAlias;

		/**
		 * The password of the key from the key store
		 */
		private String keyPassword;

		public String getKeyValue() {
			return this.keyValue;
		}

		public void setKeyValue(String keyValue) {
			this.keyValue = keyValue;
		}

		public void setKeyUri(String keyUri) {
			this.keyUri = keyUri;
		}

		public String getKeyUri() {
			return this.keyUri;
		}

		public String getKeyStore() {
			return keyStore;
		}

		public void setKeyStore(String keyStore) {
			this.keyStore = keyStore;
		}

		public String getKeyStorePassword() {
			return keyStorePassword;
		}

		public void setKeyStorePassword(String keyStorePassword) {
			this.keyStorePassword = keyStorePassword;
		}

		public String getKeyAlias() {
			return keyAlias;
		}

		public void setKeyAlias(String keyAlias) {
			this.keyAlias = keyAlias;
		}

		public String getKeyPassword() {
			return keyPassword;
		}

		public void setKeyPassword(String keyPassword) {
			this.keyPassword = keyPassword;
		}
	}

	public class Jwk {

		/**
		 * The URI to get verification keys to verify the JWT token. This can be set when
		 * the authorization server returns a set of verification keys.
		 */
		private String keySetUri;

		public String getKeySetUri() {
			return this.keySetUri;
		}

		public void setKeySetUri(String keySetUri) {
			this.keySetUri = keySetUri;
		}
	}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305

看到没,终于见到久违的配置加载类了,他加载的是配置文件前缀为“security.oauth2.resource”的配置。我们也可以看到支持的token类型、Jwt等

public interface OAuth2AccessToken {

	public static String BEARER_TYPE = "Bearer";

	public static String OAUTH2_TYPE = "OAuth2";

	...
}

	public class Jwt {

		/**
		 * The verification key of the JWT token. Can either be a symmetric secret or
		 * PEM-encoded RSA public key. If the value is not available, you can set the URI
		 * instead.
		 */
		private String keyValue;

		/**
		 * The URI of the JWT token. Can be set if the value is not available and the key
		 * is public.
		 */
		private String keyUri;

		/**
		 * The location of the key store.
		 */
		private String keyStore;

		/**
		 * The key store's password
		 */
		private String keyStorePassword;

		/**
		 * The alias of the key from the key store
		 */
		private String keyAlias;

		/**
		 * The password of the key from the key store
		 */
		private String keyPassword;

		public String getKeyValue() {
			return this.keyValue;
		}

		public void setKeyValue(String keyValue) {
			this.keyValue = keyValue;
		}

		public void setKeyUri(String keyUri) {
			this.keyUri = keyUri;
		}

		public String getKeyUri() {
			return this.keyUri;
		}

		public String getKeyStore() {
			return keyStore;
		}

		public void setKeyStore(String keyStore) {
			this.keyStore = keyStore;
		}

		public String getKeyStorePassword() {
			return keyStorePassword;
		}

		public void setKeyStorePassword(String keyStorePassword) {
			this.keyStorePassword = keyStorePassword;
		}

		public String getKeyAlias() {
			return keyAlias;
		}

		public void setKeyAlias(String keyAlias) {
			this.keyAlias = keyAlias;
		}

		public String getKeyPassword() {
			return keyPassword;
		}

		public void setKeyPassword(String keyPassword) {
			this.keyPassword = keyPassword;
		}
	}

	public class Jwk {

		/**
		 * The URI to get verification keys to verify the JWT token. This can be set when
		 * the authorization server returns a set of verification keys.
		 */
		private String keySetUri;

		public String getKeySetUri() {
			return this.keySetUri;
		}

		public void setKeySetUri(String keySetUri) {
			this.keySetUri = keySetUri;
		}
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110

通过设置断点到,我们可以看到配置加载的源头是OAuth2AutoConfiguration,这个正式我们pom中加载的自动加载jar包:

@Configuration
@ConditionalOnClass({ OAuth2AccessToken.class, WebMvcConfigurer.class })
@Import({ OAuth2AuthorizationServerConfiguration.class,
		OAuth2MethodSecurityConfiguration.class, OAuth2ResourceServerConfiguration.class,
		OAuth2RestOperationsConfiguration.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
public class OAuth2AutoConfiguration {

	private final OAuth2ClientProperties credentials;

	public OAuth2AutoConfiguration(OAuth2ClientProperties credentials) {
		this.credentials = credentials;
	}

	@Bean
	public ResourceServerProperties resourceServerProperties() {
		// 资源服务器属性加载
		return new ResourceServerProperties(this.credentials.getClientId(),
				this.credentials.getClientSecret());
	}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

它加载了OAuth2ClientProperties属性,也就是客户端security.oauth2.client配置信息:

#oauth2客户端
security:  
  oauth2:
    client:
      accessTokenUri: http://127.0.0.1:7000/oauth/token
      userAuthorizationUri: http://127.0.0.1:7000/oauth/authorize
      clientId: my_client_id
      clientSecret: my_client_secret

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

也就是说oauth2的client客户端信息是必须要配置的(我之前说过这里可以不配置,我这里道个歉,误导大家了,对不住,很多事还是的看源码!),最终我们跟进到tokenService的加载类:

@Configuration
@ConditionalOnMissingBean(AuthorizationServerEndpointsConfiguration.class)
public class ResourceServerTokenServicesConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public UserInfoRestTemplateFactory userInfoRestTemplateFactory(
			ObjectProvider<List<UserInfoRestTemplateCustomizer>> customizers,
			ObjectProvider<OAuth2ProtectedResourceDetails> details,
			ObjectProvider<OAuth2ClientContext> oauth2ClientContext) {
		return new DefaultUserInfoRestTemplateFactory(customizers, details,
				oauth2ClientContext);
	}
	// 远程tokenService配置类
	@Configuration
	@Conditional(RemoteTokenCondition.class)
	protected static class RemoteTokenServicesConfiguration {
		// tokenService服务配置类
		@Configuration
		@Conditional(TokenInfoCondition.class)
		protected static class TokenInfoServicesConfiguration {

			private final ResourceServerProperties resource;

			protected TokenInfoServicesConfiguration(ResourceServerProperties resource) {
				this.resource = resource;
			}
			// 远程tokenService创建:根据类型创建
			@Bean
			public RemoteTokenServices remoteTokenServices() {
				RemoteTokenServices services = new RemoteTokenServices();
				services.setCheckTokenEndpointUrl(this.resource.getTokenInfoUri());
				services.setClientId(this.resource.getClientId());
				services.setClientSecret(this.resource.getClientSecret());
				return services;
			}

		}

		@Configuration
		@ConditionalOnClass(OAuth2ConnectionFactory.class)
		@Conditional(NotTokenInfoCondition.class)
		protected static class SocialTokenServicesConfiguration {

			private final ResourceServerProperties sso;

			private final OAuth2ConnectionFactory<?> connectionFactory;

			private final OAuth2RestOperations restTemplate;

			private final AuthoritiesExtractor authoritiesExtractor;

			private final PrincipalExtractor principalExtractor;

			public SocialTokenServicesConfiguration(ResourceServerProperties sso,
					ObjectProvider<OAuth2ConnectionFactory<?>> connectionFactory,
					UserInfoRestTemplateFactory restTemplateFactory,
					ObjectProvider<AuthoritiesExtractor> authoritiesExtractor,
					ObjectProvider<PrincipalExtractor> principalExtractor) {
				this.sso = sso;
				this.connectionFactory = connectionFactory.getIfAvailable();
				this.restTemplate = restTemplateFactory.getUserInfoRestTemplate();
				this.authoritiesExtractor = authoritiesExtractor.getIfAvailable();
				this.principalExtractor = principalExtractor.getIfAvailable();
			}

			@Bean
			@ConditionalOnBean(ConnectionFactoryLocator.class)
			@ConditionalOnMissingBean(ResourceServerTokenServices.class)
			public SpringSocialTokenServices socialTokenServices() {
				return new SpringSocialTokenServices(this.connectionFactory,
						this.sso.getClientId());
			}

			@Bean
			@ConditionalOnMissingBean({ ConnectionFactoryLocator.class,
					ResourceServerTokenServices.class })
			public UserInfoTokenServices userInfoTokenServices() {
				UserInfoTokenServices services = new UserInfoTokenServices(
						this.sso.getUserInfoUri(), this.sso.getClientId());
				services.setTokenType(this.sso.getTokenType());
				services.setRestTemplate(this.restTemplate);
				if (this.authoritiesExtractor != null) {
					services.setAuthoritiesExtractor(this.authoritiesExtractor);
				}
				if (this.principalExtractor != null) {
					services.setPrincipalExtractor(this.principalExtractor);
				}
				return services;
			}

		}

		// 用户信息tokenService配置类:当preferTokenInfo: false也就是使用user-info-uri获取用户认证信息
		@Configuration
		@ConditionalOnMissingClass("org.springframework.social.connect.support.OAuth2ConnectionFactory")
		@Conditional(NotTokenInfoCondition.class)
		protected static class UserInfoTokenServicesConfiguration {
			// 资源服务器属性配置
			private final ResourceServerProperties sso;
			// OAuth2Rest模板
			private final OAuth2RestOperations restTemplate;
			// 权限提取器
			private final AuthoritiesExtractor authoritiesExtractor;
			// 用户认证信息提取器
			private final PrincipalExtractor principalExtractor;

			public UserInfoTokenServicesConfiguration(ResourceServerProperties sso,
					UserInfoRestTemplateFactory restTemplateFactory,
					ObjectProvider<AuthoritiesExtractor> authoritiesExtractor,
					ObjectProvider<PrincipalExtractor> principalExtractor) {
				this.sso = sso;
				this.restTemplate = restTemplateFactory.getUserInfoRestTemplate();
				this.authoritiesExtractor = authoritiesExtractor.getIfAvailable();
				this.principalExtractor = principalExtractor.getIfAvailable();
			}

			@Bean
			@ConditionalOnMissingBean(ResourceServerTokenServices.class)
			public UserInfoTokenServices userInfoTokenServices() {
				// 通过用户信息url和clientid创建用户信息token服务
				UserInfoTokenServices services = new UserInfoTokenServices(
						this.sso.getUserInfoUri(), this.sso.getClientId());
				services.setRestTemplate(this.restTemplate);
				services.setTokenType(this.sso.getTokenType());
				if (this.authoritiesExtractor != null) {
					services.setAuthoritiesExtractor(this.authoritiesExtractor);
				}
				if (this.principalExtractor != null) {
					services.setPrincipalExtractor(this.principalExtractor);
				}
				return services;
			}

		}

	}

	@Configuration
	@Conditional(JwkCondition.class)
	protected static class JwkTokenStoreConfiguration {

		private final ResourceServerProperties resource;

		public JwkTokenStoreConfiguration(ResourceServerProperties resource) {
			this.resource = resource;
		}

		@Bean
		@ConditionalOnMissingBean(ResourceServerTokenServices.class)
		public DefaultTokenServices jwkTokenServices(TokenStore jwkTokenStore) {
			DefaultTokenServices services = new DefaultTokenServices();
			services.setTokenStore(jwkTokenStore);
			return services;
		}

		@Bean
		@ConditionalOnMissingBean(TokenStore.class)
		public TokenStore jwkTokenStore() {
			return new JwkTokenStore(this.resource.getJwk().getKeySetUri());
		}
	}

	@Configuration
	@Conditional(JwtTokenCondition.class)
	protected static class JwtTokenServicesConfiguration {

		private final ResourceServerProperties resource;

		private final List<JwtAccessTokenConverterConfigurer> configurers;

		private final List<JwtAccessTokenConverterRestTemplateCustomizer> customizers;

		public JwtTokenServicesConfiguration(ResourceServerProperties resource,
				ObjectProvider<List<JwtAccessTokenConverterConfigurer>> configurers,
				ObjectProvider<List<JwtAccessTokenConverterRestTemplateCustomizer>> customizers) {
			this.resource = resource;
			this.configurers = configurers.getIfAvailable();
			this.customizers = customizers.getIfAvailable();
		}

		@Bean
		@ConditionalOnMissingBean(ResourceServerTokenServices.class)
		public DefaultTokenServices jwtTokenServices(TokenStore jwtTokenStore) {
			DefaultTokenServices services = new DefaultTokenServices();
			services.setTokenStore(jwtTokenStore);
			return services;
		}

		@Bean
		@ConditionalOnMissingBean(TokenStore.class)
		public TokenStore jwtTokenStore() {
			return new JwtTokenStore(jwtTokenEnhancer());
		}

		@Bean
		@ConditionalOnMissingBean(JwtAccessTokenConverter.class)
		public JwtAccessTokenConverter jwtTokenEnhancer() {
			JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
			String keyValue = this.resource.getJwt().getKeyValue();
			if (!StringUtils.hasText(keyValue)) {
				keyValue = getKeyFromServer();
			}
			if (StringUtils.hasText(keyValue) && !keyValue.startsWith("-----BEGIN")) {
				converter.setSigningKey(keyValue);
			}
			if (keyValue != null) {
				converter.setVerifierKey(keyValue);
			}
			if (!CollectionUtils.isEmpty(this.configurers)) {
				AnnotationAwareOrderComparator.sort(this.configurers);
				for (JwtAccessTokenConverterConfigurer configurer : this.configurers) {
					configurer.configure(converter);
				}
			}
			return converter;
		}

		private String getKeyFromServer() {
			RestTemplate keyUriRestTemplate = new RestTemplate();
			if (!CollectionUtils.isEmpty(this.customizers)) {
				for (JwtAccessTokenConverterRestTemplateCustomizer customizer : this.customizers) {
					customizer.customize(keyUriRestTemplate);
				}
			}
			HttpHeaders headers = new HttpHeaders();
			String username = this.resource.getClientId();
			String password = this.resource.getClientSecret();
			if (username != null && password != null) {
				byte[] token = Base64.getEncoder()
						.encode((username + ":" + password).getBytes());
				headers.add("Authorization", "Basic " + new String(token));
			}
			HttpEntity<Void> request = new HttpEntity<>(headers);
			String url = this.resource.getJwt().getKeyUri();
			return (String) keyUriRestTemplate
					.exchange(url, HttpMethod.GET, request, Map.class).getBody()
					.get("value");
		}

	}

	@Configuration
	@Conditional(JwtKeyStoreCondition.class)
	protected class JwtKeyStoreConfiguration implements ApplicationContextAware {

		private final ResourceServerProperties resource;
		private ApplicationContext context;

		@Autowired
		public JwtKeyStoreConfiguration(ResourceServerProperties resource) {
			this.resource = resource;
		}

		@Override
		public void setApplicationContext(ApplicationContext context) throws BeansException {
			this.context = context;
		}

		@Bean
		@ConditionalOnMissingBean(ResourceServerTokenServices.class)
		public DefaultTokenServices jwtTokenServices(TokenStore jwtTokenStore) {
			DefaultTokenServices services = new DefaultTokenServices();
			services.setTokenStore(jwtTokenStore);
			return services;
		}

		@Bean
		@ConditionalOnMissingBean(TokenStore.class)
		public TokenStore tokenStore() {
			return new JwtTokenStore(accessTokenConverter());
		}

		@Bean
		@ConditionalOnMissingBean(JwtAccessTokenConverter.class)
		public JwtAccessTokenConverter accessTokenConverter() {
			Assert.notNull(this.resource.getJwt().getKeyStore(), "keyStore cannot be null");
			Assert.notNull(this.resource.getJwt().getKeyStorePassword(), "keyStorePassword cannot be null");
			Assert.notNull(this.resource.getJwt().getKeyAlias(), "keyAlias cannot be null");

			JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

			Resource keyStore = this.context.getResource(this.resource.getJwt().getKeyStore());
			char[] keyStorePassword = this.resource.getJwt().getKeyStorePassword().toCharArray();
			KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keyStore, keyStorePassword);

			String keyAlias = this.resource.getJwt().getKeyAlias();
			char[] keyPassword = Optional.ofNullable(
					this.resource.getJwt().getKeyPassword())
					.map(String::toCharArray).orElse(keyStorePassword);
			converter.setKeyPair(keyStoreKeyFactory.getKeyPair(keyAlias, keyPassword));

			return converter;
		}
	}

	private static class TokenInfoCondition extends SpringBootCondition {

		@Override
		public ConditionOutcome getMatchOutcome(ConditionContext context,
				AnnotatedTypeMetadata metadata) {
			ConditionMessage.Builder message = ConditionMessage
					.forCondition("OAuth TokenInfo Condition");
			Environment environment = context.getEnvironment();
			Boolean preferTokenInfo = environment.getProperty(
					"security.oauth2.resource.prefer-token-info", Boolean.class);
			if (preferTokenInfo == null) {
				preferTokenInfo = environment
						.resolvePlaceholders("${OAUTH2_RESOURCE_PREFERTOKENINFO:true}")
						.equals("true");
			}
			String tokenInfoUri = environment
					.getProperty("security.oauth2.resource.token-info-uri");
			String userInfoUri = environment
					.getProperty("security.oauth2.resource.user-info-uri");
			if (!StringUtils.hasLength(userInfoUri)
					&& !StringUtils.hasLength(tokenInfoUri)) {
				return ConditionOutcome
						.match(message.didNotFind("user-info-uri property").atAll());
			}
			if (StringUtils.hasLength(tokenInfoUri) && preferTokenInfo) {
				return ConditionOutcome
						.match(message.foundExactly("preferred token-info-uri property"));
			}
			return ConditionOutcome.noMatch(message.didNotFind("token info").atAll());
		}

	}

	private static class JwtTokenCondition extends SpringBootCondition {

		@Override
		public ConditionOutcome getMatchOutcome(ConditionContext context,
				AnnotatedTypeMetadata metadata) {
			ConditionMessage.Builder message = ConditionMessage
					.forCondition("OAuth JWT Condition");
			Environment environment = context.getEnvironment();
			String keyValue = environment
					.getProperty("security.oauth2.resource.jwt.key-value");
			String keyUri = environment
					.getProperty("security.oauth2.resource.jwt.key-uri");
			if (StringUtils.hasText(keyValue) || StringUtils.hasText(keyUri)) {
				return ConditionOutcome
						.match(message.foundExactly("provided public key"));
			}
			return ConditionOutcome
					.noMatch(message.didNotFind("provided public key").atAll());
		}

	}

	private static class JwkCondition extends SpringBootCondition {

		@Override
		public ConditionOutcome getMatchOutcome(ConditionContext context,
				AnnotatedTypeMetadata metadata) {
			ConditionMessage.Builder message = ConditionMessage
					.forCondition("OAuth JWK Condition");
			Environment environment = context.getEnvironment();
			String keyUri = environment
					.getProperty("security.oauth2.resource.jwk.key-set-uri");
			if (StringUtils.hasText(keyUri)) {
				return ConditionOutcome
						.match(message.foundExactly("provided jwk key set URI"));
			}
			return ConditionOutcome
					.noMatch(message.didNotFind("key jwk set URI not provided").atAll());
		}

	}

	private static class JwtKeyStoreCondition extends SpringBootCondition {

		@Override
		public ConditionOutcome getMatchOutcome(ConditionContext context,
												AnnotatedTypeMetadata metadata) {
			ConditionMessage.Builder message = ConditionMessage
					.forCondition("OAuth JWT KeyStore Condition");
			Environment environment = context.getEnvironment();
			String keyStore = environment
					.getProperty("security.oauth2.resource.jwt.key-store");
			if (StringUtils.hasText(keyStore)) {
				return ConditionOutcome
						.match(message.foundExactly("provided key store location"));
			}
			return ConditionOutcome
					.noMatch(message.didNotFind("key store location not provided").atAll());
		}

	}

	private static class NotTokenInfoCondition extends SpringBootCondition {

		private TokenInfoCondition tokenInfoCondition = new TokenInfoCondition();

		@Override
		public ConditionOutcome getMatchOutcome(ConditionContext context,
				AnnotatedTypeMetadata metadata) {
			return ConditionOutcome
					.inverse(this.tokenInfoCondition.getMatchOutcome(context, metadata));
		}

	}

	private static class RemoteTokenCondition extends NoneNestedConditions {

		RemoteTokenCondition() {
			super(ConfigurationPhase.PARSE_CONFIGURATION);
		}

		@Conditional(JwtTokenCondition.class)
		static class HasJwtConfiguration {

		}

		@Conditional(JwkCondition.class)
		static class HasJwkConfiguration {

		}

		@Conditional(JwtKeyStoreCondition.class)
		static class HasKeyStoreConfiguration {

		}
	}

	static class AcceptJsonRequestInterceptor implements ClientHttpRequestInterceptor {

		@Override
		public ClientHttpResponse intercept(HttpRequest request, byte[] body,
				ClientHttpRequestExecution execution) throws IOException {
			request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
			return execution.execute(request, body);
		}

	}

	static class AcceptJsonRequestEnhancer implements RequestEnhancer {

		@Override
		public void enhance(AccessTokenRequest request,
				OAuth2ProtectedResourceDetails resource,
				MultiValueMap<String, String> form, HttpHeaders headers) {
			headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
		}

	}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450

当我们配置资源服务通过用户信息url方式验证的时候如下TokenServices配置生效:

			@Bean
			@ConditionalOnMissingBean(ResourceServerTokenServices.class)
			public UserInfoTokenServices userInfoTokenServices() {
				UserInfoTokenServices services = new UserInfoTokenServices(
						this.sso.getUserInfoUri(), this.sso.getClientId());
				services.setRestTemplate(this.restTemplate);
				services.setTokenType(this.sso.getTokenType());
				if (this.authoritiesExtractor != null) {
					services.setAuthoritiesExtractor(this.authoritiesExtractor);
				}
				if (this.principalExtractor != null) {
					services.setPrincipalExtractor(this.principalExtractor);
				}
				return services;
			}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

当我们配置token认证方式的时候,如下配置生效:

			@Bean
			@ConditionalOnMissingBean({ ConnectionFactoryLocator.class,
					ResourceServerTokenServices.class })
			public UserInfoTokenServices userInfoTokenServices() {
				UserInfoTokenServices services = new UserInfoTokenServices(
						this.sso.getUserInfoUri(), this.sso.getClientId());
				services.setTokenType(this.sso.getTokenType());
				services.setRestTemplate(this.restTemplate);
				if (this.authoritiesExtractor != null) {
					services.setAuthoritiesExtractor(this.authoritiesExtractor);
				}
				if (this.principalExtractor != null) {
					services.setPrincipalExtractor(this.principalExtractor);
				}
				return services;
			}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

也就是说两个tokenService是通过preferTokenInfo改变的。

以上就是资源服务与授权服务分离情况下使用token的方式访问资源服务的整个验证流程解析的全部。如有错误之处,欢迎各位提出宝贵意见。

参考文章:

  1. https://blog.csdn.net/lixiang987654321/article/details/107745692
  2. http://lzhpo.com/article/170
  3. https://zhuanlan.zhihu.com/p/111346535
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/765912
推荐阅读
相关标签
  

闽ICP备14008679号