Spring Boot 3 微服务认证中心:6 种登录模式策略分发 + 国密生物识别 + 防篡改交付基线
第一章:项目背景与痛点
我正在负责的是一款居家健康监测 SaaS 平台,面向慢病管理场景。平台的用户角色非常多样:
1:医护人员 / 健康助理
通过 Web 后台查看患者健康数据、生成健康报告、管理随访记录
2:患者(老人):
通过 App 端每日上报血压、血糖、血氧等生命体征
3:患者家属:
. 通过 App 端远程查看家人的健康数据
多端、多角色的业务形态,决定了登录需求天然多样化:Web 后台用账号密码登录、App 端老人不便输入密码时用人脸识别登录、年轻人用指纹快速解锁、忘记密码时走短信验证码流程,
多端、多角色的业务形态,决定了登录需求天然多样化:Web 后台用账号密码登录、App 端老人不便输入密码时用人脸识别登录、年轻人用指纹快速解锁、忘记密码时走短信验证码流程
痛点一:登录方式多样,扩展难维护
最传统的实现方式是堆 if-else:
if ("password".equals(grantType)) { /* 密码登录 */ }
else if ("sms".equals(grantType)) { /* 短信登录 */ }
else if ("face".equals(grantType)) { /* 人脸识别 */ }
// ... 每加一种登录方式,改一次
痛点二:交付后的责任归属说不清
项目交付后,线上版本是否原厂交付、是否被第三方修改过,常常难以追溯。因此需要一套"原厂证据链",出问题能一验就知道版本归属。
解决思路
针对这两个痛点,我在项目中做了两件事:
认证层面:用策略模式统一多种登录方式的分发,新增登录方式只需 +1 个类,原有代码零改动
交付层面:用 git 构建指纹 + SHA256 哈希清单 + 数字签名,构建一套"原厂证据链"(补充数字签名可由个人选择,这样当出现公司拿本人的账号生成的也不是代表本人操作),出问题能一验就知道锅该谁背
第二章:认证中心整体架构
独立微服务设计
认证中心作为独立的微服务 xxxx-auth,不依赖任何业务服务,只通过 Feign 调用用户服务(xxxx-user-api)获取账号信息。这样设计的好处:
认证服务可以独立部署、独立扩缩容
业务服务不需要感知登录细节,只信任网关传来的 JWT
认证逻辑的变更不会影响业务服务
整体架构如下:
用户(Web/App)
↓
Spring Cloud Gateway(端口 80)
↓ JWT 鉴权
yining-auth(认证中心) ←→ Redis(验证码/token黑名单)
↓ Feign
yining-system(用户服务)
技术选型:
| 组件 | 选型 | 用途 |
| 整体框架 | stringboot3.2 | 基座框架 |
| JWT库 | jjwt 0.11.2 | 令牌生成与解析 |
| 缓存 | Redis | 验证码、token 黑名单、登录失败计数 |
| 短信服务 | 某短信服务 | 短信验证码 |
| 国密算法 | SM2 | 人脸识别/指纹的生物特征签名验证 |
| 对称加密 | AES | 手机号脱敏传输 |
| RSA 加密 | RSA | 密码前端加密传输 |
| 图形验证码 | 图形验证码组件 | 防机器人 |
| 对象存储 | 对象存储组件(列如:MinIo等等) | 文件上传/下载/分片/断点续传/元数据管理 |
| 服务发现 | 服务发现组件 | 服务注册 + 配置中心 |
认证中心模块结构:
xxxxx-auth/src/main/java/org/xxx/auth/
├── granter/ # 核心:6 种登录授权器(策略模式)
├── controller/ # 接口层:AuthController(登录/登出/验证码)
├── config/ # 配置:RSA 公钥、验证码、某云短信
├── utils/ # 工具:TokenUtil、LoginUtil、ValidateCodeUtils
└── properties/ # 配置属性类
第三章:核心设计 — 策略模式分发登录
统一接口:ITokenGranter
所有登录方式都实现同一个接口,对外暴露唯一的 grant 方法:
// xxxx-auth/src/main/java/org/xxxxx/auth/granter/ITokenGranter.java
public interface ITokenGranter {
/**
* 获取用户信息
*
* @param tokenParameter 授权参数
* @return UserInfo
*/
UserInfo grant(TokenParameter tokenParameter) throws Exception;
}
不管密码登录、短信登录还是人脸识别,对上层都是"传入参数,返回用户信息"。上层完全不需要知道底层认证逻辑的差异。
策略池:TokenGranterBuilder
用一个 ConcurrentHashMap 作为策略池,静态初始化块注册所有登录方式:
// xxxx-auth/src/main/java/org/xxxxxx/auth/granter/TokenGranterBuilder.java
@AllArgsConstructor
public class TokenGranterBuilder {
/**
* TokenGranter缓存池
*/
private static final Map<String, ITokenGranter> GRANTER_POOL = new ConcurrentHashMap<>();
static {
GRANTER_POOL.put(PasswordTokenGranter.GRANT_TYPE, SpringUtil.getBean(PasswordTokenGranter.class));
GRANTER_POOL.put(SmsCodeTokenGranter.GRANT_TYPE, SpringUtil.getBean(SmsCodeTokenGranter.class));
GRANTER_POOL.put(RefreshTokenGranter.GRANT_TYPE, SpringUtil.getBean(RefreshTokenGranter.class));
GRANTER_POOL.put(FaceRecogTokenGranter.GRANT_TYPE, SpringUtil.getBean(FaceRecogTokenGranter.class));
GRANTER_POOL.put(FingerprintTokenGranter.GRANT_TYPE, SpringUtil.getBean(FingerprintTokenGranter.class));
}
/**
* 获取TokenGranter
*/
public static ITokenGranter getGranter(String grantType) {
ITokenGranter tokenGranter = GRANTER_POOL.get(
Func.toStr(grantType, PasswordTokenGranter.GRANT_TYPE));
if (tokenGranter == null) {
throw new SecureException(ExceptionConstant.NOT_FOUND_GRANTTYPE);
}
return tokenGranter;
}
}
每个 Grantor 都有一个 GRANT_TYPE 常量(如 "password"、"face_recog"、"fingerprint"),作为 Map 的 key。
分发入口:AuthController.login
// xxxx-auth/src/main/java/org/xxxxx/auth/controller/AuthController.java
@PostMapping("login")
public R<AuthInfo> login(@RequestParam(defaultValue = "password") String grant_type,
@RequestParam(required = false) String account,
@RequestParam(required = false) String password,
@RequestParam(required = false) String sms_code,
@RequestParam(required = false) String phone_device_id) throws Exception {
String userType = Func.toStr(WebUtil.getRequest().getHeader(TokenUtil.USER_TYPE_HEADER_KEY),
CommonConstant.DEFAULT_USER_TYPE);
TokenParameter tokenParameter = new TokenParameter();
tokenParameter.getArgs()
.set("account", account)
.set("smsCode", sms_code)
.set("password", password)
.set("userType", userType)
.set("grantType", grant_type)
.set("phoneDeviceId", phone_device_id);
// 查看账号是否在黑名单中
if (redisUtil.hasKey(CacheNames.ACCOUNT_LOGIN_BLACK + account)) {
Long remainTime = redisUtil.getExpire(CacheNames.ACCOUNT_LOGIN_BLACK + account) / 60;
return R.fail("账号失败次数已超过5次,请" + remainTime + "分钟后再试");
}
// ★ 核心分发:根据 grant_type 拿到对应的 Grantor
ITokenGranter granter = TokenGranterBuilder.getGranter(grant_type);
UserInfo userInfo = granter.grant(tokenParameter);
// 后续:根据 userType(web/app)走不同的 token 生成逻辑
// ...
}
整个流程 :
前端传 grant_type 参数("password" / "sms" / "face_recog" / "fingerprint" / "refresh_token")
先查账号黑名单(登录失败 5 次会被锁)
一行代码分发:TokenGranterBuilder.getGranter(grant_type) 拿到对应 Grantor
调用 grant(tokenParameter) 拿到用户信息
后续根据 userType 走 Web 登录或 App 登录的 token 生成逻辑
开闭原则的体现:
新增一种登录方式,只需要做两步:
新建一个类实现 ITokenGranter 接口
在 TokenGranterBuilder 的静态块里注册
原有的 AuthController、其他 Grantor 完全不用改。这正是开闭原则(对扩展开放,对修改关闭)的典型应用。
项目中预留的 SocialTokenGranter(社交登录)就是例子——类已经定义,只是 Builder 注册那行被注释了,未来要启用只需取消注释
第四章:交付完整性基线 — 防篡改 + 防甩锅
1.痛点:版本归属难以追溯
实际项目中,交付后的版本归属常常难以追溯——线上运行的版本是否原厂交付、是否被第三方修改过,往往说不清。因此需要一套"原厂证据链",出问题能一验就知道版本归属。
2.构建期:Git 指纹注入
在根 pom.xml 里加 git-commit-id-plugin 插件,构建期把 git 状态打进每个 jar 的 git.properties
<!-- 根 pom.xml -->
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>4.9.10</version>
<executions>
<execution>
<id>get-the-git-infos</id>
<goals><goal>revision</goal></goals>
<phase>initialize</phase>
</execution>
</executions>
<configuration>
<skipPoms>true</skipPoms>
<includeOnlyProperties>
<includeOnlyProperty>^git.branch$</includeOnlyProperty>
<includeOnlyProperty>^git.build.time$</includeOnlyProperty>
<includeOnlyProperty>^git.commit.id.abbrev$</includeOnlyProperty>
<includeOnlyProperty>^git.dirty$</includeOnlyProperty>
</includeOnlyProperties>
</configuration>
</plugin>
构建完成后,每个 jar 的 target/classes/git.properties 内容如下:
#Generated by Git-Commit-Id-Plugin
git.branch=feature/t0
git.build.time=2026-08-24T16\:56\:49+0800
git.commit.id.abbrev=22c3707
git.dirty=true
作用:jar 内任何字节变化(反编译改 class、改配置、加内容后重打)都会导致哈希变化,git.properties 能追溯到具体 commit。
局限性:git.properties 只做版本追溯,防不了 git 账号被冒用——公司申请的 git 账号离职后可被冒充提交/构建,commit author 仍指向原负责人(建议项目负责人补充数字签名,以增强基线的不可伪造性。)。
3.打包期:SHA256 哈希清单
在 deploy/xxx_package.sh(Windows 对应 xxxx_package.bat)末尾加一步:遍历 6 个模块 jar,算 SHA-256 写入 integrity-manifest-{profile}.txt:
# deploy/xxxx_package.sh
function xxxx_generate_integrity_manifest() {
cd "${project_directory}/target"
manifest="integrity-manifest-${profile}.txt"
: > "${manifest}"
if command -v sha256sum >/dev/null 2>&1; then
hash_cmd="sha256sum"
else
hash_cmd="shasum -a 256"
fi
for jar_dir in xxx-auth xxx-gateway xxx-i18n xxx-log xxx-ossfile xxx-system; do
jar_file="${jar_dir}/${jar_dir}.jar"
if [ -f "${jar_file}" ]; then
${hash_cmd} "${jar_file}" >> "${manifest}"
fi
done
echo "完整性清单已生成: ${project_directory}/target/${manifest}"
}
生成的 integrity-manifest-prod.txt 内容示例:
73A91194D5A311A4872C74EAA86CFA7579A3FACBC5E1F86496E9F3B1C687F2E3 target\xxx-auth\xxx-auth.jar
5908388441EF2659CE9B06145234C7C376A674EA3098593A99F88631C9373E7D target\xxx-gateway\xxx-gateway.jar
17834376461A25EADB834BBFBA23676B61DED01F6DDAAA65E8F0E68766D61871 target\xxx-i18n\xxx-i18n.jar
B67F06C8642DE504E24DA6F9F715FA7B79CA947F4934503BA89177BE3AF4C861 target\xxx-log\xxx-log.jar
E5414D1ED49065212F72491CB56CB5FFDFADB22BA256E706010DA8B3F01BDE21 target\xxx-ossfile\xxx-ossfile.jar
26FFDE5DE88143F89BCA37D92A07F6E1C253ED7C97DDF0FE6C859F1C1926E7D5 target\xxx-system\xxx-system.jar
4.签名:私钥保护基线真实性
希清单只解决了"jar 字节是否被改",但防不了基线本身被伪造——如果接手方用你的 git 账号重新打包,生成一份新的 manifest,哈希也能对上。
更隐蔽的风险:git 账号是公司申请的,离职后别人可以用你的账号提交/构建,commit author 仍指向你。
解决方案:用私钥数字签名保护基线。
用 openssl 生成 RSA 密钥对(只做一次):
# 生成私钥(4096 位 RSA)
openssl genrsa -out integrity_private.key 4096
# 从私钥导出公钥
openssl rsa -in integrity_private.key -pubout -out integrity_public.key
打包后用私钥签名 manifest:
openssl dgst -sha256 -sign integrity_private.key \
-out integrity-manifest-prod.txt.sig \
integrity-manifest-prod.txt
校验时用公钥验签(任何人能做,公钥公开):
openssl dgst -sha256 -verify integrity_public.key \
-signature integrity-manifest-prod.txt.sig \
integrity-manifest-prod.txt
预期输出(签名有效):Verified OK;预期输出(签名无效/伪造):Verification Failure。
关键:私钥离线保存(U盘/个人加密盘),绝不提交 git、不放公司可碰的位置。脚本被拿走没用,没有私钥签不出东西。
5.校验:外部可信方本地运行
校验脚本建议部署到服务器
如果 integrity_check.sh + 基线文件 + diff 日志都放在服务器上,接手方只要想甩锅,就能:
改完 jar 后跑一遍 baseline 子命令重生成基线覆盖 → 校验永远"一致"
或直接改校验脚本让它恒返回 PASS
或删掉 integrity_diff.log 销毁证据
校验必须从"外部可信方"(你的本地电脑)发起,服务器只被远程读取哈希,不留任何痕迹。
校验脚本通过 ssh 远程执行 docker exec sha256sum /app.jar 取线上 jar 哈希,拉回本地跟基线比对:
# 本地执行(不部署服务器)
./integrity_check.sh prod user@服务器IP
预期输出:
=== 交付完整性校验(本地外部可信方) ... ===
[1/2] 验证基线签名...
PASS 基线签名有效(原厂基线,未被伪造)
[2/2] 远程校验容器内 jar...
基线哈希(前16位) | 线上哈希(前16位) | 容器 | 结果
--------------------------|--------------------------|---------------|----------
73A91194D5A311A4... | 73A91194D5A311A4... | xxx-auth | PASS
5908388441EF2659... | 5908388441EF2659... | xxx-gateway| PASS
...
结论: 全部一致,jar 未被篡改
6.真实价值:防甩锅、防篡改证据链
这套不是"防篡改监控",是"防甩锅、防篡改证据链"——出问题被扣锅时,你手里攥着"原厂签名基线 + 构建指纹",一比对就知道锅该谁背,是否是被客户找第三方改动过
场景一:交付后冒出 10 个 bug 怪原负责人。跑校验,若线上哈希 ≠ 基线 → jar 被接手的人改过,新 bug 是他们改的,不是你的锅。
更多推荐
所有评论(0)