赞
踩
对于不熟悉OAuth2的童鞋,在阅读文本之前,请先阅读我的另一篇文章《oauth2认证流程》,以便对OAuth的认证流程有一定的了解。
在设计比较好的系统中,他们一般有如下特点:
这里我要重点讲解的是关于OAuth2的单点登录实现的相关技术。 在一个大的平台中,我们一般会有一个授权独立的授权中心,这里管理了所有的客户端信息、用户信息、角色信息、权限信息等,当我们访问我们平台的任意资源的时候,最终会路由到授权中心进行授权校验,在OAuth2的部署中也可以存在两种方式:
针对第一种情况,我这里不做介绍,网络上也有较多的资料,我重点讲在OAuth2中如何实现授权服务与资源服务分离,所以重点也会放在OAuth Client端的配置和代码解析上。
通过《oauth2认证流程》一文,我当时也提到,关于资源分离的服务进行认证的时候,会有对应的过滤器进行拦截处理,最终通过远程tokenStore获取用户授权信息,然后对用的访问权限进行校验。在讲解该流程之前,我们要把环境搭建起来,以便进行测试。
OAuth Client校验权限的前提是请求query参数或header参数中携带了access_token字段,当发现对应参数的时候,会请求配置的url(授权服务接口地址)校验对应的token的权限信息,如果通过则可以访问,如果校验失败则不允许访问。
为了能顺利演示资源服务如何与授权服务交互,首先,我准备了以下服务和数据,先关配置如下:
(1)客户端信息
首先,我们在授权服务的‘oauth_client_details’表中注册(插入)如下数据,用于标识所有登录客户端的id、秘钥、可访问的资源服务、可访问的授权方式、认证后的令牌失效时间以及可使用的重定向地址以及其他额外参数等。这个数据很关键,它影响登录认证、token过期时间、可访问那些资源服务等,具体参数如下:
INSERT INTO `oauth_client_details` VALUES ('my_client_id','resource_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、 客户端id为my_client_id
2、 客户端秘钥明文为my_client_secret(通过Bcrypt加密后的密文)
3、 客户端可以访问的资源服务的id为resource_server
4、 客户端秘钥过期时间为7200秒即2个小时
5、 重定向地址暂时设置为’http://www.baidu.com’(如授权码模式接收授权码地址,多个以逗号隔开)
6、 自定义的一个json信息
(2)用户信息
我提供了一个用户表,在实现clientDetails接口是载入用户权限信息返回给Security,该用户表结构:
-- -- 创建用户表 -- CREATE TABLE IF NOT EXISTS t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, /* 表标识 */ username varchar(32) DEFAULT '', /* 用户名 */ password varchar(255) DEFAULT '', /* 密码 */ mobile varchar(16) DEFAULT '', /* 手机 */ email varchar(32) DEFAULT '', /* 电子邮件 */ userType tinyint DEFAULT 1, /* 1:运营商,2学校,3机构,4教师,5家长 */ relativeId varchar(20) DEFAULT '', /* 关联学校或机构id */ head varchar(256) DEFAULT '', /* 用户头像 */ admin tinyint DEFAULT 0, /* 是否超级管理员 */ enabled tinyint DEFAULT 1, /* 可用性 */ expired tinyint DEFAULT 0, /* 是否过期 */ locked tinyint DEFAULT 0, /* 是否锁定 */ createUser varchar(32) DEFAULT '', /* 创建用户名 */ createTime datetime default now(), /* 创建时间 */ reserver1 varchar(64) default NULL, /* 保留字段 */ reserver2 varchar(64) default NULL, /* 保留字段 */ primary key(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='system user';
授权服务初始化过程中插入了用户数据,用户名admin、密码123456(登录时会用到):
package com.easystudy.listener; import java.util.List; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import com.easystudy.enums.UserType; import com.easystudy.model.Right; import com.easystudy.model.RightItem; import com.easystudy.model.Role; import com.easystudy.model.RoleRight; import com.easystudy.model.SysUrl; import com.easystudy.model.User; import com.easystudy.model.UserRole; import com.easystudy.service.RightItemService; import com.easystudy.service.RightService; import com.easystudy.service.RoleRightService; import com.easystudy.service.RoleService; import com.easystudy.service.UrlService; import com.easystudy.service.UserRoleService; import com.easystudy.service.UserService; import com.easystudy.util.CheckUtil; import lombok.extern.slf4j.Slf4j; /** * @文件名称: AppStartupListener.java * @功能描述: 系统初始化,用于系统初始化工作 * @版权信息: www.easystudy.com * @技术交流: 961179337(QQ群) * @编写作者: lixx2048@163.com * @联系方式: 941415509(QQ) * @开发日期: 2020年7月26日 * @历史版本: V1.0 */ @Slf4j @WebListener public class AppStartupListener implements ServletContextListener{ @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private UrlService urlService; @Autowired private RightService rightService; @Autowired private RightItemService rightItemService; @Autowired private RoleRightService roleRightService; @Autowired private UserRoleService userRoleService; @Override public void contextInitialized(ServletContextEvent sce) { log.info("授权中心正在初始化..."); try{ // 初始化管理员 initAdmin(); }catch(Exception e){ log.info(e.getMessage()); } log.info("授权中心完成初始化!"); } @Override public void contextDestroyed(ServletContextEvent sce) { log.info("授权中心正在退出..."); log.info("授权中心退出完成!"); } private void initAdmin() { // 角色管理 Role role = new Role(); role.setName("ROLE_SUPER_ADMIN"); role.setDescription("超级管理员角色"); role.setUserType((long)UserType.USER_TYPE_SERVICE.getValue()); List<Role> roles = roleService.findByAttributes("ROLE_SUPER_ADMIN", null, (long)UserType.USER_TYPE_SERVICE.getValue(), null, 0L, 1L); if (CheckUtil.isNull(roles)) { roleService.add(role); } else { role.setId(roles.get(0).getId()); } // 添加超级管理员 User user = new User(); user.setUsername("admin"); user.setPassword(new BCryptPasswordEncoder().encode("123456")); user.setUserType((byte)UserType.USER_TYPE_SERVICE.getValue()); user.setAdmin((byte)1); // 默认值字段赋默认值 user.setEnabled((byte)1); user.setExpired((byte)0); user.setLocked((byte)0); User u = userService.findByUsername("admin"); if (CheckUtil.isNull(u)) { userService.add(user); } else { user.setId(u.getId()); } // 增加用户角色信息 try { UserRole userRole = new UserRole(); userRole.setUserId(user.getId()); userRole.setRoleId(role.getId()); userRoleService.add(userRole); } catch (Exception e) { if (!(e instanceof DuplicateKeyException)) { e.printStackTrace(); log.error("增加用户角色异常:" + e.getMessage()); } } // 添加接口地址 try { SysUrl url = new SysUrl(); url.setId(0L); url.setDescription("所有接口"); url.setModify(-1); url.setUrl("/**"); urlService.add(url); } catch (Exception e) { if (!(e instanceof DuplicateKeyException)) { e.printStackTrace(); log.error("增加管理员接口异常:" + e.getMessage()); } } // 添加权限 try { Right right = new Right(); right.setId(0L); right.setName("超级管理员权限"); right.setDescription("所有权限"); rightService.add(right); } catch (Exception e) { if (!(e instanceof DuplicateKeyException)) { e.printStackTrace(); log.error("增加管理员接口异常:" + e.getMessage()); } } // 角色权限 try { RoleRight rr = new RoleRight(); rr.setRoleId(role.getId()); rr.setRightId(0L); roleRightService.add(rr); } catch (Exception e) { if (!(e instanceof DuplicateKeyException)) { e.printStackTrace(); log.error("增加角色权限异常:" + e.getMessage()); } } // 添加权限接口明细 try { RightItem item = new RightItem(); item.setRightId(0L); item.setUrlId(0L); rightItemService.add(item); } catch (Exception e) { if (!(e instanceof DuplicateKeyException)) { e.printStackTrace(); log.error("增加管理员权限接口异常:" + e.getMessage()); } } } }
#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
这里,我配置的授权服务器token校验地址为http://127.0.0.1:7000/oauth/check_token,使用token进行校验(当然也可以使用用户信息进行校验,指定授权服务器获取用户认证信息地址并设置preferTokenInfo为false即可)
(2)授权服务器资源配置
资源服务器可以对本资源服务的所有资源的访问权限进行配置,包括配置资源服务自己的id、接口访问权限等,如我的配置如下所示:
package com.easystudy.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; /** * @文件名称: ResourceServerConfiguration.java * @功能描述: 资源服务访问配置 * @版权信息: www.easystudy.com * @技术交流: 961179337(QQ群) * @编写作者: lixx2048@163.com * @联系方式: 941415509(QQ) * @开发日期: 2020年7月27日 * @历史版本: V1.0 */ @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要与数据库的对应,否则无权访问 private static final String DEMO_RESOURCE_ID = "resource_server_id"; /** * @功能描述: 以代码形式配置资源服务器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)请求地址:http://localhost:7000/oauth/token?username=admin&password=123456&grant_type=password
(2)请求方式:POST
(3)请求头:Authorization:base64(my_client_id:my_client_secret)
请求头:
请求参数:
最后换取的token:59fc7cf6-2a13-4b5d-8e17-711c26cc8705(如果token在2小时失效则依旧可以通过该接口换取新token)
这种方式对应的资源服务配置如下:
#oauth2客户端
security:
oauth2:
resource:
filter-order: 3
id: resource_server_id
tokenInfoUri: http://127.0.0.1:7000/oauth/check_token
preferTokenInfo: true
preferTokenInfo设置为true并且指定了token授权服务器验证地址,这种方式会通过token获取认证信息。具体流程我稍后慢慢解析。
通过《oauth2认证流程》一文的认证流程分析可以知道,OAuth客户端认证请求处理的过滤器是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 { // 根据请求参数(包括请求查询参数或头部参数)中的access_token远程授权服务换取认证信息 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 { // 保存认证信息:OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE=59fc7cf6-2a13-4b5d-8e17-711c26cc8705 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); }
认证信息由TokenExtractor的实现类BearerTokenExtractor类生成,具体代码如下:
protected String extractToken(HttpServletRequest request) { // first check the header... // 提取头部中的access_token参数 String token = extractHeaderToken(request); // bearer type allows a request parameter as well if (token == null) { logger.debug("Token not found in headers. Trying request parameters."); // 再次从查询参数中获取access_token token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN); if (token == null) { logger.debug("Token not found in request parameters. Not an OAuth2 request."); } // 保存token类型为Bearer else { request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE); } } return token; } // 提取头部Authorization的token,格式: Bearer token,xx protected String extractHeaderToken(HttpServletRequest request) { // 获取授权字段Authorization Enumeration<String> headers = request.getHeaders("Authorization"); while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that) String value = headers.nextElement(); // 如果是以Bearer开始 if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) { String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim(); // Add this here for the auth details later. Would be better to change the signature of this method. request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim()); // 提取Bearer之后的token int commaIndex = authHeaderValue.indexOf(','); if (commaIndex > 0) { authHeaderValue = authHeaderValue.substring(0, commaIndex); } return authHeaderValue; } } return null; }
最后交给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; }
可以看到该认证管理器最终使用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; }
那么问题来了,请求的地址url为“http://127.0.0.1:7000/oauth/check_token”的授权服务是如何返回用户认证信息的呢?通过《OAuth2认证流程解析》一文,我们可以直接定位到CheckTokenEndpoint端点,可以看到它的实现代码如下:
@FrameworkEndpoint public class CheckTokenEndpoint { private ResourceServerTokenServices resourceServerTokenServices; private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter(); protected final Log logger = LogFactory.getLog(getClass()); private WebResponseExceptionTranslator<OAuth2Exception> exceptionTranslator = new DefaultWebResponseExceptionTranslator(); public CheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) { this.resourceServerTokenServices = resourceServerTokenServices; } /** * @param exceptionTranslator the exception translator to set */ public void setExceptionTranslator(WebResponseExceptionTranslator<OAuth2Exception> exceptionTranslator) { this.exceptionTranslator = exceptionTranslator; } /** * @param accessTokenConverter the accessTokenConverter to set */ public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) { this.accessTokenConverter = accessTokenConverter; } @RequestMapping(value = "/oauth/check_token") @ResponseBody public Map<String, ?> checkToken(@RequestParam("token") String value) { // 通过资源服务器token服务查询对应token OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value); if (token == null) { throw new InvalidTokenException("Token was not recognised"); } // 数据库token是否过期 if (token.isExpired()) { throw new InvalidTokenException("Token has expired"); } // 通过jdbctokenStore读取用户认证信息 OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue()); // 将认证信息转化为map Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication); // gh-1070 response.put("active", true); // Always true if token exists and not expired return response; } @ExceptionHandler(InvalidTokenException.class) public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception { logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage()); // This isn't an oauth resource, so we don't want to send an // unauthorized code here. The client has already authenticated // successfully with basic auth and should just // get back the invalid token error. @SuppressWarnings("serial") InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) { @Override public int getHttpErrorCode() { return 400; } }; return exceptionTranslator.translate(e400); } }
授权服务器resourceServerTokenServices的默认实现类是org.springframework.security.oauth2.provider.token.DefaultTokenServices
它的实现代码为:
public OAuth2AccessToken readAccessToken(String accessToken) {
return tokenStore.readAccessToken(accessToken);
}
可以看到最终还是通过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]}
可以看到查询到了包括如下重要信息:
当token过期之后前端浏览器显示错误:
无资源服务器访问权限:
认证成功之后访问响应:
通过上述流程的解析,我们看到我们在资源服务与授权服务分离的情况下如何检验携带的token是否有对应访问权限的验证的整个过程。首先,oauth2会检查头部或查询参数是否携带access_token,没有则使用本地安全配置投票查看是否可以访问对应资源,如果存在则从授权服务指定地址获取认证信息,然后检查该token是否有改资源服务的访问权限;
以上就是通过token获取认证信息并授权访问的过程,这里只演示了认证部分,授权部分可以直接在资源服务的访问安全策略配置或者直接使用oauth2的注解形式进行控制;
这种方式其实是在访问资源服务的时候一并带上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
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); }
最终调用认证管理器统一认证
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; }
此时我们的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);
}
它最终通过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"); } }
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;
}
测试:
我们可以通过携带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
根据以上介绍,授权服务器提供了一个获取用户认证信息的接口,在访问该接口前客户端将获取的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; }
认证成功之后,发布认证事件
Authentication authResult = authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
eventPublisher.publishAuthenticationSuccess(authResult);
最后达到我们的端点:
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; } }
这里我访问的接口是/hi, 认证成功之后访问结果:
综上所述,资源服务器从授权服务器获取用户信息的方式有两种:
我们看到资源服务器id在配置文件中配置失效,必须通过代码配置,为什么?? 关于这个问题,我们首先看看资源服务的加载过程,才能找到最终原因。
我们直接端点在我们的资源服务器配置的某个接口即可看到授权配置的加载整个过程,具体的启动流程我跟进了一下,如下所示:
ConfigurationClassEnhancer$BeanMethodInterceptor->
WebSecurityConfiguration$$EnhancerBySpringCGLIB->
WebSecurity.doBuild->
WebSecurity.init->
WebSecurity.performBuild->
ResourceServerSecurityConfigurer.configure->
ResourceServerSecurityConfigurer.oauthAuthenticationManager
之所以加载对应配置,原因是引入了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()); } }
它导入了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; ... }
这就是说通过配置文件配置的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(); } }
其中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; } }
在看看我们自己的资源服务配置:
@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(); } }
默认资源服务名称为oauth2-resource:
// 默认的资源服务名称
private String resourceId = "oauth2-resource";
我们重写了对应方法,给定了自己的资源服务器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
指定对应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); }
我们通过以上代码知道,我们可以通过资源服务器的配置security.oauth2.resource.preferTokenInfo为true或false来控制资源服务器是以何种方式到授权服务器获取认证信息,它包括两种方式:
那么问题来了,它是如何通过配置方式控制不同的校验方式的?首先我带领大家看看启动的堆栈信息与第三节加载一样
我们依然关注资源服务配置类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; } }
这里的关键就是配置tokenStore的代码:
// 创建资源服务安全配置类
ResourceServerSecurityConfigurer resources = new ResourceServerSecurityConfigurer();
// 根据不同的配置生成不同的资源服务token服务类
ResourceServerTokenServices services = resolveTokenServices();
if (services != null) {
resources.tokenServices(services);
}
其他具体的实现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); }
它是从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(); } }
它很简单,是继承了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; } } }
看到没,终于见到久违的配置加载类了,他加载的是配置文件前缀为“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; } }
通过设置断点到,我们可以看到配置加载的源头是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()); } }
它加载了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
也就是说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)); } } }
当我们配置资源服务通过用户信息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;
}
当我们配置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; }
也就是说两个tokenService是通过preferTokenInfo改变的。
以上就是资源服务与授权服务分离情况下使用token的方式访问资源服务的整个验证流程解析的全部。如有错误之处,欢迎各位提出宝贵意见。
源码获取、合作、技术交流请获取如下联系方式:
QQ交流群:961179337
微信账号:lixiang6153
公众号:IT技术快餐
电子邮箱:lixx2048@163.com
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。