txw/docs/SSO认证模块详解.md

43 KiB
Raw Blame History

碳信网 SSO 认证模块详解

本文档基于 txw-sso 服务代码分析,详细说明注册登录流程、表结构定义、外部服务依赖。


一、模块定位

txw-sso 是碳信网项目的统一认证服务,负责:

  • 用户账号密码登录/短信验证码登录
  • OAuth2.0 令牌管理
  • DID去中心化身份登录集成
  • 图形验证码/短信验证码服务
  • 账户锁定(防暴力破解)

二、核心表结构

2.1 OAuth2 令牌相关表

txw_sso_access_token访问令牌表

CREATE TABLE txw_sso_access_token (
  uuid VARCHAR(64) PRIMARY KEY,        -- 令牌UUID
  yh_uuid VARCHAR(64),                  -- 用户UUID
  qyuuid VARCHAR(64),                  -- 企业UUID
  access_token VARCHAR(512),           -- 访问令牌
  refresh_token VARCHAR(512),          -- 刷新令牌
  clientid VARCHAR(64),                -- 客户端ID
  sqnr TEXT,                           -- 授权内容(JSON)
  gqsj DATETIME,                       -- 过期时间
  gllp VARCHAR(512)                    -- 关联令牌(第三方token)
);

txw_sso_refresh_token刷新令牌表

CREATE TABLE txw_sso_refresh_token (
  uuid VARCHAR(64) PRIMARY KEY,
  yh_uuid VARCHAR(64),
  refresh_token VARCHAR(512),
  clientid VARCHAR(64),
  sqnr TEXT,
  gqsj DATETIME                         -- 过期时间(通常30天)
);

txw_sso_code授权码表

CREATE TABLE txw_sso_code (
  uuid VARCHAR(64) PRIMARY KEY,
  yh_uuid VARCHAR(64),
  sqm VARCHAR(128),                    -- 授权码
  clientid VARCHAR(64),
  sqnr TEXT,
  gqsj DATETIME,                       -- 过期时间(通常10分钟)
  cdxdz VARCHAR(512),                  -- 重定向地址
  rzzt VARCHAR(2),                     -- 认证状态
  access_token VARCHAR(512)
);

txw_sso_clientOAuth2客户端表

CREATE TABLE txw_sso_client (
  uuid VARCHAR(64) PRIMARY KEY,
  clientid VARCHAR(64),                -- 客户端ID
  sqmy VARCHAR(256),                    -- 授权密钥(secret)
  yxbz VARCHAR(2),                      -- 有效标志(Y/N)
  fwlpyxq VARCHAR(32),                 -- 访问令牌有效期(秒)
  sxlpyxq VARCHAR(32),                 -- 刷新令牌有效期(秒)
  cdxdz VARCHAR(512),                  -- 重定向地址
  sqnr TEXT,                           -- 授权范围
  dcdz VARCHAR(512)                    -- 登出地址
);

2.2 用户信息存储

注意: 用户信息(账号、密码、手机号等)不存储在 SSO 服务,而是存储在 txw-mhzc碳门户服务中,通过 API 调用获取。

用户信息表结构在 mhzc 服务中定义YhxxbDTO


三、登录流程详解

3.1 账号密码登录

用户输入(用户名+密码+图形验证码)
         │
         ▼
┌─────────────────────────────────────────────────┐
│  校验图形验证码                                  │
│  verifyService.checkCaptcha(uuid, code)        │
│  验证码错误 → 返回错误                           │
└─────────────────────┬───────────────────────────┘
                      │ 验证通过
                      ▼
┌─────────────────────────────────────────────────┐
│  校验账号                                        │
│  yhxxService.getYhxxByDlzh(username)            │
│  用户不存在 → 返回"用户名或密码错误"             │
└─────────────────────┬───────────────────────────┘
                      │ 用户存在
                      ▼
┌─────────────────────────────────────────────────┐
│  检查账户锁定状态                                │
│  accountLockService.checkLockStatus(yhUuid)      │
│  已锁定 → 返回"密码错误次数过多,账户已锁定"      │
└─────────────────────┬───────────────────────────┘
                      │ 未锁定
                      ▼
┌─────────────────────────────────────────────────┐
│  校验密码                                        │
│  MD5(rawPassword) == dlmm                      │
│  密码错误 → 记录错误次数 → 达到阈值则锁定         │
│            → 返回"用户名或密码错误"              │
└─────────────────────┬───────────────────────────┘
                      │ 密码正确
                      ▼
┌─────────────────────────────────────────────────┐
│  清除密码错误次数缓存                            │
│  accountLockService.clearCache(yhUuid)          │
└─────────────────────┬───────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────┐
│  创建访问令牌                                    │
│  oauth2TokenService.createAccessToken()          │
│  → 生成 access_token + refresh_token            │
│  → 存储到数据库 + Redis                         │
└─────────────────────┬───────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────┐
│  返回登录凭证                                    │
│  { accessToken, refreshToken, expiresTime }     │
│  同时写入 HttpOnly Cookie                        │
└─────────────────────────────────────────────────┘

API 路径: POST /sso/auth/login

请求参数:

{
  "username": "string",           // 登录账号
  "password": "string",           // 密码(MD5)
  "captchaVerification": "uuid",  // 图形验证码UUID
  "captchaCode": "string"         // 图形验证码内容
}

3.2 短信验证码登录

用户输入(手机号+短信验证码+图形验证码)
         │
         ▼
┌─────────────────────────────────────────────────┐
│  校验图形验证码                                  │
└─────────────────────┬───────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────┐
│  检查短信验证码是否正确                          │
│  Redis: sms_token:{phone+sms}                   │
│  不存在或已过期 → 返回"验证码错误"               │
└─────────────────────┬───────────────────────────┘
                      │ 验证码正确
                      ▼
┌─────────────────────────────────────────────────┐
│  根据手机号查询用户                              │
│  yhxxService.getYhxxBySjhm(phone)              │
│  用户不存在 → 返回"该手机号未注册"              │
└─────────────────────┬───────────────────────────┘
                      │ 用户存在
                      ▼
┌─────────────────────────────────────────────────┐
│  创建访问令牌(同账号密码登录)                    │
└─────────────────────────────────────────────────┘

API 路径: POST /sso/auth/loginBySMS

请求参数:

{
  "sjhm": "string",               // 手机号
  "sms": "string",                // 短信验证码
  "captchaVerification": "uuid",  // 图形验证码UUID
  "captchaCode": "string"         // 图形验证码内容
}

3.3 DID 登录(去中心化身份)

概述: DID登录是基于区块链的去中心化身份认证用户通过DID APP扫描二维码获取可验证凭证Verifiable Presentation完成身份认证。

关键概念:

  • DIDDecentralized Identifier:去中心化身份标识符
  • VPVerifiable Presentation:可验证展示,包含用户身份证明
  • VCVerifiable Credential:可验证凭证,由可信签发者签发

完整流程(时序图):

┌─────────┐         ┌─────────┐         ┌─────────┐         ┌─────────┐
│  用户   │         │ 前端    │         │ SSO服务  │         │DID服务   │
└────┬────┘         └────┬────┘         └────┬────┘         └────┬────┘
     │                   │                   │                   │
     │  1.点击登录        │                   │                   │
     │──────────────────>│                   │                   │
     │                   │                   │                   │
     │  2.请求二维码       │                   │                   │
     │                   │POST /did/pub/login/qrcode            │
     │                   │───────────────────────────────────────>
     │                   │                   │                   │
     │                   │                   │  3.生成reqId      │
     │                   │                   │  4.缓存状态=PAGE  │
     │                   │                   │                   │
     │                   │  5.返回{reqId, url}│                   │
     │                   │<───────────────────────────────────────│
     │                   │                   │                   │
     │  6.显示二维码       │                   │                   │
     │<───────────────────│                   │                   │
     │                   │                   │                   │
     │  7.扫描二维码       │                   │                   │
     │───────────────────────────────────────────────────────────│
     │                   │                   │                   │
     │                   │                   │  8.用户在DID APP  │
     │                   │                   │     确认授权      │
     │                   │                   │                   │
     │  9.VP返回          │                   │                   │
     │<──────────────────────────────────────────────────────────│
     │                   │                   │                   │
     │  10.提交VP验证     │                   │                   │
     │                   │POST /did/pub/getvp/login?reqId=xxx   │
     │                   │───────────────────────────────────────>
     │                   │                   │                   │
     │                   │                   │  11.验证VP         │
     │                   │                   │  12.更新状态=QR    │
     │                   │                   │                   │
     │                   │                   │  13.DID回调        │
     │                   │                   │POST /did/pub/callback/login
     │                   │                   │<──────────────────────────────────────────────────│
     │                   │                   │                   │
     │                   │                   │  14.验证VP签名     │
     │                   │                   │                   │
     │                   │                   │  15.提取身份信息   │
     │                   │                   │     (手机号/DID)   │
     │                   │                   │                   │
     │                   │                   │  16.查询/创建用户  │
     │                   │                   │     绑定DID       │
     │                   │                   │                   │
     │                   │                   │  17.更新状态=CALLBACK│
     │                   │                   │                   │
     │                   │  18.轮询结果       │                   │
     │                   │GET /did/pub/backresult/login?reqId=xxx│
     │                   │───────────────────────────────────────>
     │                   │                   │                   │
     │                   │                   │  19.检查状态      │
     │                   │                   │  20.返回用户信息  │
     │                   │  21.返回authLoginRespVO               │
     │                   │<───────────────────────────────────────│
     │                   │                   │                   │
     │  22.登录成功       │                   │                   │
     │<───────────────────│                   │                   │

DID登录状态机

状态 说明
PAGE 1 初始状态,等待扫码
QR 2 已扫码,等待授权
CALLBACK 3 已授权,流程完成
PHONE_BIND 4 需要手机号绑定(手机号为空时)
BUSI_FAILURE 5 认证失败
BUSI_SUCCESS 6 认证成功

伪代码:

// ========== DID Controller ==========

/**
 * 1. 获取登录二维码
 * 流程请求DID服务端获取二维码URL生成reqId并缓存状态
 */
@PostMapping("/pub/login/qrcode")
public CommonResult<QrCodeInfo> getLoginUrlQRCode() {
    // 1. 从系统参数获取DID登录VP请求地址
    String vpUrl = XtcsUtils.getXtcs("TXW_DID_LOGIN_VP_URL");
    
    // 2. 生成唯一请求标识
    String reqId = UUID.randomUUID().toString() + UUID.randomUUID().toString();
    
    // 3. 构建完整URL
    String qrCodeUrl = vpUrl + "?reqId=" + reqId;
    
    // 4. 缓存请求状态30分钟过期
    CacheUtils.cacheData("TXW:DID:REQ:ID:" + reqId, "1", 30*60*1000);
    
    // 5. 返回二维码信息
    return CommonResult.success(QrCodeInfo.builder()
        .reqId(reqId)
        .url(qrCodeUrl)
        .build());
}

/**
 * 2. 获取登录凭证VP
 * 流程验证状态调用DID SDK构建VP请求
 */
@GetMapping("/pub/getvp/login")
public String getLoginVP(@RequestParam String reqId) {
    // 1. 验证请求状态
    String status = CacheUtils.getCacheData("TXW:DID:REQ:ID:" + reqId);
    if (!"1".equals(status)) {
        return jsonError("请求状态不正确,请重新操作");
    }
    
    // 2. 更新状态为已扫码
    CacheUtils.cacheData("TXW:DID:REQ:ID:" + reqId, "2");
    
    // 3. 创建DID客户端
    IDidClient didClient = new DidClient(
        XtcsUtils.getXtcs("TXW_DID_CONSOLE_LOGIN_URL"),
        DidClient.version15
    );
    
    // 4. DID控制台登录
    Response<AccessToken> login = didClient.login(
        XtcsUtils.getXtcs("TXW_DID_CONSOLE_USERNAME"),
        XtcsUtils.getXtcs("TXW_DID_CONSOLE_PASSWORD")
    );
    
    // 5. 构建VP请求扩展信息
    LoginExtend extend = LoginExtend.builder()
        .requestId(reqId)
        .method("POST")
        .callbackUrl(callbackUrl + "?reqId=" + reqId)
        .authorizedName("碳信网")
        .authorizedNameEn("TanxinWeb")
        .build();
    
    // 6. 添加需要获取的凭证类型
    List<DidExtendVcsInfo> vcs = new ArrayList<>();
    vcs.add(DidExtendVcsInfo.builder()
        .vctId("100001")  // 个人身份凭证
        .vctVersion("v9")
        .issuer("did:cndid:cndid")
        .build());
    extend.setVcs(vcs);
    
    // 7. 构建可验证展示
    VerifiablePresentation<LoginExtend> loginvp = VerifiablePresentation.builder()
        .presentationUsage("DID_LOGIN_REQUEST")
        .proof(proofList)
        .extend(extend)
        .build();
    
    // 8. 生成并签名VP
    Response<VerifiablePresentation<LoginExtend>> res = 
        didClient.vpSign(loginvp, LoginExtend.class);
    
    // 9. 返回VP JSON
    return JsonUtils.toJson(res);
}

/**
 * 3. 登录回调处理
 * 流程DID服务端回调验证VP创建/绑定用户
 */
@PostMapping("/pub/callback/login")
public String getLoginVPCallback(@RequestParam String reqId, 
                                  @RequestBody Map<String, Object> vpRequestBody) {
    // 1. 解析VP
    VerifiablePresentation request = BeanUtils.toBean(vpRequestBody, 
        VerifiablePresentation.class);
    
    // 2. 验证VP签名
    String checkMsg = checkvp(request);
    if (checkMsg != null) {
        return jsonError(checkMsg);
    }
    
    // 3. 更新状态为已回调
    CacheUtils.cacheData("TXW:DID:REQ:ID:" + reqId, "3");
    
    // 4. 提取凭证信息
    List<VerifiableCredential> vcList = request.getVerifiableCredential();
    if (vcList == null || vcList.isEmpty()) {
        return jsonError("凭证为空");
    }
    
    VerifiableCredential vc = vcList.get(0);
    String holderDid = vc.getHolder();  // 获取DID标识
    
    // 5. 提取身份信息
    DidCredentialSubject cs = BeanUtils.toBean(vc.getCredentialSubject(), 
        DidCredentialSubject.class);
    String phone = cs.getPhone();  // 手机号
    String legalName = cs.getLegalName();  // 法人姓名
    String entName = cs.getEntname();  // 企业名称
    
    // 6. 根据不同条件查找/创建用户
    YhxxbDTO yhxx = null;
    if (StringUtils.isNotBlank(phone)) {
        // 6.1 根据手机号查找
        yhxx = yhxxService.getYhxxBySjhm(phone);
    } else if (idCard != null) {
        // 6.2 根据身份证查找
        yhxx = yhxxService.getYhxxBySfzjhm(idCard);
    } else {
        // 6.3 根据DID查找
        yhxx = yhxxService.getYhxxByDid(holderDid);
    }
    
    if (yhxx == null && StringUtils.isBlank(phone)) {
        // 6.4 手机号为空,需要后续绑定
        CacheUtils.cacheData("TXW:DID:USER:DID:" + reqId, holderDid, 30*60*1000);
        CacheUtils.cacheData("TXW:DID:USER:DID:Data:" + reqId, 
            JsonUtils.toJson(cs), 30*60*1000);
        CacheUtils.cacheData("TXW:DID:REQ:ID:" + reqId, "4");  // PHONE_BIND
        return jsonSuccess("需要手机绑定");
    }
    
    if (yhxx == null && StringUtils.isNotBlank(phone)) {
        // 6.5 新用户,创建用户
        yhxx = new YhxxbDTO();
        yhxx.setYhUuid(IdUtil.fastSimpleUUID());
        yhxx.setDid(holderDid);
        yhxx.setSjhm1(phone);
        yhxx.setDlzh(phone);
        yhxx.setZsxm1(legalName != null ? legalName : entName);
        yhxx = yhxxService.saveYhxxByDid(yhxx);
    } else {
        // 6.6 已有用户绑定DID
        yhxx.setDid(holderDid);
        yhxxService.updateDid(yhxx);
    }
    
    // 7. 创建企业信息DID企业认证
    if (StringUtils.isNotBlank(entName)) {
        YhxxbDTO qyxx = new YhxxbDTO();
        qyxx.setQymc(entName);
        qyxx.setNsrsbh(cs.getUniscid());  // 统一社会信用代码
        qyxx.setYhUuid(yhxx.getYhUuid());
        yhxxService.intQyxxByDid(qyxx);
    }
    
    // 8. 缓存用户信息
    CacheUtils.cacheData("TXW:DID:USER:Data:" + reqId, 
        JsonUtils.toJson(yhxx), 30*60*1000);
    
    return jsonSuccess("true");
}

/**
 * 4. 查询登录结果(轮询)
 * 流程前端轮询检查状态返回accessToken
 */
@GetMapping("/pub/backresult/login")
public CommonResult<R> getLoginCallbackResult(@RequestParam String reqId) {
    // 1. 获取状态
    String status = CacheUtils.getCacheData("TXW:DID:REQ:ID:" + reqId);
    
    if ("6".equals(status)) {  // 认证成功
        // 2. 获取用户信息
        String userJson = CacheUtils.getCacheData("TXW:DID:USER:Data:" + reqId);
        YhxxbDTO yhxx = JsonUtils.toBean(userJson, YhxxbDTO.class);
        
        // 3. 创建Token
        OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(
            yhxx.getYhUuid(), "default", null);
        
        // 4. 返回登录凭证
        AuthLoginRespVO respVO = new AuthLoginRespVO();
        respVO.setAccessToken(accessTokenDO.getAccessToken());
        respVO.setRefreshToken(accessTokenDO.getRefreshToken());
        respVO.setExpiresTime(accessTokenDO.getGqsj());
        
        // 5. 写入Cookie
        Cookie cookie = new Cookie("TXW_TOKEN", accessTokenDO.getAccessToken());
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        response.addCookie(cookie);
        
        return CommonResult.success(R.ok().put("status", status)
            .put("authLoginRespVO", respVO));
    } else if ("5".equals(status)) {  // 认证失败
        String msg = CacheUtils.getCacheData("TXW:DID:BUSI:FAILURE:MSG:" + reqId);
        return CommonResult.success(R.ok().put("status", status).put("msg", msg));
    }
    
    // 继续轮询
    return CommonResult.success(R.ok().put("status", status));
}

/**
 * 5. DID绑定手机号DID用户关联手机号
 */
@PostMapping("/auth/didBindPhone")
public CommonResult<AuthLoginRespVO> didBindPhone(@RequestBody DidBindPhoneReqVO reqVO) {
    // 1. 验证短信验证码
    String sms = reqVO.getSms();
    String phone = reqVO.getSjhm();
    if (!stringRedisTemplate.hasKey("sms_token:" + phone + sms)) {
        throw exception("验证码错误");
    }
    
    // 2. 获取缓存的DID信息
    String reqId = reqVO.getReqId();
    String holderDid = CacheUtils.getCacheData("TXW:DID:USER:DID:" + reqId);
    String csJson = CacheUtils.getCacheData("TXW:DID:USER:DID:Data:" + reqId);
    
    if (holderDid == null || csJson == null) {
        throw exception("缓存DID为空");
    }
    
    // 3. 创建用户
    DidCredentialSubject cs = JsonUtils.toBean(csJson, DidCredentialSubject.class);
    YhxxbDTO existYhxx = yhxxService.getYhxxBySjhm(phone);
    
    if (existYhxx != null) {
        throw exception("该手机号已注册");
    }
    
    YhxxbDTO yhxx = new YhxxbDTO();
    yhxx.setYhUuid(IdUtil.fastSimpleUUID());
    yhxx.setDid(holderDid);
    yhxx.setSjhm1(phone);
    yhxx.setDlzh(phone);
    yhxx.setZsxm1(cs.getLegalName() != null ? cs.getLegalName() : cs.getEntname());
    yhxx = yhxxService.saveYhxxByDid(yhxx);
    
    // 4. 创建企业信息
    YhxxbDTO qyxx = new YhxxbDTO();
    qyxx.setQymc(cs.getEntname());
    qyxx.setNsrsbh(cs.getUniscid());
    qyxx.setYhUuid(yhxx.getYhUuid());
    yhxxService.intQyxxByDid(qyxx);
    
    // 5. 创建Token并返回
    return createTokenAfterLoginSuccess(yhxx.getYhUuid());
}

DID SDK 调用伪代码:

// ========== DID SDK 使用示例 ==========

/**
 * DID客户端初始化
 */
IDidClient didClient = new DidClient(
    consoleUrl,           // DID控制台地址
    DidClient.version15   // API版本
);

/**
 * 1. 控制台登录
 */
Response<AccessToken> loginResponse = didClient.login(
    username,  // 用户名
    password   // 密码
);
// 返回: { code: 200000, data: { accessToken: "xxx", expiresIn: 3600 } }

/**
 * 2. 构建登录VP请求
 */
LoginExtend extend = LoginExtend.builder()
    .requestId(reqId)           // 请求唯一标识
    .method("POST")              // 回调方法
    .callbackUrl(callbackUrl)   // 回调地址
    .authorizedName("碳信网")    // 授权平台名称
    .authorizedNameEn("TanxinWeb")
    .build();

List<Proof> proofList = new ArrayList<>();

VerifiablePresentationBuilder<LoginExtend> builder = 
    VerifiablePresentation.builder();
builder.presentationUsage("DID_LOGIN_REQUEST");
builder.proof(proofList);
builder.extend(extend);

VerifiablePresentation<LoginExtend> loginvp = builder.build();

/**
 * 3. 生成并签名VP
 */
Response<VerifiablePresentation<LoginExtend>> signResponse = 
    didClient.vpSign(loginvp, LoginExtend.class);
// 返回签名后的VP JSON

/**
 * 4. 验证VP
 */
Response<Boolean> verifyResponse = didClient.vpVerify(vpJson);
// 返回: { code: 200000, data: true/false }

/**
 * 5. 提取凭证信息
 */
VerifiableCredential vc = vp.getVerifiableCredential().get(0);
String holderDid = vc.getHolder();  // DID标识符
DidCredentialSubject subject = vc.getCredentialSubject();
// subject.getPhone()      - 手机号
// subject.getLegalName()  - 法人姓名
// subject.getEntname()    - 企业名称
// subject.getUniscid()    - 统一社会信用代码

VP数据结构示例

{
  "@context": ["https://www.w3.org/2018/credentials/v1"],
  "type": ["VerifiablePresentation"],
  "holder": "did:cndid:xxxxxx",
  "verifiableCredential": [{
    "@context": ["https://www.w3.org/2018/credentials/v1"],
    "type": ["VerifiableCredential"],
    "credentialSubject": {
      "id": "did:cndid:xxxxxx",
      "phone": "13800138000",
      "legalName": "张三",
      "entname": "示例企业",
      "uniscid": "91110000xxxxxxxxxx"
    },
    "proof": {
      "type": "EcdsaSecp256k1Signature2019",
      "creator": "did:cndid:issuer",
      "created": "2025-01-01T00:00:00Z"
    }
  }],
  "proof": {
    "type": "EcdsaSecp256k1Signature2019",
    "created": "2025-01-01T00:00:00Z"
  }
}

前端轮询实现伪代码:

/**
 * DID登录轮询
 */
async pollLoginResult() {
    while (this.isPolling) {
        try {
            // 1. 查询登录结果
            const { data } = await rzbackresultlogin(this.reqId);
            
            // 2. 根据状态处理
            switch (data.status) {
                case 6:  // 认证成功
                    this.stopPolling();
                    await this.yhinit();  // 刷新用户信息
                    break;
                    
                case 5:  // 认证失败
                    this.stopPolling();
                    MessagePlugin.info({ content: data.msg });
                    break;
                    
                case 4:  // 需要手机绑定
                    // 显示手机绑定界面
                    this.showPhoneBind = true;
                    this.stopPolling();
                    break;
                    
                default:  // 继续轮询
                    // 2秒后继续
                    await new Promise(resolve => setTimeout(resolve, 2000));
                    break;
            }
        } catch (error) {
            console.error('查询失败', error);
            // 1.5秒后重试
            await new Promise(resolve => setTimeout(resolve, 1500));
        }
    }
}

/**
 * 生成二维码
 */
generateQRCode() {
    new QRCode(this.$refs.qrcodeElement, {
        text: this.qrcodeText,
        width: 200,
        height: 200,
        colorDark: '#000000',
        colorLight: '#ffffff',
        correctLevel: QRCode.CorrectLevel.H
    });
}

3.4 OAuth2 授权码模式

用户访问第三方应用
         │
         ▼
┌─────────────────────────────────────────────────┐
│  第三方应用重定向到授权页面                       │
│  GET /oauth2/authorize?clientId=xxx&redirectUri= │
└─────────────────────┬───────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────┐
│  用户在授权页面点击授权                          │
└─────────────────────┬───────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────┐
│  生成授权码                                      │
│  oauth2CodeService.createCode()                 │
│  → 存储到数据库(10分钟过期)                      │
│  → 重定向到第三方应用redirectUri?code=xxx        │
└─────────────────────────────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────┐
│  第三方应用用授权码换取令牌                       │
│  POST /oauth2/token/create                       │
│  → 验证clientId + clientSecret                   │
│  → 验证授权码                                    │
│  → 生成 accessToken + refreshToken               │
│  → 返回令牌                                      │
└─────────────────────────────────────────────────┘

四、Token 创建与刷新机制

4.1 创建访问令牌

// OAuth2TokenServiceImpl.createAccessToken()
1. 验证客户端配置 (clientId)
2. 创建 SessionInfo
3. 创建 refreshToken (如果非default客户端)
4. 创建 accessToken
5. 存储到数据库
6. 缓存到 Redis
7. 返回令牌信息

Redis Key 格式:

oauth2_access_token:{accessToken} → SessionInfo JSON

4.2 刷新令牌

用户使用 refreshToken 请求刷新
         │
         ▼
┌─────────────────────────────────────────────────┐
│  验证 refreshToken 是否有效                      │
│  → 检查是否过期                                 │
│  → 检查 clientId 是否匹配                        │
└─────────────────────┬───────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────┐
│  删除旧的 accessToken                           │
│  → 从数据库删除                                 │
│  → 从 Redis 删除                                │
└─────────────────────┬───────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────┐
│  生成新的 accessToken                           │
│  → 沿用原来的 refreshToken                      │
│  → 更新过期时间                                 │
└─────────────────────────────────────────────────┘

API 路径: PUT /oauth2/token/refresh?refreshToken=xxx&clientId=xxx


五、验证码服务

5.1 图形验证码

请求获取验证码
         │
         ▼
┌─────────────────────────────────────────────────┐
│  生成验证码 UUID + 图片                          │
│  verifyService.getCaptcha(remoteId)             │
│  → UUID 存入 Redis (5分钟过期)                   │
│  → 返回 { uuid, 图片Base64 }                    │
└─────────────────────────────────────────────────┘

校验验证码
         │
         ▼
┌─────────────────────────────────────────────────┐
│  verifyService.checkCaptcha(uuid, code)         │
│  → 从 Redis 获取 UUID 对应的验证码                │
│  → 比较用户输入与存储的验证码(忽略大小写)          │
│  → 验证后删除 Redis 记录                         │
└─────────────────────────────────────────────────┘

5.2 短信验证码

发送短信请求
         │
         ▼
┌─────────────────────────────────────────────────┐
│  校验图形验证码                                  │
└─────────────────────┬───────────────────────────┘
                      ▼
┌─────────────────────────────────────────────────┐
│  检查手机号是否被锁定                            │
│  Redis: sms_token:{phone} 存在则锁定             │
│  锁定中 → 返回"操作过于频繁"                     │
└─────────────────────┬───────────────────────────┘
                      │ 未锁定
                      ▼
┌─────────────────────────────────────────────────┐
│  发送短信                                        │
│  smService.sendCaptcha(phone, code)              │
│  → 调用外部短信网关(阿里云)                      │
│  → 验证码存入 Redis: sms_token:{phone+code}      │
│    有效期3分钟                                  │
└─────────────────────────────────────────────────┘

Redis Key 规则:

sms_token:{phone}         → 锁定标记(1分钟)
sms_token:{phone+code}   → 验证码(3分钟)

六、外部服务依赖

6.1 依赖关系图

                    ┌──────────────────┐
                    │   外部服务       │
                    └───────┬──────────┘
                            │
        ┌───────────────────┼───────────────────┐
        ▼                   ▼                   ▼
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  短信网关     │    │  DID服务     │    │  mhzc服务    │
│ (阿里云)      │    │ (ChainWeaver)│   │ (用户中心)   │
└──────────────┘    └──────────────┘    └──────────────┘
        │                   │                   │
        │                   │                   │
        ▼                   ▼                   ▼
┌──────────────────────────────────────────────────┐
│              txw-sso 服务                        │
│  ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │
│  │ AuthService │ │ DidController│ │YhxxService │ │
│  └─────────────┘ └─────────────┘ └────────────┘ │
└──────────────────────────────────────────────────┘

6.2 外部服务清单

服务 依赖方式 用途
短信网关(阿里云) REST API 发送短信验证码
DID服务 SDK (org.chainweaver.did.sdk) DID登录、企业实名认证
txw-mhzc Feign RPC 用户信息查询、用户注册

6.3 短信网关配置

// SMSConfig 配置
sms.host       短信API地址
sms.app        应用标识
sms.key        密钥(用于签名校验)
sms.template   短信模板(验证码内容格式)

签名校验算法:

vcode = MD5(MD5(timestamp + app + token + key))

6.4 DID 服务配置

配置项 说明
TXW_DID_LOGIN_VP_URL 登录凭证请求地址
TXW_DID_BUSI_LICE_VP_URL 企业实名凭证请求地址
TXW_DID_LOGIN_CALLBACK_URL 登录回调地址
TXW_DID_BUSI_LICE_CALLBACK_URL 企业实名回调地址
TXW_DID_CONSOLE_LOGIN_URL DID控制台登录地址
TXW_DID_CONSOLE_USERNAME DID控制台用户名
TXW_DID_CONSOLE_PASSWORD DID控制台密码
TXW_DID_CONSOLE_DIDCODE DID标识
TXW_DID_AUTH_NAME 授权平台名称
TXW_DID_AUTH_EN_NAME 授权平台英文名称

七、Redis 缓存设计

7.1 Key 规则汇总

Key Pattern 值类型 过期时间 用途
oauth2_access_token:%s SessionInfo JSON 动态(令牌过期时间) 访问令牌缓存
sms_token:%s phone 1分钟 手机号锁定
sms_token:%s%s phone 3分钟 短信验证码
TXW:DID:REQ:ID:%s status 30分钟 DID请求状态
TXW:DID:USER:DID:%s did 30分钟 DID用户绑定
TXW:DID:USER:DID:Data:%s DidCredentialSubject JSON 30分钟 DID用户数据
TXW:DID:USER:Data:%s YhxxbDTO JSON 30分钟 DID登录用户信息
TXW:DID:BUSI:FAILURE:MSG:%s msg 30分钟 DID认证失败消息

7.2 账户锁定 Key

Key Pattern 说明
account_lock:%s 账户锁定标记

锁定规则: 密码连续错误5次锁定30分钟


八、接口清单

8.1 认证接口 (/auth)

接口 方法 说明
/auth/login POST 账号密码登录
/auth/logout POST 登出
/auth/refresh-token POST 刷新令牌
/auth/sendMsg POST 发送短信验证码
/auth/loginBySMS POST 短信验证码登录
/auth/didBindPhone POST DID绑定手机号
/auth/changePassword POST 修改密码
/auth/resetPassword POST 重置密码(管理员)

8.2 OAuth2接口 (/oauth2)

接口 方法 说明
/oauth2/token/create POST 创建令牌
/oauth2/token/check GET 校验令牌
/oauth2/token/remove DELETE 删除令牌
/oauth2/token/refresh PUT 刷新令牌
/oauth2/authorize GET 授权码模式授权
/oauth2/authorize POST 授权确认

8.3 验证码接口 (/verify)

接口 方法 说明
/verify/get POST 获取滑动验证码
/verify/captcha POST 获取图形验证码
/verify/checkCaptcha POST 校验图形验证码

8.4 DID接口 (/did)

接口 方法 说明
/did/pub/login/qrcode POST 获取登录二维码
/did/pub/getvp/login GET 获取登录凭证
/did/pub/callback/login POST 登录回调
/did/pub/backresult/login GET 查询登录结果
/did/busilice/qrcode POST 获取认证二维码
/did/pub/getvp/busi GET 获取企业实名凭证
/did/pub/callback/busi POST 企业实名回调
/did/pub/backresult/busi GET 查询认证结果

九、安全机制

9.1 密码安全

  • 密码使用 MD5 存储(实际应为更安全的哈希算法)
  • 支持密码复杂度校验6-20位字母+数字)
  • 新旧密码不能相同

9.2 防暴力破解

  • 密码连续错误5次锁定账户30分钟
  • 短信验证码错误3次锁定手机号1分钟
  • 验证码一次性使用,验证后立即失效

9.3 Token安全

  • AccessToken 默认有效期2小时从客户端配置读取
  • RefreshToken 默认有效期30天
  • Token 支持客户端关联支持第三方平台SSO

9.4 Cookie安全

Cookie cookie = new Cookie(COOKIE_TOKEN_KEY, token);
cookie.setPath("/");
cookie.setHttpOnly(true);  // 防止XSS读取
// cookie.setMaxAge(-1);  // 浏览器会话级别

十、错误码

错误码 说明
AUTH_LOGIN_BAD_CREDENTIALS 用户名或密码错误
AUTH_LOGIN_CAPTCHA_CODE_ERROR 验证码错误
AUTH_LOGIN_PASSWORD_ERROR_LOCK 密码错误次数过多,账户已锁定
AUTH_PASSWORD_CONFIRM_MISMATCH 新密码与确认密码不一致
AUTH_PASSWORD_COMPLEXITY_INVALID 密码复杂度不符合要求
AUTH_PASSWORD_SAME_AS_OLD 新密码不能与旧密码相同
OAUTH2_SJHM_LOCK 手机号操作过于频繁
OAUTH2_SJHM_NOT_EXISTS 手机号未注册
OAUTH2_LOGIN_SMS_NOT_EXISTS 短信验证码不存在或已过期
OAUTH2_LOGIN_SJHM_NOT_EXISTS 短信登录手机号不存在

文档生成时间: 2026-05-03 基于 txw-sso 服务代码分析