# 碳信网 SSO 认证模块详解 > 本文档基于 txw-sso 服务代码分析,详细说明注册登录流程、表结构定义、外部服务依赖。 --- ## 一、模块定位 txw-sso 是碳信网项目的**统一认证服务**,负责: - 用户账号密码登录/短信验证码登录 - OAuth2.0 令牌管理 - DID(去中心化身份)登录集成 - 图形验证码/短信验证码服务 - 账户锁定(防暴力破解) --- ## 二、核心表结构 ### 2.1 OAuth2 令牌相关表 #### txw_sso_access_token(访问令牌表) ```sql 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(刷新令牌表) ```sql 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(授权码表) ```sql 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_client(OAuth2客户端表) ```sql 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` **请求参数:** ```json { "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` **请求参数:** ```json { "sjhm": "string", // 手机号 "sms": "string", // 短信验证码 "captchaVerification": "uuid", // 图形验证码UUID "captchaCode": "string" // 图形验证码内容 } ``` ### 3.3 DID 登录(去中心化身份) **概述:** DID登录是基于区块链的去中心化身份认证,用户通过DID APP扫描二维码获取可验证凭证(Verifiable Presentation),完成身份认证。 **关键概念:** - **DID(Decentralized Identifier)**:去中心化身份标识符 - **VP(Verifiable Presentation)**:可验证展示,包含用户身份证明 - **VC(Verifiable 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 | 认证成功 | **伪代码:** ```java // ========== DID Controller ========== /** * 1. 获取登录二维码 * 流程:请求DID服务端获取二维码URL,生成reqId并缓存状态 */ @PostMapping("/pub/login/qrcode") public CommonResult 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 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 vcs = new ArrayList<>(); vcs.add(DidExtendVcsInfo.builder() .vctId("100001") // 个人身份凭证 .vctVersion("v9") .issuer("did:cndid:cndid") .build()); extend.setVcs(vcs); // 7. 构建可验证展示 VerifiablePresentation loginvp = VerifiablePresentation.builder() .presentationUsage("DID_LOGIN_REQUEST") .proof(proofList) .extend(extend) .build(); // 8. 生成并签名VP Response> 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 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 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 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 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 调用伪代码:** ```java // ========== DID SDK 使用示例 ========== /** * DID客户端初始化 */ IDidClient didClient = new DidClient( consoleUrl, // DID控制台地址 DidClient.version15 // API版本 ); /** * 1. 控制台登录 */ Response 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 proofList = new ArrayList<>(); VerifiablePresentationBuilder builder = VerifiablePresentation.builder(); builder.presentationUsage("DID_LOGIN_REQUEST"); builder.proof(proofList); builder.extend(extend); VerifiablePresentation loginvp = builder.build(); /** * 3. 生成并签名VP */ Response> signResponse = didClient.vpSign(loginvp, LoginExtend.class); // 返回签名后的VP JSON /** * 4. 验证VP */ Response 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数据结构示例:** ```json { "@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" } } ``` **前端轮询实现伪代码:** ```javascript /** * 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 创建访问令牌 ```java // 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 短信网关配置 ```java // SMSConfig 配置 sms.host → 短信API地址 sms.app → 应用标识 sms.key → 密钥(用于签名校验) sms.template → 短信模板(验证码内容格式) ``` **签名校验算法:** ```java 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安全 ```java 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 服务代码分析*