4、认证与授权:OAuth2.0与OpenID Connect实战、JWT安全签发与验证、多因素认证(MFA)集成、RBAC/ABAC权限模型
好,咱们进入第四章。这一章的内容,说白了就是支付系统的「门禁系统」和「权限地图」。你想想看,一个支付系统每天要处理多少敏感操作?用户登录、商户提现、风控规则修改……每一个动作背后,都得有人知道「你是谁」「你能干什么」。
我这些年做支付安全,见过太多因为认证授权出问题的案例。有的系统JWT签名密钥硬编码在代码里,有的OAuth2流程里回调地址没校验,还有的权限模型直接用数据库里一个「is_admin」字段搞定……嗯,这些坑咱们今天一个一个填上。
核心要点:认证(Authentication)解决「你是谁」,授权(Authorization)解决「你能做什么」。两者缺一不可,而且必须分开设计。
4.1 OAuth2.0与OpenID Connect实战
先说说OAuth2.0。很多人把它和「登录」混为一谈,其实不对。OAuth2.0本质是授权协议,它解决的是「第三方应用如何获取用户资源的访问权限」。而OpenID Connect(OIDC)是在OAuth2.0之上加了一层身份认证层,告诉你「这个用户到底是谁」。
在支付系统里,我常用的场景是这样的:商户后台需要接入第三方数据分析工具,这时候商户管理员授权该工具读取交易数据。OAuth2.0的授权码模式(Authorization Code)是最安全的,也是我唯一推荐在生产环境使用的模式。
为什么?因为access token不会直接暴露在浏览器端。流程是这样的:
- 用户点击「授权」后,浏览器重定向到授权服务器
- 用户登录并确认授权
- 授权服务器返回一个临时的授权码(code)到回调地址
- 后端拿着这个code去换access token
- 后续请求用access token访问资源
这里有个关键点——回调地址必须严格校验。我曾经在项目中遇到过,攻击者伪造回调地址,把授权码发到了自己的服务器上。解决方案很简单:在注册应用时绑定白名单回调地址,服务端做精确匹配,不要用前缀匹配。
我的习惯:OAuth2.0的state参数一定要用。它是一个随机字符串,防止CSRF攻击。每次授权请求生成一个,回调时校验是否一致。别嫌麻烦,这个参数救过我的项目。
OpenID Connect的集成其实就是在OAuth2.0基础上多要一个ID Token。这个ID Token是JWT格式的,里面包含了用户的基本信息(sub、name、email等)。我建议用标准库解析,不要自己写JWT解析逻辑。
4.2 JWT安全签发与验证
JWT(JSON Web Token)现在几乎是微服务认证的标配。但说实话,很多人用JWT的方式让我捏一把汗。
先看一个典型的错误示例:
// 错误示例:使用HS256,密钥硬编码
String secret = "my-secret-key";
String token = Jwts.builder()
.setSubject(userId)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
这个代码有什么问题?问题大了去了。HS256是对称签名,意味着签发和验证用的是同一个密钥。如果密钥泄露,任何人都可以伪造token。更可怕的是,密钥直接写在代码里,git提交后全世界都看到了。
我推荐的做法是:
- 使用非对称算法(RS256或ES256):私钥签发,公钥验证。私钥只存在于认证服务,其他服务只持有公钥。
- 密钥定期轮换:我习惯每30天换一次密钥对,旧密钥保留一个宽限期(比如24小时),让还在有效期内的token能正常验证。
- 设置合理的过期时间:access token 15分钟,refresh token 7天。别学某些系统把token有效期设成一年。
警告:永远不要在JWT的payload里存放敏感信息!JWT的payload只是Base64编码,不是加密。任何人拿到token都可以解码看到里面的内容。密码、信用卡号这些东西,想都别想放进去。
JWT验证的完整流程,我一般这样写:
// 推荐做法:RS256,公钥验证
public Claims verifyToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(publicKey) // 公钥
.setAllowedClockSkewSeconds(60) // 允许60秒时钟偏差
.build()
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
// 记录日志,返回401
throw new AuthenticationException("Token无效或已过期");
}
}
这里有个细节:时钟偏差。不同服务器的系统时间可能有几秒差异,设置一个允许的偏差范围可以避免误判。我一般设60秒,够用了。
4.3 多因素认证(MFA)集成
单靠密码认证,在支付系统里是绝对不够的。密码可能被泄露、被撞库、被钓鱼。多因素认证(MFA)就是再加一层保险。
常见的MFA方式有三种:
| 方式 | 原理 | 安全性 | 用户体验 |
|---|---|---|---|
| TOTP(时间基一次性密码) | 基于共享密钥和时间戳生成6位数字 | 高 | 中等(需要安装认证器App) |
| SMS验证码 | 发送6位数字到手机 | 中(存在SIM卡交换攻击风险) | 高 |
| 硬件密钥(如YubiKey) | 基于FIDO2/WebAuthn协议 | 极高 | 低(需要携带硬件) |
我个人最推荐TOTP。为什么?因为它不依赖短信通道,没有额外的通信成本,而且安全性足够。实现起来也不复杂:
// TOTP生成示例(使用Google Authenticator兼容算法)
public String generateTOTP(String secretKey) {
long timeWindow = System.currentTimeMillis() / 30000; // 30秒窗口
byte[] data = ByteBuffer.allocate(8).putLong(timeWindow).array();
byte[] hash = HmacSHA256(secretKey, data);
int offset = hash[hash.length - 1] & 0xf;
int binary = ((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
int otp = binary % 1000000;
return String.format("%06d", otp);
}
避坑指南:我曾经遇到过一个问题——用户手机丢了,TOTP无法生成,账号登不进去。解决方案是提供备用恢复码(recovery codes),在用户首次绑定MFA时生成10个一次性恢复码,让用户保存好。每个恢复码只能用一次,用完作废。
集成MFA时,我建议采用「渐进式」策略:高风险操作(提现、修改安全设置)强制MFA,低风险操作(查看余额、交易记录)可以只验证密码。这样既保证了安全,又不至于让用户觉得太麻烦。
4.4 RBAC/ABAC权限模型
权限模型是授权系统的核心。RBAC(基于角色的访问控制)和ABAC(基于属性的访问控制)是两种主流方案。
RBAC 是最常用的。它的核心思想是:用户 -> 角色 -> 权限。一个用户可以有多个角色,一个角色可以有多个权限。比如「财务主管」角色可以审核提现,「普通操作员」角色只能查看交易记录。
在支付系统里,我一般这样设计RBAC:
-- 数据库表结构示例
CREATE TABLE roles (
id INT PRIMARY KEY,
name VARCHAR(50) UNIQUE, -- 'finance_manager', 'operator', 'admin'
description TEXT
);
CREATE TABLE permissions (
id INT PRIMARY KEY,
name VARCHAR(100) UNIQUE, -- 'withdraw:approve', 'transaction:view'
resource VARCHAR(50), -- 'withdraw', 'transaction'
action VARCHAR(50) -- 'approve', 'view'
);
CREATE TABLE role_permissions (
role_id INT REFERENCES roles(id),
permission_id INT REFERENCES permissions(id),
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE user_roles (
user_id INT REFERENCES users(id),
role_id INT REFERENCES roles(id),
PRIMARY KEY (user_id, role_id)
);
但RBAC有个问题——它不够灵活。比如你想让某个用户只能查看「自己部门」的交易数据,RBAC就做不到了。这时候需要ABAC。
ABAC 基于属性做判断。属性可以是用户属性(部门、职级)、资源属性(交易金额、交易时间)、环境属性(IP地址、设备类型)。判断逻辑用策略引擎(如Casbin、OPA)来执行。
举个例子:
// ABAC策略示例(伪代码)
policy "高额提现审批" {
// 只有财务经理可以审批超过10万的提现
effect = "allow"
condition = [
user.role == "finance_manager",
resource.type == "withdraw",
resource.amount > 100000,
resource.currency == "CNY"
]
}
在实际项目中,我通常采用混合模式:用RBAC做基础权限控制,用ABAC做精细化策略。比如「普通操作员」角色默认只能查看交易,但如果交易金额超过50万,需要额外有「大额交易查看」权限(ABAC动态判断)。
我的经验:权限模型不要一开始就设计得太复杂。先上RBAC,等业务方提出「为什么张三能看到李四的数据」这种问题时,再逐步引入ABAC。过度设计是权限系统最大的敌人。
知识体系总览
下面这张图展示了本章的核心逻辑关系:
这张图把认证和授权分成了三层。最上层是认证,解决「你是谁」;中间是授权,解决「你能做什么」;最下层是安全控制,执行最终的访问决策。三者环环相扣,缺一不可。
好了,这一章的内容就到这里。认证与授权是支付系统的基石,设计得好,后面的安全防护会轻松很多。记住:不要偷懒,不要硬编码,不要相信用户输入。嗯,这些原则在后面的章节还会反复出现。