JSON Web Token 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)
- 补充上时间线?画图?
- 隐患是什么
- 为什么一开始不这么做
- 这个封面挺好做的,以后笔记我也做一个,,要是能自动生成就好了
一、认证
- 为了保存信息用的,除了认证信息还有些其他信息
cookie:在前端直接用cookie保存
session:用cookie在前端保存session_id,在后端用session_id保存内容
session在内存中,另外,cookies被获取的话,可能会跨站请求伪造攻击
- 获取token会怎么样?不会跨站?
- cookie网络传输时是明文吗?
- 和cookie相比,是在服务器端验证了下 token是否存在么,,纯cookie模式就是cookie发啥我都信
- 怎么验证?
- 放在Authorization可以防止XSS和XSRF?
检查
- 签名是否有效
- 是否过期
- 接受方是否是自己
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
- 单点失败?
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
JWT简介
原理
JWT 的原理是,服务器认证以后,生成一个字符串,发回给用户,就像下面这样。
它是一个很长的字符串,没有换行,中间用点(.
)分隔成三个部分。
- 怎么是json对象,方便前端使用么?话说这是认证用的,还指望用里面的信息?
- 后端会再转换成json对象么?还是说一开始是个json对象,一开始这个怎么来的,为什么是少见的后端先生成JSON,因为后端先拿到数据么
以后,用户与服务端通信的时候,都要发回这个字符串。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
- 无状态概念!
JWT 的三个部分依次如下
Header.Payload.Signature
元数据被定义为:描述数据的数据,对数据及信息资源的描述性信息。
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
payload 负载
是一个JSON 对象, 用来存放实际需要传递的数据,形如:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
其中payload官方规定了7个字段:
-
iss (issuer):签发人
-
exp (expiration time):过期时间
-
sub (subject):主题
-
aud (audience):受众
-
nbf (Not Before):生效时间
-
iat (Issued At):签发时间
-
jti (JWT ID):编号
除官方字段外,也可以直接使用私有字段(就像js对象的成员一样,毕竟是json数据),如之前例子中的admin
注意,JWT 默认是不加密的,不要把机密信息放在这个部分。Base64URL虽然人类难以看懂,但不是加密doge
- 可逆么?不可逆是不是和md5类似
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
签名的生成:
需要指定一个密钥(secret,一般用随机盐),这个密钥只有服务器才知道。使用 Header 里面指定的签名算法,按照下面的公式产生签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
这里以HMACSHA256加密算法为例
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.
)分隔,就可以返回给用户。
- 为什么又把header和payload编码再加密?不能直接加密?只能从字符串加密?
- 怎么解密?能解密的话还要前两部分干什么?
3.4 Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。这就是 Base64URL 算法。
Base64可以被解码,没有加密
JWT 的使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization
字段里面。
Authorization: Bearer <token>
另一种做法是,放在 POST 请求的数据体里面。
cookie是和域名有关的,所以不能跨域
- 安全机制把,原理是什么?后端人看见跨域cookie直接不用?
- 自动发送?
JWT 的几个特点
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
- 自包含:自己包含用户需要的信息,不需要查询数据库
验签:
服务端拿到jwt后,将其中的header和payload取出来,和自己服务端保存的secret放一起计算一次signature,如果计算出来的signature和之前保存的signature一样,则说明数据没有收到更改
如果中间人改了三部分中其中一项,将导致验签时计算出来的signature和之前不一样,无法认证。如果中间人修改了signature,直接就不用算就能知道被修改了。(发来的签名和现场计算的签名要和保存的签名一样。jwt认证用的,携带的信息不会在前端更改)
这就是为什么signature要header.payload.secret一起加密。我们需要header、payload中的信息,所以只是编码;而用于认证的加密则要三项信息都没有被修改。
-
这是防修改的方式,和计网有点儿像?
-
如果被人拿到了token假装身份,解决方式是超时淘汰?
-
是不是安全要考虑防修改、防伪装、防查看?这个是传递的死数据,不超时就不会主动修改,那会主动修改的知道有没有被中间人修改?
-
防查看这里倒是直接不写认证信息,,,要是cookie的话,不会要每次把密码发上去查查数据库把,,或者登录一次获取用户名cookie,然后谁拿着这个cookie就一直用?
-
密码保存到cookie岂不是很麻烦?就算加密也只能对称吧,浏览器给他加个盐?
需求分析
前后端通信方式
- 不能用cookie,跨域
- 放在header的Authorization中
- 需要传递编码后的字符串数据,能有+、换行吗?
- 所以用base64url
- 需要传递编码后的字符串数据,能有+、换行吗?
目的:
- 不敏感的用户信息能读取
- 所以不能更改
- 所以用base64url,可以还原信息
- 认证的secret要加密,密钥不能让人读取
- 要加密
- 要保证所有数据不能被中间人更改,因为后端还要读取这些信息
- 要一起加密
总结需求:
-
数据要有合适的解析格式
-
数据要有合适的传输格式,要用能跨域、防止攻击的传输方式
-
要能从jwt读取用户非敏感信息,
- 因此这部分数据不能被篡改
-
不能从jwt读取密钥,所以传输时要加密
为什么不直接传输json、对json加密?为了方便么?都先json数据类型 为什么要用base64?因为数据类型、特殊符号吗?还是说加密算法没法直接加密json和这些特殊符号,所以原始数据要做处理?类似于加一层思想,屏蔽了特殊性?
但是处理的时候json更方便,虽然java创建时传参是<String, Objecr>map,
- 这种map对应json?json是不是全字符串?还是说js数据类型?
话说java不用json包的话是不是经常用map代替,,都是键值对甚么区别,,
JWT验证对象
JWTVerifier,指定密钥和创建jwt时一样
常见异常
按触发顺序:
- 算法不匹配异常
- 签名不匹配异常
- 过期异常
- payload异常(payload数据不对劲儿,可能被修改)
可以看出是先验签再看数据
- https不会被拦截?
springboot使用JWT
封装成工具类
这里是ruoyi-cloud中的jwt工具类,省略了导包
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.constant.TokenConstants;
import com.ruoyi.common.core.text.Convert;
import io.jsonwebtoken.Jwts;
public class JwtUtils
{
public static String secret = TokenConstants.SECRET;
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
public static String createToken(Map<String, Object> claims)
{
String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public static Claims parseToken(String token)
{
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
/**
* 根据令牌获取用户标识
*
* @param token 令牌
* @return 用户ID
*/
public static String getUserKey(String token)
{
Claims claims = parseToken(token);
return getValue(claims, SecurityConstants.USER_KEY);
}
/**
* 根据令牌获取用户标识
*
* @param claims 身份信息
* @return 用户ID
*/
public static String getUserKey(Claims claims)
{
return getValue(claims, SecurityConstants.USER_KEY);
}
/**
* 根据令牌获取用户ID
*
* @param token 令牌
* @return 用户ID
*/
public static String getUserId(String token)
{
Claims claims = parseToken(token);
return getValue(claims, SecurityConstants.DETAILS_USER_ID);
}
/**
* 根据身份信息获取用户ID
*
* @param claims 身份信息
* @return 用户ID
*/
public static String getUserId(Claims claims)
{
return getValue(claims, SecurityConstants.DETAILS_USER_ID);
}
/**
* 根据令牌获取用户名
*
* @param token 令牌
* @return 用户名
*/
public static String getUserName(String token)
{
Claims claims = parseToken(token);
return getValue(claims, SecurityConstants.DETAILS_USERNAME);
}
/**
* 根据身份信息获取用户名
*
* @param claims 身份信息
* @return 用户名
*/
public static String getUserName(Claims claims)
{
return getValue(claims, SecurityConstants.DETAILS_USERNAME);
}
/**
* 根据身份信息获取键值
*
* @param claims 身份信息
* @param key 键
* @return 值
*/
public static String getValue(Claims claims, String key)
{
return Convert.toStr(claims.get(key), "");
}
}
重点总结:
- 密钥作为静态变量,导入
有用auth0包下的jwt的,也是工厂模式,方法名格式是create和withxxx
验证是给框架或者拦截器
JWT标准里面定义的标准claim包括:
iss(Issuser)
:JWT的签发主体;sub(Subject)
:JWT的所有者;aud(Audience)
:JWT的接收对象;exp(Expiration time)
:JWT的过期时间;nbf(Not Before)
:JWT的生效开始时间;iat(Issued at)
:JWT的签发时间;jti(JWT ID)
:是JWT的唯一标识。
token过期的续期方案
单token方案
- 将 token 过期时间设置为15分钟;
- 前端发起请求,后端验证 token 是否过期;如果过期,前端发起刷新token请求,后端为前端返回一个新的token;
- 前端用新的token发起请求,请求成功;
- 如果要实现每隔72小时,必须重新登录,后端需要记录每次用户的登录时间;用户每次请求时,检查用户最后一次登录日期,如超过72小时,则拒绝刷新token的请求,请求失败,跳转到登录页面。
另外后端还可以记录刷新token的次数,比如最多刷新50次,如果达到50次,则不再允许刷新,需要用户重新授权。
在若依中,似乎没见前端调用refresh接口,,后端要是小于过期时间了就刷新(redis重新设置);
token要是失效了,直接抛异常(redis里查不到token就是失效,不用比较时间);
但是刷新token的比较时间却是直接获取当前登录用户,然后在java内存比较,,比起去redis找会快一些么,,这个用户放不放redis似乎关系不大了,,
双token方案
- 登录成功以后,后端返回
access_token
和refresh_token
,客户端缓存此两种token; - 使用
access_token
请求接口资源,成功则调用成功;如果token超时,客户端携带refresh_token
调用token刷新接口获取新的access_token
; - 后端接受刷新token的请求后,检查
refresh_token
是否过期。如果过期,拒绝刷新,客户端收到该状态后,跳转到登录页;如果未过期,生成新的access_token
返回给客户端。 - 客户端携带新的
access_token
重新调用上面的资源接口。 - 客户端退出登录或修改密码后,注销旧的token,使
access_token
和refresh_token
失效,同时清空客户端的access_token
和refresh_toke
。
微信网页授权是通过OAuth2.0机制实现的,也使用了双token方案。
微信网页授权方案
- 用户在第三方应用的网页上完成微信授权以后,第三方应用可以获得 code(授权码)。code的超时时间为10分钟,一个code只能成功换取一次access_token即失效。
- 第三方应用通过code获取网页授权凭证access_token和刷新凭证 refresh_token。
- access_token是调用授权关系接口的调用凭证,由于access_token有效期(2个小时)较短,当access_token超时后,可以使用refresh_token进行刷新。
- refresh_token拥有较长的有效期(30天),当refresh_token失效的后,需要用户重新授权。
后端实现token过期还可以利用Redis来存储token,设置redis的键值对的过期时间。如果发现redis中不存在token的记录,说明token已经过期了。