在HTTP请求添加名为Authorization的header,形式如下: (token前面
Bearer
是标准前缀字符串)
Authorization: Bearer
Claims
,可以理解为需要保存的信息,加密后会存储在token
中,解密后会从token自动解析出来配合
ant design pro
前端实现需要解决以下问题:
JWT TOKEN
需要存储哪些字段以及定义,如何创建和解析JWT
认证信息JWT TOKEN
过期后,前后端如何配合自动刷新使用库
github.com/golang-jwt/jwt/v5
Claims
定义
jwt.RegisteredClaims
是github.com/golang-jwt/jwt/v5
预设的标准claims
,后面用到其ExpiresAt
过期时间字段,UserClaims
为自定义存储的字段,最终加密到token中的是jwtClaims
结构
type UserClaims struct {
// 可以添加字段存储其他信息
UserID uint `json:"userId"`
RoleID uint `json:"roleId"`
UserName string `json:"username"`
}
type jwtClaims struct {
UserClaims
jwt.RegisteredClaims
}
expireHours
和refreshHours
分别是设置过期时间,以及距离过期时间多久返回新token,后续在gin中间件中判断,方式是在Header
中返回x-refresh-token
:newToken
type JWT struct {
secret []byte
expireHours time.Time
refreshHours time.Duration
}
// j := NewJWT("密钥", 2, 1)
// token过期时间是2小时, 距离过期时间还有1小时时在`Header`中返回`x-refresh-token`:`newToken`
func NewJWT(secret string, expireHours int, refreshHours int) *JWT {
return &JWT{
secret: []byte(secret),
expireHours: time.Now().Add(time.Duration(expireHours) * time.Hour),
refreshHours: time.Hour * time.Duration(refreshHours),
}
}
func (j *JWT) CreateToken(u UserClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtClaims{
UserClaims: u,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(j.expireHours),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
})
return token.SignedString(j.secret)
}
func (j *JWT) ParseToken(tokenString string) (*jwtClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &jwtClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.secret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*jwtClaims); ok && token.Valid {
return claims, nil
} else {
return nil, errors.New("Token无效")
}
}
type BaseResponse struct {
Success bool `json:"success"`
Data any `json:"data"`
Message string `json:"message"`
ErrorMessage string `json:"errorMessage"`
}
func OkWithData(c *gin.Context, data any) {
c.JSON(200, BaseResponse{Success: true, Data: data})
}
func OkWithMsg(c *gin.Context, msg string) {
c.JSON(200, BaseResponse{Success: true, Message: msg})
}
func Fail(c *gin.Context, errMsg string) {
c.JSON(200, BaseResponse{Success: false, ErrorMessage: errMsg})
}
// 使用如下:
// // token过期时间是2小时, 距离过期时间还有1小时时在`Header`中返回`x-refresh-token`:`newToken`
// j := NewJWT("密钥", 2, 1)
// r.USE(JWTAuth(j))
func JWTAuth(j *JWT) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Request.Header.Get("Authorization")
token = strings.TrimPrefix(token, "Bearer ")
claims, err := j.ParseToken(token)
if err != nil {
response.Fail(c, "Token错误: "+err.Error())
c.Abort()
return
}
if time.Since(claims.ExpiresAt.Local()) < j.refreshHours {
newToken, err := j.CreateToken(claims.UserClaims)
if err != nil {
response.Fail(c, "Token刷新错误: "+err.Error())
c.Abort()
return
}
c.Header("x-refresh-token", newToken)
}
c.Set("claims", claims)
c.Next()
}
}
gin中直接调用该函数可以方便的获取到存储的机构体信息
func GetUserToken(c *gin.Context) *UserClaims {
claims, _ := c.Get("claims")
if claims == nil {
return &UserClaims{}
}
return &claims.(*jwtClaims).UserClaims
}
处理登录请求后返回
jwt token
使用localStorage.setItem('jwt', token)
存储起来
app.tsx
的文件request
定义中添加拦截器,实现方法:
- 请求前,在
header
中添加头·Authorization·: 'Bearer ’ + localStorage.getItem(‘jwt’) || ‘’- 请求后,检测
header
中是否存在x-refresh-token
,存在的话更新token,目的是防止token过期自动刷新
实现如下:
function setToken(token: string) {
localStorage.setItem('jwt', token);
}
function getToken(): string {
return localStorage.getItem('jwt') || '';
}
export const request = {
...errorConfig,
// 请求前拦截器
requestInterceptors: [
(url: string, options: RequestConfig) => {
const authHeader = { Authorization: 'Bearer ' + getToken() };
return {
url: `${url}`,
options: { ...options, interceptors: true, headers: authHeader },
};
},
],
// 请求后拦截器
responseInterceptors: [
(response: Response, options: RequestConfig) => {
if (response.headers.has('x-refresh-token')) {
setToken(response.headers.get('x-refresh-token') || '');
}
return response;
},
],
};
jwt_middleware.go
package middleware
import (
"errors"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// 可以添加字段存储其他信息
type UserClaims struct {
UserID uint `json:"userId"`
RoleID uint `json:"roleId"`
UserName string `json:"username"`
}
type jwtClaims struct {
UserClaims
jwt.RegisteredClaims
}
type JWT struct {
secret []byte
expireHours time.Time
refreshHours time.Duration
}
// j := NewJWT("密钥", 2, 1)
// token过期时间是2小时, 距离过期时间还有1小时时在`Header`中返回`x-refresh-token`:`newToken`
func NewJWT(secret string, expireHours int, refreshHours int) *JWT {
return &JWT{
secret: []byte(secret),
expireHours: time.Now().Add(time.Duration(expireHours) * time.Hour),
refreshHours: time.Hour * time.Duration(refreshHours),
}
}
func (j *JWT) CreateToken(u UserClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtClaims{
UserClaims: u,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(j.expireHours),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
})
return token.SignedString(j.secret)
}
func (j *JWT) ParseToken(tokenString string) (*jwtClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &jwtClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.secret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*jwtClaims); ok && token.Valid {
return claims, nil
} else {
return nil, errors.New("Token无效")
}
}
// 使用如下:
// // token过期时间是2小时, 距离过期时间还有1小时时在`Header`中返回`x-refresh-token`:`newToken`
// j := NewJWT("密钥", 2, 1)
// r.USE(JWTAuth(j))
func JWTAuth(j *JWT) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Request.Header.Get("Authorization")
token = strings.TrimPrefix(token, "Bearer ")
claims, err := j.ParseToken(token)
if err != nil {
response.Fail(c, "Token错误: "+err.Error())
c.Abort()
return
}
if time.Since(claims.ExpiresAt.Local()) < j.refreshHours {
newToken, err := j.CreateToken(claims.UserClaims)
if err != nil {
response.Fail(c, "Token刷新错误: "+err.Error())
c.Abort()
return
}
c.Header("x-refresh-token", newToken)
}
c.Set("claims", claims)
c.Next()
}
}
func GetUserToken(c *gin.Context) *UserClaims {
claims, _ := c.Get("claims")
if claims == nil {
return &UserClaims{}
}
return &claims.(*jwtClaims).UserClaims
}
type BaseResponse struct {
Success bool `json:"success"`
Data any `json:"data"`
Message string `json:"message"`
ErrorMessage string `json:"errorMessage"`
}
func OkWithData(c *gin.Context, data any) {
c.JSON(200, BaseResponse{Success: true, Data: data})
}
func OkWithMsg(c *gin.Context, msg string) {
c.JSON(200, BaseResponse{Success: true, Message: msg})
}
func Fail(c *gin.Context, errMsg string) {
c.JSON(200, BaseResponse{Success: false, ErrorMessage: errMsg})
}
main.go
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
j := NewJWT("密钥", 2, 1)
r := gin.Default()
r.POST("/login", func(ctx *gin.Context) {
// 处理登录逻辑返回token, 前端需讲token存储到localstorage
token, err := j.CreateToken(UserClaims{UserID: 1, RoleID: 1, UserName: "Leo"})
if err != nil {
response.Fail(ctx, err.Error())
return
}
response.OkWithData(ctx, gin.H{"token": token})
})
auth := r.Group("/api")
auth.Use(JWTAuth(j))
auth.GET("/test", func(ctx *gin.Context) {
u := GetUserToken(ctx)
response.OkWithMsg(ctx, fmt.Sprintf("用户ID:%d角色ID:%d用户名:%s", u.UserID, u.RoleID, u.UserName))
})
}