权限控制

1.介绍

权限控制(AC)即 Access Control,一句话介绍:「谁(Who)可以对什么资源(What)进行什么操作(How)」

权限控制属于系统设计的一部分,常见于几乎所有 ToB 的系统内。尤其是一些传统的层级很多的企业内,权限控制非常重要

1.1.基础概念:

  • 什么是资源:资源是一个非常宽泛的概念,任何需要控制的实体都可以定义为资源,如:页面、链接、接口、数据
  • 操作有什么:读写,或者更详细一些:增删改查
  • 什么是权限:对某种资源的某个操作,称为权限

1.2.常见的权限模型:

  1. 基于角色的访问控制(RBAC,Role-Based Access control),最常用的模型。
  2. 基于属性的访问控制(ABAC,Attribute Base Access Control)
  3. 访问控制列表(ACL,Access Control List)
  4. 自主访问控制(DAC)
  5. 强制访问控制(MAC)
  6. 基于任务和工作流的访问控制(TBAC)
  7. 基于任务和角色的访问控制(T-RBAC)
  8. 基于对象的访问控制(OBAC)
  9. 使用控制模型(UCON)

1.3.开源项目

权限控制,灵活多变,并不存在灵活通用任意场景的开源项目。所有几乎所有的公司都是自己开发或者使用开源的OA系统。

2.RBAC-基于角色的访问控制

RBAC是一套成熟的权限模型。在传统权限模型中,我们直接把权限赋予用户。而在RBAC中,增加了"角色"的概念,我们首先把权限赋予角色,再把角色赋予用户。 这样,由于增加了角色,授权会更加灵活方便。在RBAC中,根据权限的复杂程度,又可分为RBAC0、RBAC1、RBAC2、RBAC3。 其中,RBAC0是基础,RBAC1、RBAC2、RBAC3都是以RBAC0为基础的升级。我们可以根据自家产品权限的复杂程度,选取适合的权限模型。

基础概念:

  • User,用户
  • Role,角色
  • Permission,权限
  • User to Role,用户-角色之间的映射关系
  • Role to Permission,角色-权限之间的映射关系

2.1.RBAC0【基本模型】

RBAC0是基础,很多产品只需基于RBAC0就可以搭建权限模型了。在这个模型中,我们把权限赋予角色,再把角色赋予用户。用户和角色,角色和权限都是多对多的关系。 用户拥有的权限等于他所有的角色持有权限之和。

2.2.RBAC1【角色分层模型】

RBAC1建立在RBAC0基础之上,在角色中引入了继承的概念。 简单理解就是,给角色可以分成几个等级,每个等级权限不同,从而实现更细粒度的权限管理。

2.3.RBAC2【角色限制模型】

RBAC2同样建立在RBAC0基础之上,仅是对用户、角色和权限三者之间增加了一些限制。 这些限制可以分成两类。

静态职责分离 SSD(Static Separation of Duty):

  • 互斥角色限制:角色间存在互斥关系,用户只能选择其中一个角色;
  • 基数限制:一个用户拥有的角色数量是有上限的,一个角色对应的权限数量也是有上限的;
  • 先决条件限制:角色之间存在前置依赖关系,用户必须先获得低级角色才能拥有高级角色;

动态职责分离 DSD(Dynamic Separation of Duty):

  • 如果用户拥有两个及以上角色,则同一时间段内有且只有一个角色可以生效

限制条件有些抽象,举几个例子

  • 出纳和会计就是两个互斥的角色,不能由同一个人同时担任(互斥角色一般是两者权限存在串通或者监督关系)
  • 一些体系内角色有高级和低级区别,比如专员和经理

2.4.RBAC3【统一模型】

也是最完善的模型:RBAC3 = RBAC1 + RBAC2

2.5.用户组【基于RBAC的延展】

我们可以把一个部门看成一个用户组,如销售部,财务部,再给这个部门直接赋予角色,使部门拥有部门权限,这样这个部门的所有用户都有了部门权限。 用户组概念可以更方便的给群体用户授权,且不影响用户本来就拥有的角色权限。

本质是,在用户和角色之间增加一层对于关系-用户组。用户组可以是部门、岗位、职级。

  • 优势:只需要维护用户组和角色的关系,用户组的数量少,便于维护
  • 缺点:不够灵活,无法针对某个人单独分配权限

3.ABAC-基于属性的访问控制

ABAC解释

传统的 RBAC 与 ACL 等访问控制机制中,可以认为是 ABAC 的子集,对于 RBAC,只是我们的访问机制的实现只是基于属性 role 而已,ACL 则是基于属性是 identity 的 AC。

属性包括很多,比如 岗位(研发、产品、管理、运营等),职级(P1、P2、P3、P4等),公司(总公司、子公司、分公司、外包等)

本质上与RBAC的用户组概念是一样的,只是概念更加抽象。

基础概念:

  • attribute,属性。可以是角色、岗位、职级等任意的 kv结构数据
  • subject,科目,一般指的是用户。
  • object,对象。一般指的是需要管理的资源。比如,某个系统的某个菜单的功能
  • operation,操作。表示subject可以的操作。比如,读、写、修改、下载等
  • policy,策略。对operation进行更细维度的判断,可以理解为数据权限。
  • environment conditions,语境 或者 上下文条件。比如当前时间是否运行操作、数据状态是否运行操作。

4.ACL-访问控制列表

目前在 Linux/Unix 文件系统中使用,也是使用最广泛的权限控制方式。 是一种粗粒度的文件系统权限控制模式。

4.1.Linux的控制方式

一个文件拥有三种用户关系,所有者、所属组和其他,这也意味着这个文件的权限只有三种,当我们想给一个特殊用户设置第四种权限时,就可以使用acl权限去设置。

acl主要分为3个方面可以控制。

1.权限模式(Scheme)

  • IP:从 IP 地址粒度进行权限控制
  • Digest:最常用,用类似于 username:password 的权限标识来进行权限配置, 便于区分不同应用来进行权限控制
  • World:最开放的权限控制方式,是一种特殊的 digest 模式,只有一个权限标识"world:anyone"
  • Super:超级用户

2.授权对象

  • 授权对象指的是权限赋予的用户或一个指定实体,例如 IP 地址或是机器灯。

3.权限 Permission

  • CREATE:数据节点创建权限,允许授权对象在该 Znode 下创建子节点
  • DELETE:子节点删除权限,允许授权对象删除该数据节点的子节点
  • READ:数据节点的读取权限, 允许授权对象访问该数据节点并读取其数据内容或子节点列表等
  • WRITE:数据节点更新权限,允许授权对象对该数据节点进行更新操作
  • ADMIN:数据节点管理权限,允许授权对象对该数据节点进行 ACL 相关设置操作

4.2.云产品

访问管理(Cloud Access Management,CAM),是各类云厂商使用的权限管理方式。

以腾讯云为例:

支持CAM的业务接口-概览

可以通过访问管理(CAM)创建、管理和销毁用户(组),操作不同的资源的不同的权限。

5.身份认证

5.1.单点登录SSO

单点登录(SSO)是一种认证机制,使得用户只需要一次登录,就可以访问多个相互信任的系统或应用服务。

技术选型方案

方案 技术框架 特点 适用场景
Cookie + Session Spring Session 轻量、本地适用 子系统同域部署,中小型系统,不要求复杂授权逻辑
OAuth2 / OIDC Spring Authorization Server、Keycloak、Auth0 标准协议、最通用 互联网企业/SaaS统一身份认证,多语言、多端系统对接
JWT + 网关鉴权 Spring Security + JWT + Gateway 微服务适配、高性能 微服务架构,高并发、无状态服务集群
CAS(Central Authentication Service) Apereo CAS 老牌稳定、学校/政府常用 政企内网 / 传统系统

实现原理

  • Cookie + Session:多个子系统共享统一的 Session 存储(如 Redis)。
  • OAuth2 / OIDC:使用授权服务器颁发AccessToken/IDToken,使用标准协议完成认证授权。
  • JWT + Gateway:用户登录后,颁发JWT Token,网关统一验证 Token,有效期/权限解析在 JWT 中完成。
  • CAS 单点登录: CAS 作为统一登录入口,所有子系统重定向到 CAS Server 认证。登录成功后颁发 Ticket,子系统校验后建立本地会话

当前主流: OAuth2/OIDC 或 JWT + 网关 的组合,既标准又灵活。

分布式认证鉴权最佳实践 架构方案: 在分布式/微服务架构中,登录认证通常由一个 独立的登录认证服务集中处理,避免各服务重复实现登录逻辑。

  1. 网关层鉴权:网关鉴权是将所有认证鉴权逻辑集中在API网关层处理,下游微服务无需关心认证鉴权问题,但是需要将各个微服务的权限信息都统一配置在网关中。
    • 优点:网关层集中处理,简化微服务逻辑。适合小型团队快速开发迭代、对安全的要求不高的项目中使用。
  2. 各微服务自行鉴权:主要是把Token验签和权限识别都放在各个微服务之中,网关只做网络转发。
  3. 网关+微服务的混合式鉴权。
    • 处理流程:网关只提供验签服务,对前端发起请求中的 Token 进行有效性校验,并提取Token中的用户和权限信息,将提取到信息放到Header中进行转发,同时标记当前请求已被网关处理。 这种方式可以有效的避免分散式鉴权中各个微服务重复解析Token 而带来的时间成本,也能减少集中式鉴权中权限信息维护在网关的配置复杂度。 各个微服务在接收到网关转发的请求后,直接读取Header中的用户权限信息,构建 Authentication 对象,后续使用 SpringSecurity 进行权限验证。

统一认证服务模块设计:

├── commons/          # 通用基础模块
│   └── security      # 安全认证,包括基础的安全配置、异常定义、权限识别等
├── auth/             # 认证中心
│   └── user          # 用户服务(登录)
├── infra/            # 基础设施
│   └── gateway       # 网关服务,提供JWT验签、动态路由等。
└── business/         # 业务服务
    └── order         # 订单服务

5.2.标准协议

  • CAS:耶鲁大学开发的单点登录系统称为CAS(Central Authentication Server),他是一个开源的、相对比较简单易用的SSO解决方案。
  • OAuth2:一种授权框架,允许用户授权第三方应用程序访问他们在其他服务上存储的资源,而无需共享他们的凭据。
  • LDAP:一种用于访问分布式目录服务的协议,用于管理组织中的用户和资源。
  • OIDC(OpenID Connect):基于OAuth 2.0的标准,提供身份验证和授权功能,允许用户使用一个身份验证服务来登录多个应用程序。
  • SAML:一种基于XML的标准,用于在不同的安全域之间传递身份验证和授权数据。
  • Kerberos:一种网络身份验证协议,用于在客户端和服务器之间进行安全通信。
  • JAAS:Java Authentication Authorization Service,Java验证和授权API,提供了灵活和可伸缩的机制来保证客户端或服务器端的Java程序。

除此之外,重要的还有用户身份管理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC权限管理等。高性能微服务JWT + Gateway政企内网 / 传统系统CAS 单点登录

5.3.身份认证框架

但是可以借助开源工具,实现控制。比如:

  • Apache Shrio:https://shiro.apache.org/
  • oltu:https://oltu.apache.org/
  • Spring Security
  • MaxKey:https://gitee.com/dromara/MaxKey 单点登录认证系统(Single Sign On System),寓意是最大钥匙,是业界领先的企业级IAM身份管理和身份认证产品。 支持OAuth 2.0/OpenID Connect、SAML 2.0、JWT、CAS、SCIM等标准协议, 提供简单、标准、安全和开放的用户身份管理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC权限管理和资源管理等。
  • oauth2-shiro:https://gitee.com/mkk/oauth2-shiro 整合Apache Oltu 与 Shiro,提供一个轻量的OAUTH2应用框架,并根据不同的应用场景提供不同的实现(如web场景,移动设备)
  • spring-lhbauth:https://gitee.com/lvhaibao/spring-lhbauth 基于OAuth2和JWT实现认证授权。
  • oauth2:https://gitee.com/lyric/oauth2 Golang实现的OAuth 2.0服务端,依照协议RFC 6749实现,具有简单使用、灵活、模块化的特点;支持内存存储、文件存储、redis存储、mongodb存储。
  • oauthserver:https://gitee.com/jeesun/oauthserver 是一个基于Spring Boot Oauth2的完整的独立的Oauth2 Server微服务。项目的目的是,仅仅需要创建相关数据表,修改数据库的连接信息,你就可以得到一个Oauth2 Server微服务。
  • JustAuth:https://gitee.com/yadong.zhang/JustAuth 是一个第三方授权登录的工具类库,它可以让我们脱离繁琐的第三方登录 SDK。 集成了诸如:Github、Gitee、支付宝、新浪微博、微信、Google、Facebook、Twitter、StackOverflow等国内外数十家第三方平台。

5.4.SSO方案1-CAS

SSO:本质是"统一认证入口+信任共享",解决"多系统登录繁琐"和"认证逻辑冗余"的问题

  1. CAS。统一认证中心
  2. JWT。携带认证信息

关键概念:

  1. TGT(Ticket Granting Ticket)全局会话凭证,存在认证中心的Cookie里,证明用户"已经登录过认证中心";
  2. ST(Service Ticket)临时服务票据,一次性、有有效期,业务系统用它向认证中心"验明正身"。

标准流程:

  1. 首次登录系统A用户访问系统A,系统A发现未登录,重定向到SSO认证中心,附带回调地址:sso.com/login?redirect=systemA.com;
  2. 用户在认证中心输入账号密码,验证通过;
  3. 认证中心生成TGT(全局会话),写入自己域名下的Cookie(仅sso.com可访问);
  4. 认证中心生成ST(临时票据),重定向回系统A:systemA.com?ticket=ST-123;
  5. 系统A拿着ST,通过后端接口调用认证中心验证;
  6. 验证通过,认证中心返回用户信息,系统A创建自己的本地Session,用户成功访问系统A。

免登访问系统B

  1. 用户访问系统B,系统B发现未登录,重定向到SSO认证中心;
  2. 认证中心检测到Cookie里有有效的TGT(证明已登录),直接生成新的ST,跳过登录页面,重定向回系统B;
  3. 系统B拿着ST去认证中心验证,通过后创建本地Session,用户免登访问系统B。

认证中心直接把用户ID、用户名放在重定向URL里,系统A不就直接能用了?何必搞ST这么复杂?

答案是安全!URL是明文传输,会被浏览器历史记录、服务器日志留存,用户信息直接暴露,风险极高。 而ST的优势在于:一次性有效:验证完就失效,就算被截获也没用;时效性短:通常几分钟就过期,窗口风险极小; 后端验证:用户信息只在认证中心和业务系统的后端交互,前端完全接触不到。

CAS的坑:生产环境要注意什么?

CAS虽经典,但在分布式、高并发场景下有明显短板:

  1. 网络开销大每个业务系统验证都要调用认证中心,高峰期容易成为瓶颈;
  2. 单点风险SSO认证中心挂了,所有系统都无法登录;
  3. 跨域Cookie风险TGT存在Cookie里,跨域场景下可能遭遇CSRF攻击。

于是,更适合分布式架构的"基于Token的SSO方案"应运而生,核心就是JWT。

5.5.SSO方案2-JWT

JWT(JSON Web Token)是目前微服务架构中最流行的SSO方案,核心优势是"无状态"——服务端不用存Session,仅凭Token本身就能完成验证。

实现流程:

  1. 用户在SSO认证中心登录,认证中心生成JWT,返回给前端;
  2. 前端存储JWT(存储位置后面详细说);
  3. 用户访问系统A,前端在HTTP请求头的Authorization字段中带上JWT(格式:Bearer );
  4. 系统A用服务端密钥验证JWT签名:签名通过则解析Payload获取用户信息,直接完成认证,无需调用认证中心。
  • 优点:各业务系统独立验证Token,不用依赖认证中心,性能高、扩展性好,完美适配微服务。
  • 缺点:
    1. JWT本身不存储Session,无法实现"记住我"功能,也就无法主动失效。解决方案:后端存储到Redis中,配合定时任务清理失效的Token。
    2. JWT的存储位置需要前端保存,不能跨域,需要单独实现跨域传输的机制。

JWT该存在哪里?

敏感系统用"HttpOnly Cookie + SameSite=Strict"(防XSS和CSRF);非敏感系统用LocalStorage,前端自行管理。

5.6.SSO方案3-OAuth

上述方案都存在 无法退出(A系统退出,其他系统也必须退出)、跨域(尤其是同主域名不同子域名间的共享认证)

注意:OAuth2.0是"授权",SSO是"认证"。可以用OAuth2.0实现SSO,但不能说两者是一回事!

5.7.登录安全问题

1.使用HTTPS。不要使用get请求发送敏感信息

2.WEB前端可以通过某种算法,对密码字段进行加密后,在将密码作为Http请求的内容进行提交,常见的包括对称和非对称加密。

  • 对称加密:采用对称密码编码技术,它的特点是文件加密和解密使用相同的密钥加密。
  • 非对称加密:需要两个密钥,公开密钥(publickey)和私有密钥(privatekey)。 公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。

3.防获取

  • 方案一:验证码。MVC场景。控制器将把数据的Model封装到View中,这种存在Session的连接方式,允许了在Session中存取信息。

那么我们可以利用一些开源的验证码生成工具,例如JAVA中的Kaptcha,在服务端存放生成一个验证码值以及一个验证码的生成图片, 将图片以Base64编码,并返回给View,在View中解码Base64并加载图片,并于用户下次登录时再进行比对。

  • 方案二:token令牌

前后端分离场景。现在非常流行的前后端分离的开发模式大大提高了项目的开发效率。职责、分工明确,但是由于HTTP是无状态的, 当用户登录时,根据用户的username作为key,生成随机令牌(例如UUID)作为value缓存在Redis中,并且将token返回给客户端, 当客户端登录时,将完成校验,并且删除Redis中的那条缓存记录。

那么每次从服务器中获取认证的token,确实能保证HTTP请求是由前端传回来的了,因为token在每次登陆后都会删除并被重置, 会导致黑客尝试重放账号密码数据信息来登陆的时候导致无法成功登陆。

总而言之,就是我拿到了账号以及密码的密文也登陆不了,因为,如果请求不包含后台认证的令牌token,是个非法请求。

4.防篡改

假如发送方想把一份报文发送给接收方,在发送报文前,发送方用一个哈希函数从报文文本中生成报文摘要,然后用自己的私人密钥对这个摘要进行加密, 这个加密后的摘要将作为报文的"签名"和报文一起发送给接收方,接收方首先用与发送方一样的哈希函数从接收到的原始报文中计算出报文摘要, 接着再用发送方的公用密钥来对报文附加的数字签名进行解密,如果这两个摘要相同、那么接收方就能确认报文是从发送方发送且没有被遗漏和修改过! 这就是结合"非对称密钥加解密"和"数字摘要"技术所能做的事情,这也就是人们所说的"数字签名"技术。

在这个过程中,对传送数据生成摘要并使用私钥进行加密地过程就是生成"数字签名"的过程,经过加密的数字摘要,就是"数字签名"。

因此,我们可以在WEB端对之前案例中提到的username+MD5(password)+token通过签名,得到一个字段checkCode,并将checkCode发送给服务器, 服务器根据用户发送的checkCode以及自身对原始数据签名进行运算比对,从而确认数据是否中途被篡改,以保持数据的完整性。

5.补充1:JS加密函数存在被破解

完整性检验中关于JS加密函数存在被破解的问题:
问题描述:如果黑客通过阅读前端js源码,发现加密算法,是否意味他可以构造可以被服务端解密的checkCode 来欺骗服务端呢?

采取的策略:
摘要或加密JS算法不直接以静态文件的形式存在浏览器中,而是让WEB端去请求Server,服务器可以根据随机令牌token值决定返回一个相应随机的加密策略,
以JS代码响应的方式返回,在异步请求响应中,加载JS摘要算法,这样客户端就可以动态加载数字摘要策略,保证无法仿造。

6.补充2:MD5存在隐患的问题

问题描述: 用MD5、SHA256 处理密码的过时了,并且存在不安全的问题。。。现在 PBKDF、bcrypt 都在过时中。

主要思想是: 对于MD5的破解,实际上都属于【碰撞】。比如原文A通过MD5可以生成摘要M,我们并不需要把M还原成A,只需要找到原文B,生成同样的摘要M即可。

设MD5的哈希函数是MD5(),那么:
MD5(A) = M
MD5(B) = M
任意一个B即为破解结果。
B有可能等于A,也可能不等于A。
大概意思也就是,截获了MD5加密后的密文,一样可以,找到一个不是原密码,但是加密后可以登陆成功的"伪原文"。

5.8.案例

5.8.1.扫描登录案例

微信和淘宝扫码登录背后的实现原理: https://mp.weixin.qq.com/s/_ZKxZAK5OCVMojnR5wwI2w

5.8.2.在线人数统计

利用Redis的ZSET实现。所有用户保存在一个key中(当然也可以拆分为多个key,降低大key对性能的影响)

用户识别

  • 对于需要登录的系统,可以使用用户的账号作为key进行统计
  • 对于允许匿名访问的系统,可以可用deviceId、浏览器指纹、ip作为key。目前浏览器安全要求越来越高,deviceId已经禁止访问到。 所以推荐使用浏览器指纹,利用 User-Agent、HTTP请求头信息、屏幕分辨率和颜色深度、时区和语言设置、浏览器插件详情等计算生成, 现成的JavaScript库,像 FingerprintJS 或 ClientJS

工作过程

  1. 添加在线用户:zadd key:用户标识 score:过期时间
  2. 查询在线人数:zrangeByScore score_min:当前时间 score_max:+inf (正无穷)
  3. 清除过期人数: zremrangeByScore score_min:-inf(负无穷) score_max:当前时间
  4. 退出登录删除:zrem key:用户标识

5.8.3.浏览器指纹

利用 User-Agent、HTTP请求头信息、屏幕分辨率和颜色深度、时区和语言设置、浏览器插件详情等计算生成

现成的JavaScript库,像 FingerprintJS 或 ClientJS

FingerprintJS。

ClientJS。主要功能:浏览器指纹、前端客户端的各种数据(和指纹相关的)

6.JWT

6.1.介绍

JWT(JSON Web Token)是为了网络应用环境间传递声明而执行的一种基于JSON的开发标准(RFC 7519)。 JWT描述了一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的JSON 对象作为通信双方传递信息的安全载体。 因为数字签名的存在,这些信息的传输是可信的、不可篡改的。

JWT规范地址https://datatracker.ietf.org/doc/html/rfc7519

6.2.使用场景

这里特指web服务登录,而http是无状态的,无法记录用户状态。所有就出现了session与cookie两种方案。 session数据保存在服务端与cookie数据保存在客户端。

  1. 浏览器与服务端交互,用于保存登录状态信息。通常会将信息保存到cookie中,这样登录后之后的操作就会自动携带验证信息。
  2. 服务端之间交互,通常保存到header中,用于接口鉴权。

6.3.常用的框架

在 https://jwt.io/ 网站中收录有各类语言的JWT库实现,按顺序依次是。

  • java-jwt:Auth0提供的JWT库简单实用, 依赖第三方(如JAVA运行环境)提供的证书信息(keypair); 有一问题是在生成id_token与 校验(verify)id_token时都需要 公钥(public key)与密钥(private key), 个人感觉是一不足(实际上在校验时只需要public key即可)。
  • jose4j:提供了完整的JWT实现, 可以不依赖第三方提供的证书信息(keypair, 库本身自带有RSA的实现),类定义与JWT协议规定匹配度高,易理解与上手。对称加密与非对称加密都有提供实现。
  • nimbus-jose-jwt:库类定义清晰,简单易用,易理解 , 依赖第三方提供的证书信息(keypair), 对称算法 与非对称算法皆有实现。
  • jjwt:小巧够用, 但对JWT的一些细节包装不够, 比如 Claims (只提供获取header,body)。
  • prime-jwt:支持对称算法(HMAC) 与非对称算法(RSA), 也算容易理解。
  • vertx-auth-jwt:非常困难理解,并且生成与校验id_token 时都需要公钥与私钥。

首先推荐 jose4j, 其次是 Nimbus-jose-jwt

6.4.认证流程

  • 基于session的认证流程
    • 用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个session并保存到数据库
    • 服务器为用户生成一个sessionId,并将具有sesssionId的cookie放置在用户浏览器中,在后续的请求中都将带有这个cookie信息进行访问
    • 服务器获取cookie,通过获取cookie中的sessionId查找数据库判断当前请求是否有效
  • 基于JWT的认证流程
    • 用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个token并保存到数据库
    • 前端获取到token,存储到cookie或者local storage中,在后续的请求中都将带有这个token信息进行访问
    • 服务器获取token值,通过查找数据库判断当前token是否有效

优缺点:

JWT保存在客户端,在分布式环境下不需要做额外工作。而session因为保存在服务端,分布式环境下需要实现多机数据共享 session, 一般需要结合Cookie实现认证,所以需要浏览器支持cookie,因此移动端无法使用session认证方案

安全性:

JWT的payload使用的是base64编码的,如果在JWT中存储了敏感信息,可以解码出来非常的不安全,因此在JWT中不能存储敏感数据。 在实际开发中需要用下列手段来增加JWT的安全性:

因为JWT是在请求头中传递的,所以为了避免网络劫持,推荐使用HTTPS来传输,更加安全 JWT的哈希签名的密钥是存放在服务端的,所以只要服务器不被攻破,理论上JWT是安全的。因此要保证服务器的安全JWT可以使用暴力穷举来破解, 所以为了应对这种破解方式,可以定期更换服务端的哈希签名密钥(相当于盐值)。

性能:

经过编码之后JWT将非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在local storage里面。 并且用户在系统中的每一次http请求都会把JWT携带在Header里面,HTTP请求的Header可能比Body还要大。 而sessionId只是很短的一个字符串,因此使用JWT的HTTP请求比使用session的开销大得多

6.5.分类

  • JWS:Signed JWT,Signature部分不为空。是一种常用的、简单的JWT实现方案。
  • JWE:Encrypted JWT,全文加密的JWT,数据无法使用base64解析。
  • JKU:JSON Web Key Set URL,Header中包括URI,该URI用于指定用于验证令牌秘钥的服务器,验证时需要范围URI获得JWK。
  • X5U:是 Header中的一个字段,指向一组X509公共证书的URL,与JKU功能类似

上面分类中提到当前概念

  • JWK:JWT的密钥,也就是我们常说的secret
  • X.509标准:X.509标准是密码学里公钥证书的格式标准,包括TLS/SSL(WWW万维网安全浏览的基石)在内的众多Internet协议都应用了X.509证书)

6.6.基本结构

下面是一个JWT在线构造和解构的平台:https://jwt.io/

JWT(JSON Web Token)的结构由三部分组成,分别是Header、Payload和Signature,以下为JWS的案例。

6.6.1.Header

Header包含了JWT使用的算法和类型等元数据信息,通常是JSON结构、使用Base64编码,数据内容不固定,允许自定义。

下面的案例中,Header中包含两个字段:alg和typ

  • alg(algorithm):指定了使用的加密算法,常见的有HMAC、RSA和ECDSA等算法
  • typ(type):指定了JWT的类型,通常为JWT

下面是一个示例Header:

{
  "alg": "HS256", # 使用HMAC-SHA256算法进行签名
  "typ": "JWT"    # JWT的类型为JWT
}

6.6.2.Payload

Payload包含了JWT的主要信息,通常是JSON结构、使用Base64编码,Payload中包含三个类型的字段:注册声明、公共声明和私有声明

  • 公共声明(Public Claims):是自定义的字段,用于传递非敏感信息,例如:用户ID、角色等
  • 私有声明(Private Claims):是自定义的字段,用于传递敏感信息,例如密码、信用卡号等
  • 注册声明(Registered Claims):预定义的标准字段,包含了一些JWT的元数据信息,例如:发行者、过期时间等

下面是一个示例Payload:

{
  "sub": "1234567890",  # 主题
  "name": "John Doe",   # 名称
  "iat": 1516239022     # JWT的签发时间
}

6.6.3.Signature

Signature是使用指定算法对Header和Payload进行签名生成的,用于验证JWT的完整性和真实性。 Signature的生成方式通常是将Header和Payload连接起来然后使用指定算法对其进行签名, 最终将签名结果与Header和Payload一起组成JWT,Signature的生成和验证需要使用相同的密钥。

下面是一个示例Signature:HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload), secret)

  • HMACSHA256是使用HMAC SHA256算法进行签名,
  • header和payload经过Base64编码,并使用句点(.)连接
  • secret是用于签名和验证的密钥

6.6.4.最终数据

Header、Payload和Signature使用句点(.)连接起来。因为JWT中的每一部分都是经过Base64编码的,不是加密的,因此JWT中的信息是可以被解密的。 如果响应高安全性的JWT,可以选择JWE进行全文加密。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

6.6.5.最佳实践

JWS:

# header
{
  "alg": "HS256", # 签名使用的加密算法
  "typ": "JWS"    # JWT的类型:JWS 和 JWE
}
# payload
{
    "iss": "saabadmin",   # JWT的签发者
    "sub": "02110001",    # 生成JWT的用户
	"aud": "qnbapp",      # 接收JWT的用户
    "exp": "1586073540123", # 过期的时间
    "iat": "1586071749953",  # 签发的时间
    "???": "??"           # 自定义数据,确保最小可用
}
# Signature
此部分为算法自行生产,无需控制。

JWE:JWS是最常见的JWT实现策略,但只能保证请求过程中Token不可篡改,无法实现请求报文完全加密,也无法避免Token被拦截后的重放攻击。 所以可以使用JWE,JWE模式结合了对称加密、非对称加密和签名认证,提升安全防护等级。

# header
{
  "alg": "SM4",   # 签名使用的加密算法
  "typ": "JWE",    # JWT的类型:JWS 和 JWE
  "secretId": "xxxxxxxxxx"  # 用于表示加密算法的信息。
  # 使用SM4为对称加密,双方使用同一个秘钥,秘钥不能出现在JWT中,所以使用一个标识,让接收方知道使用哪个秘钥进行验签。
  # 使用SM2非对称加密,secretId可以写入公钥,让接收方知道使用哪个私钥进行验签。
}
# payload。同JWS,但是整个json数据进行了加密

6.7.更新策略

JWT的签发是基于统一的RFC 7519标准,但Token签发后的更新策略则需要根据使用场景具体规划,不同的更新策略的带来的实现效果差距很大。以下为常见的Token更新策略及对比。

6.7.1.主动更新

主动更新模式下,客户端每次请求服务端,服务端在校验请求中的Token可用后,会主动签发新Token返回客户端,以此更新客户端Token有效期,旧Token在过期时间前仍然有效。 此策略可以保证客户端在Token有效期内操作不会出现超时情况,用户体验较好,实现方式也较简单。但由于每次请求都需要做解密和加密操作,认证服务会有较高压力。

6.7.2.被动更新

为了解决主动更新带来的认证服务压力问题,部分使用场景使用了Token被动更新策略。被动更新模式下,服务端对客户端正常请求包含的Token只做验证操作,不主动更新Token。 服务端提供单独的Token更新接口,更新动作由客户端触发,客户端通过设定的定时请求策略(请求间隔需小于Token有效期),携带旧Token或专门的RefreshToken请求服务端Token更新接口,服务端校验通过后签发新Token返回客户端。 此策略大大降低了后端认证服务的压力,同时保证客户端在Token有效期内操作不会出现超时情况。但此方案需要在客户端维护定时更新逻辑,对接外部系统的复杂度较高,一般仅在自有的移动端APP中使用。

6.7.3.判断更新

主动更新和被动更新策略都或多或少都存在不足,为了即降低服务端压力,同时降低对接复杂度,可以结合两种策略的实现方式采用服务端判断更新策略。具体实现方式如下:

  1. 服务端设置Token更新阈值,此阈值需小于Token有效期。
  2. 客户端每次请求服务端,服务端除校验Token的合法性还需判断Token有效期,如有效期低于更新阈值,则签发新Token返回客户端。

注意事项:

  1. 因客户端请求服务端的时间不确定,Token更新的时间也是不确定的,故会出现客户端实际操作超时时间小于Token有效期的情况。 可通过设置更新容忍期提升用户体验,实现思路如下: (期望最短超时时间+更新容忍期=Token有效期) 示例:如期望客户端实际操作超时不低于30分钟,则可设置Token有效期为40分钟,服务端判断Token有效期低于30分钟时更新Token,更新容忍期为10分钟。
  2. 某些场景下需控制单次登录后Token的更新期限,如JWE模式仍不满足要求,又不希望引入统一缓存,则可考虑在Token中增加Refresh过期时间(比如设置为12小时),
  3. 一次登录之后Token中Refresh过期时间不变,直至下次重新登录,避免某次Token被拦截泄露后,对方持续使用旧Token获得新Token。 客户端每次请求后端,后端先对比Refresh过期时间和Token超时时间,如Token超时时间晚于Refresh过期时间,则不再更新Token,直至客户端超时重新登录。 如Token超时时间早于Refresh过期时间,则更新Token。

6.7.4.过期后更新

当请求中的token已经过期后,再去更新token。但是需要限定被更新的时间和次数。例如登录之后24小时后必须重新登录,期间token过期了可以自动续期。

6.8.实践方案

常用的是单token方案、双token方案。微信网页授权是通过OAuth2.0机制实现的,也使用了双token方案。

  1. 登录成功以后,后端返回 access_token 和 refresh_token,客户端缓存此两种token;
  2. 使用 access_token 请求接口资源,成功则调用成功;如果token超时,客户端携带 refresh_token 调用token刷新接口获取新的 access_token;
  3. 后端接受刷新token的请求后,检查 refresh_token 是否过期。如果过期,拒绝刷新,客户端收到该状态后,跳转到登录页;如果未过期,生成新的 access_token 返回给客户端。
  4. 客户端携带新的 access_token 重新调用上面的资源接口。
  5. 客户端退出登录或修改密码后,注销旧的token,使 access_token 和 refresh_token 失效,同时清空客户端的 access_token 和 refresh_toke。

6.9.优缺点

优点

  • 轻量级:JWT是一种紧凑的标准,因此在网络上传输时的开销相对较小。
  • 自包含:JWT令牌中包含了所有必要的信息,不需要再向服务器请求更多数据。
  • 跨语言:JWT是一种开放标准,因此可以在多种编程语言中使用和解析。
  • 可扩展性:JWT的声明部分允许添加自定义声明,以满足特定应用的需求。
  • 无状态:由于JWT令牌自包含,不需要在服务器端保存会话信息,使应用可以更容易地实现无状态服务。
  • 防篡改;使用算法进行签名和验证。

缺点

  • 不可撤销:一旦JWT令牌生成并颁发,就很难撤销或回收。这意味着一旦令牌被泄露,它将有效直到过期。
  • 安全依赖于密钥管理:JWT的安全性高度依赖于密钥的安全管理。如果密钥不够安全或被泄露,令牌可能会受到威胁。
  • 不适用于大型应用:对于大型应用或需要高度扩展性的系统,JWT可能不是最佳选择,因为它可能导致扩展性问题和性能下降。
  • 携带多余信息:JWT令牌中可能包含了一些应用不需要的信息,导致传输带宽的浪费。

但在大型应用中可能会遇到一些挑战,导致大型公司较少采用:

  • 扩展性问题:JWT在某些情况下可能导致扩展性问题,特别是在处理大量声明或密钥轮换方面。这可能会使JWT变得笨重和不适合大规模应用。
  • 安全性问题:JWT的安全性高度依赖于密钥的保护,如果密钥不够安全或者泄露,那么攻击者可能会伪造JWT令牌。因此,必须非常小心地管理密钥。
  • 无法取消或回收:一旦颁发了JWT令牌,就很难取消或回收。如果需要撤销访问权限,必须等待JWT令牌到期,或者实施额外的机制。
  • 大型令牌:JWT令牌可能变得非常大,特别是当包含了多个声明或附加信息时。这可能会导致不必要的带宽开销。
  • 复杂性:JWT需要在客户端和服务器之间正确实现和验证,这可能需要大量开发工作。大型公司可能更愿意采用其他更成熟的身份验证和授权解决方案。

总结:

  • AccessKey:安全性高、方便管理。有客户端一次性生成后,服务端验证。
  • Token:结构简单、易于实现。通常服务端生成,客户端拿到后使用。

下面使用腾讯云API为例,使用accessKey的案例。

// 1.生成stringToSign
byte[] requestPayload = jsonPayload.getBytes(StandardCharsets.UTF_8);
String canonicalUri = "/";
String canonicalQueryString = "";
String canonicalHeaders = "content-type:" + contentType + "\nhost:" + endpoint + "\n";
String signedHeaders = "content-type;host";
String hashedRequestPayload = "";
if (this.profile.isUnsignedPayload()) {
  hashedRequestPayload = Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8));
} else {
  hashedRequestPayload = Sign.sha256Hex(requestPayload);
}
String canonicalRequest =
    HttpProfile.REQ_POST
        + "\n"
        + canonicalUri
        + "\n"
        + canonicalQueryString
        + "\n"
        + canonicalHeaders
        + "\n"
        + signedHeaders
        + "\n"
        + hashedRequestPayload;

String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
String service = endpoint.split("\\.")[0];
String credentialScope = date + "/" + service + "/" + "tc3_request";
String hashedCanonicalRequest = Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
String stringToSign = "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;

String secretId = this.credential.getSecretId();

// 2、生成secretSigning
String secretKey = this.credential.getSecretKey();
byte[] secretDate = Sign.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date);
byte[] secretService = Sign.hmac256(secretDate, service);
byte[] secretSigning = Sign.hmac256(secretService, "tc3_request");

// 3.使用 secretSigning 和 stringToSign 生成签名
String signature =  DatatypeConverter.printHexBinary(Sign.hmac256(secretSigning, stringToSign)).toLowerCase();

// 4.生成鉴权信息    
String authorization =
    "TC3-HMAC-SHA256 "    //固定值,描述算法
    + "Credential="       //客户唯一标识,用户查询私钥
    + secretId
    + "/"
    + credentialScope     //date + "/" + service + "/" + "tc3_request";
    + ", "
    + "SignedHeaders="    // 固定值
    + signedHeaders
    + ", "
    + "Signature="        // 签名[最重要]
    + signature;
        
// 5.设置header
Builder hb = new Headers.Builder();
hb.add("Content-Type", contentType)
    .add("Host", endpoint)
    .add("Authorization", authorization)
    .add("X-TC-Action", action)
    .add("X-TC-Timestamp", timestamp)
    .add("X-TC-Version", this.apiVersion)
    .add("X-TC-Region", this.getRegion())
    .add("X-TC-RequestClient", SDK_VERSION)
    .add("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD");
    .add("X-TC-Language", this.profile.getLanguage().getValue());

Headers headers = hb.build();
Response resp = conn.postRequest(url, requestPayload, headers);

文章信息

张雪随笔
master
Homepage
Issues
Download

This Software is under the terms of The Unlicense.