JWT 跨域认证解决方案
介绍
全称 JSON Web Token
,分为 Header
、Payload
、Signature
三个部分,应用中,服务端生成一个 JSON
格式的对象,经过加密后生成 Header.Payload.Signature
这样以点分割的字符串,返回给客户端,之后客户端使用这个字符串作为身份凭证与服务器交互。
JWT
可以分布式生成,将用户非核心的信息存入 JWT
中,使服务端不需要单独的维护用户登录凭证,同时不是将用户的凭证存入 Cookie
中,可以有效的防止 CRSF
攻击。
组成
上面说了 JWT
由三部分组成,它们默认是不加密的,使用 Base64URL
算法转为字符串,然后以点分割组成,下面对其一一学习和介绍。
Header
头部Payload
载荷Signature
签名
Base64URL
和Base64
的区别
Base64URL使用的字符集与Base64相同,但是将"+"替换为"-",将"/"替换为"_",并且不包含Base64中的"="用于填充。
例如,一个字符串"Hello, World!"的Base64编码结果是"SGVsbG8sIFdvcmxkIQ==",而相同字符串的Base64URL编码结果是"SGVsbG8sIFdvcmxkIQ"。
Header
头部用于存储 JWT
的元信息,说明使用的类型和签名算法,是一个 JSON
格式对象
头部一般格式如下:
{
"alg": "HS256",
"typ": "JWT"
}
typ
统一为 JWT
、alg
是签名算法
function base64UrlEncode(str) {
let base64 = window.btoa(str);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
var header=base64UrlEncode(JSON.stringify({
"alg": "HS256",
"typ": "JWT"
}));
console.log(header);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
就是服务器返回给客户端的 Header
部分。仔细去观察,很多网站都是这个前缀。
Payload
载荷用于存储我们所需要存放的数据,JWT
规定了7个官方字段,供选用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
也是使用一个 JSON
对象来存储数据,因为默认是不加密的,所以千万不要把用户的密码存储到这里。
function base64UrlEncode(str) {
let base64 = window.btoa(str);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
var data={
"user_id": 12345678,
"username": "xxcheng",
"exp": 1791145041,
"iss": "xxcheng"
}
var payload=base64UrlEncode(JSON.stringify(data))
console.log(payload);
// eyJ1c2VyX2lkIjoxMjM0NTY3OCwidXNlcm5hbWUiOiJ4eGNoZW5nIiwiZXhwIjoxNzkxMTQ1MDQxLCJpc3MiOiJ4eGNoZW5nIn0
eyJ1c2VyX2lkIjoxMjM0NTY3OCwidXNlcm5hbWUiOiJ4eGNoZW5nIiwiZXhwIjoxNzkxMTQ1MDQxLCJpc3MiOiJ4eGNoZW5nIn0
就是载荷部分,通过简单的 base64
转换就可以获取原来数据,千万不能存隐私数据。
Signature
签名是对签名两个部分的签名进行签名防止被篡改,需要提供一个密钥用于加盐,这个密钥必须保存好,不能泄露。
function base64UrlEncode(str) {
let base64 = window.btoa(str);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
var secret="xxcheng"
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
// qQBFCMuZLeQoirXhJ-VVOKcnEgDqXcU5QytR8oYXWRI
分类
Token
可以分为两种:Access Token
和 Refresh Token
,利用 Refresh Token
,在 Access Token
要过期后,重新生成一个新的 Access Token
返回给前端。Access Token
的有效期较短,Refresh Token
用于签发新的 Access Token
,有效期较长。这样子可以防止 Access Token
泄露,而无法废止的弊端(或者废止成本高,又回到了之前需要集中统一管理的情况,Refresh Token
虽然可能需要建个授权服务器,但是它不服务那些高负载的校验请求,只服务更新 Token
这一服务)。
Go
实现
使用 github.com/dgrijalva/jwt-go
这个库实现
Payload
先定义一个用于存储数据的载荷结构体,同时 jwt-go
为我们一个官方的七种的载荷的结构体 StandardClaims
。
type MyClaims struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
jwt.StandardClaims
}
信息配置
// 有效期
const JWTTimeExpireDuration = time.Minute * 1
// 加盐密钥
var secret = []byte("ZyRyJoZzz2ygTE1M8qOW0XFGcSulFRo40Fk871lYQp810RTW")
// 签发人
const Issuer = "xxcheng"
生成
func Test_GenerateToken(t *testing.T) {
mc := &MyClaims{
UserID: 12345678,
Username: "jpc",
StandardClaims: jwt.StandardClaims{
//使用UUID生成的一个,用于保证唯一
Id: "d1414649-5a85-4a10-b1c5-befc1ac005d3",
//设置过期时间
ExpiresAt: time.Now().Add(JWTTimeExpireDuration).Unix(),
//签发人
Issuer: Issuer,
},
}
//选择头部信息和载荷
//此时是未加密的
token := jwt.NewWithClaims(jwt.SigningMethodHS256, mc)
//打印头部
fmt.Printf("%T,%#v\n", token.Header, token.Header)
//打印载荷
fmt.Printf("%T,%#v\n", token.Claims, token.Claims)
tokenStr, _ := token.SignedString(secret)
fmt.Println("------")
fmt.Println(tokenStr)
}
map[string]interface {},map[string]interface {}{"alg":"HS256", "typ":"JWT"}
*jwt.MyClaims,&jwt.MyClaims{UserID:12345678, Username:"jpc", StandardClaims:jwt.StandardClaims{Audience:"", ExpiresAt:1691150388, Id:"d1414649-5a85-4a10-b1c5-befc1
ac005d3", IssuedAt:0, Issuer:"xxcheng", NotBefore:0, Subject:""}}
------
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NTY3OCwidXNlcm5hbWUiOiJqcGMiLCJleHAiOjE2OTExNTAzODgsImp0aSI6ImQxNDE0NjQ5LTVhODUtNGExMC1iMWM1LWJlZmMxYWMwMDVkMyIsImlzcyI6Inh4Y2hlbmcifQ.dtSHOqI9ovFFQMJ-2y4_ybrwZTXMy31zzxfun4SluTc
解析
func Test_ParseToken(t *testing.T) {
mc := new(MyClaims)
tokenStr := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NTY3OCwidXNlcm5hbWUiOiJqcGMiLCJleHAiOjE2OTExNTAzODgsImp0aSI6ImQxNDE0NjQ5LTVhODUtNGExMC1iMWM1LWJlZmMxYWMwMDVkMyIsImlzcyI6Inh4Y2hlbmcifQ.dtSHOqI9ovFFQMJ-2y4_ybrwZTXMy31zzxfun4SluTc"
_, err := jwt.ParseWithClaims(tokenStr, mc, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil {
v, _ := err.(*jwt.ValidationError)
switch v.Errors {
case jwt.ValidationErrorExpired:
fmt.Println("过期~~~")
}
return
}
fmt.Println(mc)
}
过期~~~
上面的 Token
过期了,生成一个新的重新测试
&{12345678 jpc { 1691150871 d1414649-5a85-4a10-b1c5-befc1ac005d3 0 xxcheng 0 }}
参考链接
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。