JWT基础概念
JWT是
json web token
的简称其中的 token 是令牌的意思, 其实这个令牌实质上是服务端生成的一段有规则的字符串
我们看看JWT官方自己对其的定义
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.
我们提炼出重点信息:
- JWT是一个开放的标准
- jwt本身体积比较紧凑,所以传输速度比较快
- jwt可以放在URL中传递,可以放在请求头中传递,可以放在请求体中传递
- jwt的
有效载荷
中包含一些自定义的有效信息,在某些场景中可以避免部分数据库查询
JWT使用场景
-
授权 : jwt最常见的使用场景,用户端首先登陆成功,从服务端获取jwt(令牌),那么用户后面的所有的请求中都应该包含这个令牌,服务端通过这个令牌判断允许用户的权限和访问的资源,服务.
基于这样的特点可以做单点登录(SingleSignOn,SSO)
基于此也可以做跨域认证
信息交换 : 通讯双方通过jwt可以传递信息, 信息都是签名之后,可以防止伪造
JWT的结构
如下是一个标准的JWT
JWT 是有三部分组成的,每一部分之间通过 .
(点号) 隔开
header 头部
Payload 载荷
Signature 签名
那么JWT的格式如下 :
header
.
Payload
.
Signature
头部
标头是一个json对象通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。
{
"alg": "HS256",
"typ": "JWT"
}
这一部分的json内容被 Base64Url 编码之后成为第一部分
载荷
载荷也是一个json对象,是实际承载传递数据的部分,JWT提供了7个预定义好字段可以按需使用
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了这些预定义的字段,我们可以在payload中添加自己的'私货'
,添加一些自己字段
{
"name":"admin",
"pwd":"123456"
}
有了有效载荷之后再对有效载荷进行 Base64Url 编码 ,编码之后的这部分就是 jwt 的第二部分
tips : 尽量不要将很重要和私密的信息放在其中,因为这部分解码之后是可见的
签名
签名就是将前面的编码之后header
和 编码之后的 payload
再加上秘钥secretKey
通过指定的加密算法创建出来的(签名默认算法是 HMAC SHA256)
创建签名的公式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secretKey)
通常认为签名的作用是防数据被篡改
JWT工作原理
一般而言我们携带JWT发送请求可以放在HTTP请求头中的
Authorization
中, 格式如下Authorization: Bearer <token>
tips: 我们前面说了jwt 这个令牌其实也可以放在 HTTP的url中,也可以在HTTP请求的请求报文中
当 token在HTTP的请求头中的
Authorization
中发送时可以解决CORS
的问题
Golang使用JWT
模拟使用场景 :
- 新建http服务,提供三个处理接口
/auth
,/home
,/list
/auth
接口处理用户登陆验证,并返回 token (jwt)/home
接口处理登陆成功的用户都能访问的 home服务/list
接口处理登陆成功的用户并且用户权限是admin
才能访问的 list服务tips : 这里的http服务和JWT 的 Authorization server(认证服务) 在同一服务器上,在实际开发中 Authorization server(认证服务) 可以单独部署
代码实现
Golang中有很多关于jwt的包,我们使用如下包
# 安装依赖包
go get github.com/dgrijalva/jwt-go
JWT服务的关键代码如下
// JWT中的payload中不要放重要数据,因为这部分数据通过Base64URL算法能反解出来
type Claims struct {
// 自定义的`私有`数据,在payload中
UserAccount
// jwt的标准的claims
jwt.StandardClaims
}
// 生成token
func GetToken(name, password, role string) (string, error) {
c := Claims{
UserAccount: UserAccount{
Username: name,
Password: password,
Role: role,
},
StandardClaims: jwt.StandardClaims{
// token过期时间
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
// 签发人
Issuer: "captain",
// 主题
Subject: "jwt test",
},
}
// 生成token,默认采用HMAC SHA256
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 加上签名(需要用到秘钥),生成完整的token
return token.SignedString(SecretKey)
}
// 解析token
func ParseToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (i interface{}, err error) {
return SecretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
此处我们不用框架,直接使用golang一些标准包构建一个http服务,并且集成JWT服务
http服务(包含JWT服务)端完整代码
jwt.go
package main
import (
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
"log"
"net/http"
"strings"
"time"
)
const (
// 定义token的有效时间
TokenExpireDuration = time.Hour * 1
)
var SecretKey = []byte("123456")
// 请求的账户信息
type UserAccount struct {
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
}
// 响应给客户端的数据
type ResponseToClient struct {
Code string `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// JWT中的payload中不要放重要数据,因为这部分数据通过Base64URL算法能反解出来
type Claims struct {
// 自定义的`私有`数据,在payload中
UserAccount
// jwt的标准的claims
jwt.StandardClaims
}
// 生成token
func GetToken(name, password, role string) (string, error) {
c := Claims{
UserAccount: UserAccount{
Username: name,
Password: password,
Role: role,
},
StandardClaims: jwt.StandardClaims{
// token过期时间
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
// 签发人
Issuer: "captain",
Subject: "jwt test",
},
}
// 生成token,默认采用HMAC SHA256
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 加上签名(需要用到秘钥),生成完整的token
return token.SignedString(SecretKey)
}
// 解析token
func ParseToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (i interface{}, err error) {
return SecretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
func WriteToResponse(w http.ResponseWriter, code, message string, data interface{}) {
var resp ResponseToClient
resp.Code = code
resp.Message = message
resp.Data = data
respJson, _ := json.Marshal(resp)
// 设置响应为json格式
w.Header().Set("Content-Type", "application/json;charset=utf-8")
fmt.Fprintf(w, "%v\n", string(respJson))
}
// 默认处理函数
func defaultFunc(w http.ResponseWriter, r *http.Request) {
}
// 模拟用户登陆接口,响应json数据
// 目的是让客户端从服务器端获取token
// 处理请求逻辑之后,响应给客户的数据中包含新生成的token值
func AuthFunc(w http.ResponseWriter, r *http.Request) {
var user UserAccount
// 读取客户端请求的类容
buf := make([]byte, 2048)
n, _ := r.Body.Read(buf)
// debug 调试请求的内容
log.Println("json :", string(buf[:n]))
// 将请求json数据解析出来
err := json.Unmarshal(buf[:n], &user)
// 如果解析错误,给客户端提示
if err != nil {
WriteToResponse(w, "400", err.Error(), "")
return
}
// debug 调试解析之后的内容
log.Println(user)
// 模拟验证账户登录的逻辑(账户,密码都正确)
if user.Username == "admin" && user.Password == "123456" {
tokenString, _ := GetToken(user.Username, user.Password, user.Role)
WriteToResponse(w, "200", "success", map[string]string{"token": tokenString})
return
} else {
WriteToResponse(w, "400", "Account error", "")
return
}
// 通过命令行的 CURL 测试
// curl -X POST -H -H "Content-type:application/json" -d '{"username":"admin","password":"123456","role":"admin"}' http://127.0.0.1:8080/auth
}
// 模拟用户登陆(获取token)之后再请求某个接口,响应json数据
// 请求时,在请求的数据中包含token
// 包含token的载体可以是请求的url,也可以是请求头,也可以是请求体
func homeFunc(w http.ResponseWriter, r *http.Request) {
// 此处我们模拟的token包含在请求头信息中
authorH := r.Header.Get("Authorization")
if authorH == "" {
WriteToResponse(w, "401", "request header Authorization is null", "")
return
}
// 将获取的Authorization 内容通过分割出来
authorArr := strings.SplitN(authorH, " ", 2)
// debug
log.Println(authorArr)
// Authorization的字符串通常是 "Bearer" 开头(可以理解为固定格式,标识使用承载模式),然后一个空格 再加上token的内容
// Tips: 请求头中Authorization的内容直接是token也是可以的
if len(authorArr) != 2 || authorArr[0] != "Bearer" {
WriteToResponse(w, "402", "request header Authorization formal error", "")
return
}
// 解析token这个字符串
mc, err := ParseToken(authorArr[1])
if err != nil {
WriteToResponse(w, "403", err.Error(), "")
return
}
// debug
log.Println(mc)
// 请求成功响应给客户端
WriteToResponse(w, "200", "welcome to home", "")
}
func listFunc(w http.ResponseWriter, r *http.Request) {
authorH := r.Header.Get("Authorization")
if authorH == "" {
WriteToResponse(w, "401", "request header Authorization is null", "")
return
}
authorArr := strings.SplitN(authorH, " ", 2)
// debug
log.Println(authorArr)
if len(authorArr) != 2 || authorArr[0] != "Bearer" {
WriteToResponse(w, "402", "request header Authorization formal error", "")
return
}
mc, err := ParseToken(authorArr[1])
if err != nil {
WriteToResponse(w, "403", err.Error(), "")
return
}
// 模拟通过jwt确定权限的逻辑
// 如果 用户角色是admin,那么就能访问该接口,否则就不允许
if mc.Role == "admin" {
WriteToResponse(w, "200", "列表数据", "")
return
}
WriteToResponse(w, "404", "无权限访问", "")
}
func main() {
http.HandleFunc("/", defaultFunc)
http.HandleFunc("/auth", AuthFunc)
http.HandleFunc("/home", homeFunc)
http.HandleFunc("/list", listFunc)
fmt.Println("start http server and listen 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("ListenAndServer err : ", err)
}
}
# 运行jwt服务
$ go run jwt.go
start http server and listen 8080
服务测试
测试工具是直接使用命令行下的 curl命令工具进行的测试
tips: 当然也可以使用其他的任何能发送http请求的工具进行测试(包括使用代码编写http客户端)
测试登陆验证服务
# 发送POST并携带json数据
curl -X POST -v -H "Content-type:application/json" -d '{"username":"admin","password":"123456","role":"edit"}' http://127.0.0.1:8080/auth
# 响应数据
> POST /auth HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.69.1
> Accept: */*
> Content-type:application/json
> Content-Length: 54
>
} [54 bytes data]
* upload completely sent off: 54 out of 54 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=utf-8
< Date: Sun, 05 Apr 2020 09:12:56 GMT
< Content-Length: 266
<
// 响应的json数据 包含了token
{"code":"200","message":"success","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4NDUzMCwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.VNIk9nI8SStCMCI_QyJ8gLrUbOLNSQgeoVabjQFzMS0"}}
测试访问 home 服务
# 无token请求
curl -X POST -H "Content-type:application/json" http://127.0.0.1:8080/home
# 响应
{"code":"401","message":"request header Authorization is null","data":""}
# 正确的请求
curl -X POST -H "Content-type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4MTk3NSwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.SJiK_bN7nQHFyRRTjWrNcX4IsuUkFasei21NU4FzI3U" http://127.0.0.1:8080/home
# 响应
{"code":"200","message":"welcome to home","data":""}
测试访问 list 服务
# 请求
curl -X POST -H "Content-type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4MTk3NSwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.SJiK_bN7nQHFyRRTjWrNcX4IsuUkFasei21NU4FzI3U" http://127.0.0.1:8080/list
# 响应
{"code":"404","message":"无权限访问","data":""}
参考资料
有疑问加站长微信联系(非本文作者)