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

1040 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 碳信网 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_clientOAuth2客户端表
```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完成身份认证。
**关键概念:**
- **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 | 认证成功 |
**伪代码:**
```java
// ========== 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 调用伪代码:**
```java
// ========== 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数据结构示例**
```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 服务代码分析*