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不会直接暴露在浏览器端。流程是这样的:

  1. 用户点击「授权」后,浏览器重定向到授权服务器
  2. 用户登录并确认授权
  3. 授权服务器返回一个临时的授权码(code)到回调地址
  4. 后端拿着这个code去换access token
  5. 后续请求用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提交后全世界都看到了。

我推荐的做法是:

  1. 使用非对称算法(RS256或ES256):私钥签发,公钥验证。私钥只存在于认证服务,其他服务只持有公钥。
  2. 密钥定期轮换:我习惯每30天换一次密钥对,旧密钥保留一个宽限期(比如24小时),让还在有效期内的token能正常验证。
  3. 设置合理的过期时间: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。过度设计是权限系统最大的敌人。

知识体系总览

下面这张图展示了本章的核心逻辑关系:

支付系统认证与授权知识体系 认证层(Authentication) OAuth2.0 + OIDC JWT签发与验证 多因素认证(MFA) 授权层(Authorization) RBAC(基于角色) ABAC(基于属性) 安全控制层(访问决策) 身份确认后进入授权

这张图把认证和授权分成了三层。最上层是认证,解决「你是谁」;中间是授权,解决「你能做什么」;最下层是安全控制,执行最终的访问决策。三者环环相扣,缺一不可。

好了,这一章的内容就到这里。认证与授权是支付系统的基石,设计得好,后面的安全防护会轻松很多。记住:不要偷懒,不要硬编码,不要相信用户输入。嗯,这些原则在后面的章节还会反复出现。

专注资料整理