JSON Web Token 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)
cookie:在前端直接用cookie保存
session:用cookie在前端保存session_id,在后端用session_id保存内容
session在内存中,另外,cookies被获取的话,可能会跨站请求伪造攻击
检查
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
JWT 的原理是,服务器认证以后,生成一个字符串,发回给用户,就像下面这样。
它是一个很长的字符串,没有换行,中间用点(.
)分隔成三个部分。
以后,用户与服务端通信的时候,都要发回这个字符串。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
JWT 的三个部分依次如下
Header.Payload.Signature
元数据被定义为:描述数据的数据,对数据及信息资源的描述性信息。
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
是一个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
Signature 部分是对前两部分的签名,防止数据篡改。
签名的生成:
需要指定一个密钥(secret,一般用随机盐),这个密钥只有服务器才知道。使用 Header 里面指定的签名算法,按照下面的公式产生签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
这里以HMACSHA256加密算法为例
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.
)分隔,就可以返回给用户。
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。这就是 Base64URL 算法。
Base64可以被解码,没有加密
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization
字段里面。
Authorization: Bearer >
另一种做法是,放在 POST 请求的数据体里面。
cookie是和域名有关的,所以不能跨域
- 安全机制把,原理是什么?后端人看见跨域cookie直接不用?
- 自动发送?
(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岂不是很麻烦?就算加密也只能对称吧,浏览器给他加个盐?
前后端通信方式
目的:
总结需求:
数据要有合适的解析格式
数据要有合适的传输格式,要用能跨域、防止攻击的传输方式
要能从jwt读取用户非敏感信息,
不能从jwt读取密钥,所以传输时要加密
为什么不直接传输json、对json加密?为了方便么?都先json数据类型 为什么要用base64?因为数据类型、特殊符号吗?还是说加密算法没法直接加密json和这些特殊符号,所以原始数据要做处理?类似于加一层思想,屏蔽了特殊性?
但是处理的时候json更方便,虽然java创建时传参是
话说java不用json包的话是不是经常用map代替,,都是键值对甚么区别,,
JWTVerifier,指定密钥和创建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的次数,比如最多刷新50次,如果达到50次,则不再允许刷新,需要用户重新授权。
在若依中,似乎没见前端调用refresh接口,,后端要是小于过期时间了就刷新(redis重新设置);
token要是失效了,直接抛异常(redis里查不到token就是失效,不用比较时间);
但是刷新token的比较时间却是直接获取当前登录用户,然后在java内存比较,,比起去redis找会快一些么,,这个用户放不放redis似乎关系不大了,,
access_token
和 refresh_token
,客户端缓存此两种token;access_token
请求接口资源,成功则调用成功;如果token超时,客户端携带 refresh_token
调用token刷新接口获取新的 access_token
;refresh_token
是否过期。如果过期,拒绝刷新,客户端收到该状态后,跳转到登录页;如果未过期,生成新的 access_token
返回给客户端。access_token
重新调用上面的资源接口。access_token
和 refresh_token
失效,同时清空客户端的 access_token
和 refresh_toke
。微信网页授权是通过OAuth2.0机制实现的,也使用了双token方案。
后端实现token过期还可以利用Redis来存储token,设置redis的键值对的过期时间。如果发现redis中不存在token的记录,说明token已经过期了。