写点什么

【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 在服务间传递的示意图:



  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
复制代码


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);
复制代码


业务服务从 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()

用户头像

qiaoxingxing

关注

还未添加个人签名 2021.12.07 加入

还未添加个人简介

评论

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