Spring Security Token 认证
# 前言
本文将会使用 Spring Security + JWT 实现登录及用户认证
# 前置知识
# Session 与 Cookie
在谈 session 和 cookie 前,我们先来谈谈会话。http 本身是无状态协议,服务器无法识别 HTTP 请求的出处,那么为了响应发送给相应的用户,必须让服务器知道请求来自哪里,这就是会话技术
会话就是客户端和服务器之间发生的一系列连续的请求和响应的过程。会话状态指服务器和浏览器在会话过程中产生的状态信息,借助于会话状态,服务器能够把属于同一次会话的一系列请求和响应关联起来
实现会话有两种方式:session 和 cookie。session 通过在服务器端记录信息确定用户身份,客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 session。属于同一次会话的请求都有一个相同的标识符(sessionID),客户端浏览器再次访问时只需要通过 sessionID 从 session 中查找该客户的状态就可以了。之后后端可以通过设置 cookie 的方式返回给客户端,若浏览器禁止 cookie,则可以通过 URL 重写的方式发送
cookie 是服务端在 HTTP 响应中附带传给浏览器的一个小的文本文件,一旦浏览器保存了某个 cookie,在之后的请求和响应过程中,会将此 cookie 来回传递,这样就可以通过 cookie 这个载体完成客户端和服务端的数据交互
使用 session 进行用户认证时,当用户第一次通过浏览器使用用户名和密码访问服务器时,服务器会验证用户数据,验证成功后在服务器端写入 session 数据,向客户端浏览器返回 sessionid,浏览器将 sessionid 保存在 cookie 中,当用户再次访问服务器时,会携带 sessionid,服务器会拿着 sessionid 从服务器获取 session 数据,然后进行用户信息查询,查询到,就会将查询到的用户信息返回,从而实现状态保持
cookie+session 是实现认证的一种非常好的方式,但是凡事都有两面性,它们实现的认证主要有以下缺点:
- 增加请求体积,浪费性能,因为每次请求都会携带 cookie
- 增加服务端资源消耗,因为每个客户端连接进来都需要生成 session,会占用服务端资源
- 容易遭受 CSRF 攻击,即跨站域请求伪造
注:通常 session 存储在内存中,多服务器状态需要存储到数据库中
# Token
# Acesss Token(访问令牌)
Acesss Token 是访问资源接口(API)时所需要的资源凭证,由 uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)组成
身份验证流程如下
# Refresh Token(刷新令牌)
refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作
Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录
Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样保持在内存中以应对大量的请求
# JWT
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案
这里推荐 GitHub 比较不错的 JWT 教学仓库:learn-json-web-tokens/README-zh_CN.md (opens new window)
# 组成
Token 由三部分组成(用小数点分割),为了便于阅读,下面用三行来展示,但是实际使用时是一个单独的字符串
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 // 头部
.eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9 // 载荷
.eUiabuiKv-8PYk2AkGY4Fb5KMZeorYBLw261JPQD5lM // 签名
2
3
请求头字段大致如下
Authorization: Bearer <token>
头部(Header)
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子
{
"alg": "HS256",
"typ": "JWT"
}
2
3
4
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
载荷(Payload)
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了 7 个官方字段,供选用
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
2
3
4
5
6
7
当然除了官方字段,你也可以在这个部分定义私有字段
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。如果想要加密,可以生成原始 Token 以后,用密钥再加密
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
签名(Signature)
Signature 部分是对前两部分的签名,防止数据篡改,是根据头部(第一部分)和载荷(第二部分)所计算出来的一个签名,会被用于校验 JWT 是否有效。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
2
3
4
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
注:前面提到,Header 和 Payload 串型化的算法是
Base64URL
。这个算法跟Base64
算法基本类似,但有一些小的不同。JWT 作为令牌,有些场合可能会放到 URL(比如 api. example. com/? token=xxx)。Base 64 有三个字符
+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。这就是Base64URL
算法。
# 流程
- 客户端发起登录请求,比如用户输入用户名和密码后登录。
- 服务端校验用户名和密码后,将用户 id 和一些其它信息进行加密,生成 token。
- 服务端将 token 响应给客户端。
- 客户端收到响应后将 token 存储下来。
- 下一次发送请求后需要将 token 携带上,比如放在请求头中或者其它地方。
- 服务端 token 后校验,校验通过则正常返回数据。
# 使用方式
- 放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT (如果放在 Cookie 里面自动发送,会造成不能跨域问题)
GET /calendar/v1/events
Host: api.example.com
Authorization: Bearer <token>
2
3
跨域的时候,可以把 JWT 放在 POST 请求的数据体里
通过 URL 传输
http://www.example.com/user?token=xxx
# 缺点
JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发,在到期之前就会始终有效,除非服务器部署额外的逻辑。
JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
# Token 和 JWT 的区别
Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。
# JWT 失效
我们需要实现如下功能,当用户退出登录或是修改密码时,需要使原来的 JWT 失效
我们可能想当然的是,设置对应接口,删除储存在客户端上的 token,但是这是防君子不防小人的做法,毕竟我们可以在注销之前通过一些手段将 token 拿到手,在注销后依然可以使用
或者是用户在多个设备登录,仅仅一个设备丢弃 Token,那么其他设备的之前的 Token 仍旧可以使用
为了实现上述功能,我们先要明白 JWT 遵守无状态原则,如果忽视这条原则,我们可以将 token 存入 DB(如 Redis)中,失效则删除,但是增加了每次校验时候都要先从 DB 中查询 token 是否存在的步骤(基本和 session 相同)
其他参考方案:
- 维护一个 token 黑名单,失效则加入黑名单中。
- 在 JWT 中增加一个版本号字段,失效则改变该版本号。
- 在服务端设置加密的 key 时,为每个用户生成唯一的 key,失效则改变该 key。
注:也就是说,想要实现上述功能,保证 JWT 的无状态原则基本是不可能的(理想很美满,现实很骨感)
# 开始
# 后记
这里顾及篇章,没有提到单点登录(SSO)和第三方登录功能
# 参考链接
- SpringBoot 整合 Spring Security + JWT 实现用户认证 (opens new window)
- Spring Boot 2.X 实战--Spring Security (Token)登录和注册 (opens new window)
- SpringSecurity 实现前后端分离登录 token 认证详解 (opens new window)
- JSON Web 令牌(JWT)详解 - Chen 洋 (opens new window)
- session、cookie、token 的区别? - 掘金 (juejin.cn) (opens new window)
- JSON Web 令牌(JWT)详解 - Chen 洋 - 博客园 (cnblogs.com) (opens new window)
- 还分不清 Cookie、Session、Token、JWT? | 芋道源码 (opens new window)
- JSON Web Token 入门教程 - 阮一峰的网络日志 (ruanyifeng.com) (opens new window)
- jwt - 在退出登录 / 修改密码时怎样实现JWT Token失效? (opens new window)
- How to log out when using JWT. One does not simply log out when using (opens new window)