跳到主要内容

用户中台重构

背景

早期由于业务需要快速发展,以及开发人员水平原因

没有统一的用户中台

所有的用户鉴权都是共用一套代码

每个系统需要鉴权的时候就是copy这份代码,然后基于这套代码进行鉴权

也就是没有网关,每个系统都要拥有自己的鉴权。

大致架构如下

alt text

问题

为什么需要重构呢

  1. 鉴权代码在每个服务有一套,维护起来非常不方便,需要修改多个服务。稍有不慎,某个服务的代码修改遗漏了,就会出事故
  2. 新服务鉴权困难,新服务如果需要鉴权,又需要一个比较熟悉这套鉴权逻辑的人把这部分代码迁移过去
  3. 每个服务都去鉴权,影响性能。包括请求的性能和项目编译的性能

好处

独立出来一个用户中台后,有如下好处

  1. 所有代码统一维护,提升开发效率,系统稳定性
  2. 减少业务系统代码量,提升编译启动速度
  3. 新服务无需关注鉴权,只需要关注业务即可

重构后的架构

alt text

重构后主要是网关调用用户中台进行鉴权

鉴权通过则可放行请求到对应的后端服务中

目标

  1. 用户系统拆分成一个独立系统
  2. 多端相同功能的接口需要统一成一个
  3. 多端不同token需要统一使用相同的token
  4. 多端相同功能的Filter需要统一成一个
  5. 不要影响线上业务系统的正常登入
  6. 一期只做登入校验

整体改造方法

对于一个不熟悉的用户系统,重构成用户中台。我们需要指定详细的规划与技术方案

大致分如下几个过程

  1. 熟悉整个用户相关代码、流程
  2. 整理所有需要迁移的接口、拦截器
  3. 指定迁移计划

改造需要解决的问题

整体鉴权基于Sa-token框架设计

  1. 多个系统代码大致相同,但是有细微差异,比如不同系统的鉴权token不一致,大致有如下几种token
    • X-Token
    • X-MToken
    • X-SToken
    • X-WToken

重构需要将token统一

  1. 用户信息使用redis默认的序列化方式,需要通报同类名,才能序列化成功,想要兼容旧数据,就必须保持一致

  2. 用户请求API鉴权使用request.getRequestURI()获取API路径

如果新服务新增请求context,就会多出一个服务前缀比如user-service报错

需要将所有request.getRequestURI()替换成getServletPath

  1. web端使用cookie,但是其他端不用

  2. 不同系统端比如app端和web端同个功能是两个接口,接口之前部分有细微逻辑差异,需要将多端接口合并成一个,提高代码内聚,提高代码可维护性

  3. 用户服务依赖一些基础服务,之前是内部java调用,拆分出去后需要考虑如何处理

  4. 用户服务之前发送过Spring事件,拆分出去后JVM不在共享,spring事件需要改造

  5. 部分接口耦合用户和其他服务的业务逻辑,需要拆分改造,并不影响现有业务系统

详细迁移方案

  1. 新建user-service服务

  2. 改造Sa-token源码实现多端登入验证

    • 由于多端token名字不同,sa token仅支持单个名字前缀token校验,所以需要对sa token源码进行改造 支持动态获取token前缀来进行tokan校验

    修改方式如下 修改SaTokenConfig源码

    提供一个private final ThreadLocal<String> TOKEN_NAME_HOLDER = new ThreadLocal<>();

    动态替换token名字,解决多端token name不一致

    token name的设值通过拦截器实现

@RequiredArgsConstructor
public class RequestSourceInterceptor implements HandlerInterceptor {

private static final String API_PREFIX = "/api/";

private static final String WEB_API_PREFIX = "/wapi/";

private final SaTokenConfig saTokenConfig;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

String uri = request.getServletPath();

for (SaTokenEnum value : SaTokenEnum.values()) {
boolean source = source(request, value);
if (source) {
return true;
}
}

// 未登入
if (uri.startsWith(API_PREFIX)) {
ApiOriginUtil.set(SaTokenEnum.API);
return true;
}
if (uri.startsWith(WEB_API_PREFIX)) {
ApiOriginUtil.set(SaTokenEnum.WEB_API);
return true;
}

return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
ApiOriginUtil.clear();
saTokenConfig.removeTokenName();
}

/**
* todo 未登入场景通过域名区分
* @param request
* @param saTokenEnum
* @return
*/
public boolean source(HttpServletRequest request, SaTokenEnum saTokenEnum) {
String tokenName = request.getHeader(saTokenEnum.getTokenName());
if (StrUtil.isNotBlank(tokenName)) {
ApiOriginUtil.set(saTokenEnum);
saTokenConfig.tokenName(tokenName);
return true;
}
return false;
}

}

识别token name规则如下

  1. 不同端通过不同域名过来,可以基于请求域名区分
  2. 不同端有不同的接口前缀,可以基于接口前缀区分
  3. 登入用户会传入token name到请求头

sa token主要修改方法getTokenName,其他方法会调用该方法

   public String getTokenName() {
String currentTokenName = TOKEN_NAME_HOLDER.get();
return StrUtil.isNotEmpty(currentTokenName) ? currentTokenName : tokenName;
}