3

【HZERO微服务平台3】源码分析之oauth服务token生成、校验、获取信息、传递 - InfoQ...

 2 years ago
source link: https://xie.infoq.cn/article/ee2ea70ee8b536519f639c350
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【HZERO 微服务平台 3】源码分析之 oauth 服务 token 生成、校验、获取信息、传递

作者:qiaoxingxing
  • 2021 年 12 月 13 日
  • 本文字数:3433 字

    阅读完需:约 11 分钟

【HZERO微服务平台3】源码分析之oauth服务token生成、校验、获取信息、传递

hzero-oauth 服务是基于 spring securityspring security oauthJWT 实现的统一认证服务中心,支持 oauth2.0 的四种授权模式:授权码模式简化模式密码模式客户端模式,授权流程跟标准的 oauth2 流程一致。web 端采用简化模式(implicit)登录系统,移动端可使用密码模式(password)登录系统 。

完整的功能介绍: 认证服务

深入了解 hzero-oauth 需要熟练使用 spring security oauth, 简单描述一下它的功能:

oauth2 是开放的标准协议, spring security oauth 提供了实现, 授权中心(authorization server)用@EnableAuthorizationServer及相关配置实现, 资源服务(resource server)用@EnableResourceServer及相关配置实现;授权中心提供授权(/oauth/authorize)、获取 token(/oauth/token)等接口, 资源服务实现对 token 的校验、信息提取; hzero 的 oauth 服务既是授权中心也是资源服务;

OAuth2.0 的 RFC 文档: RFC 6749 - The OAuth 2.0 Authorization Framework

spring 官方开发文档: OAuth 2 Developers Guide

如何以纯文本方式快速记录java代码的调用过程

服务间 token 的传递过程流程图

前端使用 oauth2 流程获取 token(uuid 格式), 之后的请求必须携带 token, token 在服务间传递的示意图:

c1b5fad74ca4e9166ca5f96ace84e39c.png
  1. 前端获取的 uuid 格式的 token(相当于 sessionId), 传递给网关;

  2. 网关使用 uuid token 获取用户信息, 把用户信息转换 jwt token, 并添加到jwt_tokenheader 里, 传递到后端服务; 如果获取用户信息失败, 直接返回 401(认证失败);

  3. 后端服务从 jwt_token 里解析、获取用户信息;

从 oauth 服务获取 token 的过程

调用post /oauth/token接口获取 token 的过程:

TokenEndpoint#postAccessTokenOAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);  //责任链模式, 每种授权模式对应一个granterAbstractTokenGranter#grantClientDetails client = clientDetailsService.loadClientByClientId(clientId);AbstractTokenGranter#getAccessTokenreturn tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));    AbstractTokenGranter#getOAuth2Authentication //这个方法会被子类granter覆写    return new OAuth2Authentication(storedOAuth2Request, null); DefaultTokenServices#createAccessToken(OAuth2Authentication)  //hzero修改版DefaultTokenServices#createAccessToken(OAuth2Authentication , OAuth2RefreshToken) //hzero修改版return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
  • 调试技巧: 在最内层的方法上打断点, 看调用堆栈;

  • "hzero 修改版"表示: hzero 直接把 spring 的某些代码保留包名、类名复制到了项目里, 相当于直接替换了源码, 一种不太好的 hack 方法;

  • AbstractTokenGranter的子类AuthorizationCodeTokenGranter、ImplicitTokenGranter、ClientCredentialsTokenGranter、ResourceOwnerPasswordTokenGranter分别对应四种授权模式, 可以增加新的 Granter, 优雅的实现新的认证方式.

  • 调用过程的阅读方式: 如何以纯文本方式快速记录java代码的调用过程

oauth 服务校验 token 的过程

携带 token 调用接口时, 对 token 的检验过程:

OAuth2AuthenticationProcessingFilter#doFilterAuthentication authResult = authenticationManager.authenticate(authentication);OAuth2AuthenticationManager#authenticateOAuth2Authentication auth = tokenServices.loadAuthentication(token);DefaultTokenServices#loadAuthenticationOAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);CustomRedisTokenStore#readAccessToken //从redis里读取、反序列化

如果快过期, 自动延长有效时间;

DefaultTokenServices#loadAuthentication

//如果快过期, 自动增加有效时间;if (accessToken.getExpiresIn() < 3600) {    Long deltaMs = 4 * 3600 * 1000L; //4小时, 单位是毫秒;    ((DefaultOAuth2AccessToken) accessToken).setExpiration(new Date(System.currentTimeMillis() + deltaMs));    tokenStore.storeAccessToken(accessToken, result);}

gateway 获取用户信息(principal)的过程

  • gateway 把 uuid 转换为 jwt 是在AddJwtFilter

  • 用户信息最终是 oauth 服务从CustomRedisTokenStore里读取的;

gateway 服务里:从 gateway 调用非 public 的任意接口时:

GetUserDetailsFilter#runCustomUserDetailsWithResult result = this.getUserDetailsService.getUserDetails(accessToken);GetUserDetailsServiceImpl#getUserDetails //调用oauth服务的/oauth/api/user

注意: oauth/api/user接口是 within 接口, 直接从网关调用会报错: error.permission.withinForbidden

<oauth><status>PERMISSION_WITH_IN</status><code>error.permission.withinForbidden</code><message>No access to within interface</message></oauth>

oauth 服务里:

// oauth/api/userOauthController#userreturn principal;

principal 来自 SecurityContext, SecurityContext 来自 OAuth2AuthenticationProcessingFilter:

OAuth2AuthenticationProcessingFilter#doFilterAuthentication authResult = authenticationManager.authenticate(authentication);    OAuth2AuthenticationManager#authenticate    OAuth2Authentication auth = tokenServices.loadAuthentication(token);SecurityContextHolder.getContext().setAuthentication(authResult);

关于additionInfo字段:

  • DefaultTokenServices#loadAuthentication 的返回结果包含 additionInfo, 但序列化的之后不包含, 因为 spring 添加了 ignore 注解;

  • principal 序列化把 additionInfo 字段里信息, 放到了和 client_id 同级的位置;

oauth 服务创建用户信息(principal)的过程

principal 来自Object SecurityContext.getAuthentication().getPrincipal(), Object 具体是什么类型需要看AuthenticationToken设置了什么值;

client_credentials 模式

principal 是CustomClientDetails类型:

...ClientCredentialsTokenGranter#grantAbstractTokenGranter#grantClientDetails client = clientDetailsService.loadClientByClientId(clientId);    CustomClientDetailsService#loadClientByClientId    clientDetailsWrapper.warp(clientDetails, client.getId(), client.getOrganizationId());  //角色、租户等信息来自这里return getAccessToken(client, tokenRequest);AbstractTokenGranter#getAccessTokenreturn tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));ClientCredentialsTokenGranter#getOAuth2Authentication //hzero修改版return new ClientOAuth2Authentication(storedOAuth2Request, new ClientAuthenticationToken(client)); //new ClientAuthenticationToken(client)的入参client是principal, 是CustomClientDetails
a49e9e7877838b1e712c81ae1241b0f4.png

password 模式

principal 是CustomUserDetails类型:

...ResourceOwnerPasswordTokenGranter#getOAuth2AuthenticationuserAuth = authenticationManager.authenticate(userAuth);    ProviderManager#authenticate    AbstractUserDetailsAuthenticationProvider#authenticate    user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);        CustomAuthenticationProvider#retrieveUser        return getUserDetailsService().loadUserByUsername(username);        CustomUserDetailsService#loadUserByUsername    return createSuccessAuthentication(principalToReturn, authentication, user);return new OAuth2Authentication(storedOAuth2Request, userAuth);
f60f44e186a02ee4d3474695ce0b0c2c.png

业务服务从 jwt_token 获取用户信息的过程

调试思路: 给JwtTokenExtractor打断点, 看调用堆栈;

业务服务里 hzero 没有用 spring oauth 的@EnableResourceServer, 自定义了JwtTokenFilter, 相当于OAuth2AuthenticationProcessingFilter的功能:

JwtTokenFilter#doFilterAuthentication authentication = this.tokenExtractor.extract(httpRequest);Authentication authResult = this.authenticate(authentication);    JwtTokenFilter#authenticate    this.tokenServices.loadAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authResult);

使用方法: 封装好的方法:DetailsHelper.getUserDetails()

划线
评论
复制
发布于: 2021 年 12 月 13 日阅读数: 877

qiaoxingxing

关注

还未添加个人签名 2021.12.07 加入

还未添加个人简介


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK