43 KiB
43 KiB
碳信网 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_client(OAuth2客户端表)
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),完成身份认证。
关键概念:
- 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 | 认证成功 |
伪代码:
// ========== 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 服务代码分析