一.概念
jwt,全名(json web token),是一种跨域的认证的解决方案,属于一个开放的标准。使用其规定了一种token的实现方式。
二.为什么使用
传统的的web项目,使用的都是session来认证用户的信息,具体的流程如下:
1.用户通过浏览器将账号跟密码传输给后台服务。
2.服务端对用户跟密码校验后会生成一份保存当前用户信息的session和一个对应的session_id。
3.如果返回响应的时候会将生成的session_id一并返回,浏览器会将该值写入cookie中。
4.如果用户再次通过该浏览器访问服务时,都会携带含有session_id的cookie信息至服务端。
5.服务端接收到请求后,利用cookie中session_id查找对应的账号相关的session信息,从而再次验证当前请求。
6.对于非浏览器的客户端、手机移动端等不适用,因为session依赖于cookie,而移动端经常没有cookie因为session认证本质基于cookie,所以如果cookie被截获,用户很容易收到跨站请求伪造攻击。并且如果浏览器禁用了cookie,这种方式也会失效,前后端分离系统中更加不适用,后端部署复杂,前端发送的请求往往经过多个中间件到达后端,cookie中关于session的信息会转发多次。cookie无法跨域,不适用于单点登录。
7.过程图;
存在的缺陷:
1.极度依赖浏览器保存cookie机制。并且再服务端,需要为每一个账号都创建一个session来保存相关信息。(存储消耗较大,如果用户量大,分布式存储也会持续的增大并且维护力度增加)。
2.用户登录方式多样化(不可能总是同一个浏览器,限制了*度),例如用户使用第三方或者app来访问。如果再次使用cookie模式,就会很是有1中的问题,并且也不灵活。
三,jwt的使用
1.过程图:
2.结构:
jwt-token由三部分组成,header + payload + signature组成。公式如下:
JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
header部分:
是一个json对象:
{
"typ": "JWT",
"alg": 加密方法,例如(HS256)
}
payload:
有效载荷部分,jwt主题部分,
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除此之外,可以可以根据需要自定义相关的信息(不要填充保密信息,因为该部分在加密时,仅仅用base64直接加密,可以直接反编码出来),如下:
{
"user_id": 0
"username":"",
}
例如,一段使用go填充的结构:
cla := MyClaims{
username,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
Issuer: "lx-jwt", // 签发人
},
}
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
Signature:
签名是对上面两部分的数据进行签名,过程是使用base64编码后的数据,通过制定的算哈生成哈希(不可逆),这样用于确保数据不会被篡改。另外,还需要一个密钥secret,该secret仅仅保存在服务端,并且不可公开。公式如下:
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
最终生成的token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VyTmFtZSI6Imx4IiwiZXhwIjoxNjQyODI3ODgxLCJpc3MiOiJseC1qd3QifQ.gWuzFlSSBNC8bzR-txHkHpTtOUd9ELNFczdAiwXicVY
用户每次登陆后,通过从服务端获取对应的token,从而通过认证,在具体请求中携带服务端授予的token,服务端,通过对token的解析验证,确保本次起请求的正确。
3.使用go的go-jwt实现的一个模拟获取token以及请求认证过程:
jwt_use.go
package jwt_use
import (
"errors"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
const TokenExpireDuration = time.Hour * 2
// const TokenExpireDuration = time.Second * 60
var Secret = []byte("人生路漫漫")
type MyClaims struct {
UserName string
jwt.StandardClaims
}
// get token
func GetToken(username string) (string, error) {
cla := MyClaims{
username,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
Issuer: "lx-jwt", // 签发人
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cla)
fmt.Println("Token = ",token)
return token.SignedString(Secret) // 进行签名生成对应的token
}
// parse token
func ParseToken(tokenString string) (*MyClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return Secret, nil
})
if err != nil {
return nil, err
}
if claims,ok := token.Claims.(*MyClaims);ok && token.Valid {
return claims,nil
}
return nil,errors.New("invalid token")
}
user.go
package model
type UserInfo struct {
UserName string `json:"user_name" form:"user_name"`
PassWord string `json:"pass_word" form:"pass_word"`
}
main.go
package main
import (
"encoding/base64"
"fmt"
"net/http"
"strconv"
"strings"
"git.code.oa.com/my-jwt/jwt_use"
"git.code.oa.com/my-jwt/model"
"github.com/gin-gonic/gin"
"github.com/unrolled/secure"
)
// 通过鉴权账户,并生成对应的token进行返回
func authHandler(c *gin.Context) {
// tokenString, _ := jwt_use.GetToken("lx")
// c.JSON(200, gin.H{"code": 0, "msg": "success", "data": gin.H{"token": tokenString}})
// return
user := &model.UserInfo{}
err := c.ShouldBindJSON(user)
if err != nil {
c.JSON(200, gin.H{"code": 2001, "msg": "invalid params"})
return
}
// 检查人员是否存在,并为其生成一个token,单点登陆友好选择jwt
if user.UserName == "lx" && user.PassWord == "123qweasd" {
tokenString, _ := jwt_use.GetToken(user.UserName)
c.JSON(200, gin.H{"code": 0, "msg": "success", "data": gin.H{"token": tokenString}})
return
}
c.JSON(200, gin.H{"code": 2002, "msg": "鉴权失败"})
return
}
// 中间件,认证token合法性
func jwtAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHandler := c.Request.Header.Get("authorization")
if authHandler == "" {
c.JSON(200, gin.H{"code": 2003, "msg": "请求头部auth为空"})
c.Abort()
return
}
// 前两部门可以直接解析出来
jwt := strings.Split(authHandler, ".")
cnt := 0
for _, val := range jwt {
cnt++
if cnt == 3 {
break
}
msg, _ := base64.StdEncoding.DecodeString(val)
fmt.Println("val ->", string(msg))
}
// 我们使用之前定义好的解析JWT的函数来解析它,并且在内部解析时判断了token是否过期
mc, err := jwt_use.ParseToken(authHandler)
if err != nil {
fmt.Println("err = ", err.Error())
c.JSON(http.StatusOK, gin.H{
"code": 2005,
"msg": "无效的Token",
})
c.Abort()
return
}
// 将当前请求的username信息保存到请求的上下文c上
c.Set("username", mc.UserName)
c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信息
}
}
//
func homeHandler(c *gin.Context) {
username := c.MustGet("username").(string)
c.JSON(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": gin.H{"username": username},
})
}
// ssh加密验证
func TSSHandler(port int) gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("start ->")
secureMiddleware := secure.New(secure.Options{
SSLRedirect: true,
SSLHost: ":" + strconv.Itoa(port),
})
err := secureMiddleware.Process(c.Writer, c.Request)
if err != nil {
fmt.Println("err = ", err.Error())
return
}
fmt.Println("+ err = ", err)
c.Next()
fmt.Println("success")
}
}
func main() {
r := gin.Default()
// r.Use(TSSHandler(8080))
r.POST("/auth", authHandler)
r.GET("/home", jwtAuthMiddleware(), homeHandler)
fmt.Println("ser is running")
r.Run(":18243") // http
// r.RunTLS(":8080","cert.pem", "key.pem") // https
}