你了解单点登录吗?为什么要使用单点登录?它有哪几种实现方式?每个实现方式的优缺点及适用场景是什么样的?看完这篇文章以后,你将解开以上疑惑。
单点登录
单点登录(Single Sign On),简称SSO,是目前比较流行的企业业务整合的解决方案之一。在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
优点
- 提高工作效率:用户只需一次登录就可以访问所有被授权的应用系统,无需重复输入用户名和密码,避免了频繁切换应用系统的麻烦。
- 简化密码管理:集中管理用户的身份信息,减少了员工忘记或混淆密码的情况。
- 提升安全性:集中管理减小网络攻击面,提高企业整体安全性。
- 节省开发成本:对用户登录授权集中管理,多个系统共用节省开发时间。
认证、授权、鉴权和权限控制
在介绍单点登录之前,我们需要对几个容易混淆的概念做下统一的定义。
认证(Identification)
认证是一种信用保证形式,实质就是四个字"确认身份"。认证对象需要提供自身的特定信息来证明自己的身份。例如,在登录时我们经常使用的用户名/密码、手机验证码、电子邮箱、二维码和生物特征(面部、指纹、语音和虹膜等)。在一些安全要求比较高的场景下,还会用到多因素认证方式,例如用户名/密码+手机验证码登录。
授权(Authorization)
授权就是将某种特权赋予其他实体 。这里的特权一般是指执行某种权力,在信息系统里是指授予其他系统访问其资源的权力。授权的形式有很多种,例如传国玉玺、钥匙、证书、门禁卡等。在信息系统里授权的媒介则变成了sessionid、token令牌。
鉴权(Authentication)
当资源持有者向资源申请者授权并发放了令牌之后,资源申请者就可以通过令牌访问资源了,在此过程中鉴权服务器需要对令牌的真实性做一个鉴别,这个过程就是鉴权。授权和鉴权是一一对应的流程。
权限控制(Access/Permission Control)
权限是对执行人行为的一个抽象化概念。而权限控制就是规定了执行人哪些行为是允许的,哪些行为是禁止的。
以上几个概念的顺序是认证-->授权-->鉴权-->权限控制。认证和授权一般放在一起,认证了身份之后就可以授权发放令牌。下一次访问资源时就可以拿着令牌申请,在鉴权通过以后进行权限控制。
认证和鉴权是两个比较容易混淆的概念。它们的区别是:
- 认证主要是为了确认用户的身份;鉴权则是为了确认用户的权限。
- 认证需要用户特定的身份信息进行识别;鉴权则是通过授权后得到的媒介,这个媒介具有真实性、不可伪造和不可篡改的特点。
授权/鉴权方案
授权/鉴权方案大致可以分为以下四个类型,接下来我们就详解一下这四种方案都有什么应用场景以及各自的优缺点。
Http Basic Authentication
Http Basic Authentication顾名思义,采用的是Http协议。
具体步骤如下:
- 浏览器向服务发送请求获取资源。
- 服务器返回状态码401,提示需要认证用户身份。
- 浏览器收到401的返回状态后,跳转到登录页面,用户登录。
- 用户发送带有用户名/密码加密信息的请求。
- 服务器根据Authorization字段解密后,与数据库里保存的用户名/密码进行比对校验,通过则返回请求的数据。
优点:
- 实现简单
- 几乎所有浏览器都支持
缺点:
- 客户端发送的认证请求极其容易被拦截
- 服务器端无法注销清除认证信息。客户端除非清除历史记录或关闭浏览器,可以一直保持登录状态。
适用场景:
一般在安全性较高的小型私有环境使用。
session+cookie
了解了Http Basic Authentication方式的缺点和应用场景之后,你会问有没有一种实现方式可以更安全,适用于中大型网站系统呢?答案是肯定的,而且方法有很多,session+cookie的方式就是其中一种。为了解决Http Basic Authentication方式不能在服务器中注销的问题,服务器为每个客户端生成一个会话(session),用来保存用户的会话状态,这样就可以解决服务器不能清理会话状态的问题了。
具体实现步骤:
- 浏览器请求登录页面
- 服务器生成会话(session),返回一个sessionid
- 浏览器将用户输入的用户名/密码和cookie中的sessionid一并发送给服务器
- 服务器验证用户名/密码,成功之后更新session里的用户登录信息
- 返回Http响应,提示登录成功
优点:
- 用户信息保存在服务端,相较于保存在客户端更安全
- 保存在服务端的session基本上没有大小限制,除了用户登录信息还可以保存用户行为、统计等
- 客户端使用sessionid即可快速验证身份,减少数据库访问次数
缺点:
- 保存session增加了管理和存储成本
- 容易受到CSRF(跨站请求攻击 Cross-Site Request Forgery)攻击
- 大型系统为了负载均衡,session需要共享,所以session一般放在分布式缓存里。一旦缓存崩溃,所有用户登录授权都会收到影响。
适用场景:
中大型系统的登录、注册、购物车等场景,适用于相互信任的多个站点。
token
session和token两种方式本质上没有太大的区别,都是对用户身份的授权/鉴权,唯一的不同是它们的鉴权方式不一样。
- session是将用户信息保存在服务端,通过查询redis等缓存信息鉴权
- token是将用户信息保存在客户端,服务端鉴权时通过签名来校验用户身份
由于用户信息存储位置的不同,session方式适用于大多数场景,但如果是一次性认证授权,使用token更合适。例如,第三方应用授权。
具体实现步骤:
- 客户端发送用户名/密码请求登录
- 服务端认证用户名/密码成功后,生成一个token并返回
- 客户端收到token保存在本地,如storage或cookie中
- 客户端请求服务器资源,发送携带token参数的请求,token鉴权成功则返回请求数据,否则返回401错误码
token的生成规则一般使用对称加密算法,密钥保存在服务端,客户端不能解密。服务端拿着密钥将用户id、时间戳等必要信息做对称加密。
优点:
- 可以避免CSRF(跨站请求攻击 Cross-Site Request Forgery)攻击。不同于session直接校验cookie中的sessionid,token是被当作参数放入请求体或请求头,达到避免CSRF攻击的目的。
- token在会话期间可以动态改变,sessionid则在会话期间一直有效。
- session需要cookie配合使用,所以只能在浏览器使用;token则没有这个限制,支持多种类型的客户端。
- token存储在客户端,节省了服务器存储空间,也减少了数据库查询次数。
缺点:
- token比sessionid更大,所以更占用带宽
- token鉴权需要在服务端编码、签名,更消耗性能
适用场景:
适用于中大型网站、app客户端和三方对接的鉴权。
JWT
JWT(Json Web Token)是一种基于token的对json进行加密签名实现授权鉴权的解决方案。
JWT由三个部分组成,它们之间用点(.)分隔开:
- Header(头部)
- Payload(负载)
- Signature(签名)
Header
JWT的第一部分Header是一个JSON对象,例如:
"alg"表示算法(algorithm),默认是HMAC SHA256(HS256),也可以是RSA等等。
"typ"表示token的类型,JWT令牌统一是"JWT"。
以上JSON对象会被Base64URL算法编码作为JWT的第一部分。
顺便提一下Base64URL算法,由于token有可能会被当做url的参数传递,所以JWT使用和Base64相似的Base64URL算法进行编码。Base64编码字符串有三个字符+、/和=在url里是特殊字符,Base64URL算法会将+替换为-,/替换为_,=则忽略掉。
Payload
JWT的第二部分Payload也是一个JSON对象,JWT定义了7个官方字段,这7个字段是非强制的,推荐使用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
除了7个官方字段,也可以定义私有字段,例如
需要特别强调的是,JWT默认是非加密的,所以请不要在JWT中放入敏感信息。
Signature
JWT的第三部分Signature是对Header和Payload的签名,以防止数据被篡改。
签名算法使用的是Header里指定的,JWT默认使用HMAC SHA256。按照下图的公式产生签名。secret是保存在服务器里的一个密钥,有且仅有服务器知道这个密钥。
最后,将三个部分用点(.)连接成一个字符串,就是返回给用户的JWT令牌了。
如何使用token
客户端接收到token之后会保存在cookie或local storage里。token有三种使用方式。
- 使用cookie保存,每次通信都会自动带上token。这种方式的缺点是不能实现跨域,另外也会和session一样有受到CSRF攻击的风险。
- 将token放入请求头Authorization里。
- 将token放入post的请求体里。
如何废除token
虽然token将用户信息保存在客户端,带来了很多灵活和扩展性,但是token方式依然有一些缺点。其中最大的一个缺点就是服务器无法在token有效的情况下废除其权限。所以在生成token时,应将token的有效时间设置的尽可能小。
当用户做了注销操作以后,如果我们需要给token做失效处理,可以有以下几种方案:
- 在缓存中保存一个token列表作为黑名单,鉴权时判断如果token在此黑名单中就鉴权失败,缓存里的token黑名单设置一定的有效期,到期了自动删除。
- 数据库保存用户最新一次登出或修改密码的时间,生成token时保存创建时间,将登出时间和token的创建时间作比较,如果token创建时间早于登出时间,则判断此token是过期token。
如何刷新token
为什么要刷新token?如果token的有效期设置为30分钟,那么在30分钟以后用户就要再次登录以获取新的token,这就非常的麻烦了。
刷新token之后以前的token是否废除?如果不需要废除以前的token,那么同一个时间内可能有好几个token都是有效的,如果业务没有强制要求,这种刷新token的方式比较好实现,刷新接口重新生成一个token就可以了;如果还需要废除以前的token,有以下两个方案可以选择:
方案一:缓存有效期
在redis里保存用户信息和有效期(也可以保存token,但不推荐)
- 登录成功后将用户信息或token保存在redis
- 半小时内有用户请求,则在redis里刷新有效期时间
- 半小时内没有用户请求,则redis的用户信息失效,下次请求时用户重新登录
方案二:双token
将token分为访问令牌access_token和刷新令牌refresh_token,access_token用来授权/鉴权,refresh_token用来刷新有效期。
- 登录成功后返回access_token和refresh_token,access_token有效期设置5分钟,refresh_token有效期设置为半小时或更久
- 使用access_token发送请求,请求成功则正常返回;请求失败,access_token超时,则客户端带上refresh_token调用刷新接口获取新的access_token
- 如果用户登出或修改密码,则客户端需要废除access_token和refresh_token
双token刷新的方式应用的也很广泛,微信小程序的授权就是采用的双token刷新方式。
有人可能会问,为什么不直接在单token里将有效期设置久一点,这样不是省了调用刷新接口的步骤吗?
这里打个比方,你去瑞幸买了一杯咖啡,服务员给了你一小杯咖啡并告诉你喝完可以续杯,但是这个续杯是有前提的,那就是如果你喝完之后离开了这个店下次就要重新再买一杯。服务员为什么不直接给你一大杯让你一次喝个够?因为这样会造成浪费。
同样的,一次性给你一个长时间有效期的单token,会有安全隐患,客户端有可能会存在大量长有效期的token,并且不方便在服务器废除。每次给你一个短有效期token,不够了再用同一个refresh_token来续,是不是更安全一点?