• 登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后(我会将这个token存贮到localStorage中,保证刷新页面后能记住用户登录状态),前端会根据token再去拉取一个 userInfo 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
  • 权限验证:通过token获取用户对应的菜单权限列表和按钮去权限列表,动态算出其对应的权限路由,通过 router.addRoute动态挂载这些路由。

https://blog.csdn.net/qq_63880817/article/details/133584436 讲述了如何在 Gin 中生成 token 并使用通过 Headers 传递过来的 Token https://blog.csdn.net/weixin_48255917/article/details/110622736 这个讲述了如何用 Vue 和后端的 Token 做交互

JWT 的一个问题是即使用户选择退出登录,之前生成的 Token 也是有效的,如果有人提前保存了 Token 那么还是可以使用。 这个时候一个有效的做法是维护一个过期时间等于 JWT 过期时间的列表,如果发送过来的 Token 和在维护的列表里面,就认为 Token 失效。 在退出登录的时候,将这个 Token 添加到列表中,这样就可以解决退出登录就失效的需求

下面是具体实现

GIN 部分

如何生成 token

package gin_server
 
import (
	"fmt"
 
	"github.com/dgrijalva/jwt-go"
)
 
// JWT 定义一个jwt对象;可以是其他命名,并非是前面所说的token类型,注意不要搞混乱了,想象成java中一个有多种方法类,
type JWT struct {
	SigningKey []byte
}
 
// NewJWT 初始化jwt对象,固定返回一个为123456的密钥,(基于你的项目实际需要
func NewJWT() *JWT {
	return &JWT{
		[]byte("project_name"),
	}
}
 
// JWTClaims 定义自己的负载
type JWTClaims struct {
	Name string `json:"name"`
	Role string `json:"role"`
	// StandardClaims结构体实现了Claims接口(Valid()函数)
	jwt.StandardClaims
}
 
// CreatToken 进行构造一个token,参数为接受到的负载
func (j *JWT) CreatToken(claims JWTClaims) (string, error) {
	// 指定编码的算法为jwt.SigningMethodHS256,返回一个token的指针
	token :*** jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(j.SigningKey)
}
 
// ParseToken 进行解析一个token
func (j *JWT) ParseToken(tokenString string) (*JWTClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.SigningKey, nil
	})
	//错误处理 ,使更健壮性、易懂性
	if err != nil {
		// jwt.ValidationError 是一个无效token的错误结构
		if ve, ok := err.(*jwt.ValidationError); ok {
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				// ValidationErrorMalformed是一个uint常量,表示token格式错误
				return nil, fmt.Errorf("token格式错误")
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				//ValidationErrorExpired表示token过期
				return nil, fmt.Errorf("token过期")
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				//ValidationErrorNotValidYet 表示token生效时间未到
				return nil, fmt.Errorf("token生效时间未到")
			} else {
				return nil, fmt.Errorf("token 不可用")
			}
		}
	}
	// 将token中的claims信息解析出来并断言成用户自定义的有效载荷结构
	if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, fmt.Errorf("token无效")
}
 

如何在登录的时候设置 Token

 
func login(c *gin.Context) {
	var user User
	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
 
	validUser, err := validateUser(user.Name, user.Password)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user_name or password"})
		return
	}
 
	wt := NewJWT()
 
	claims := JWTClaims{
		Name: validUser.Name,
		Role: string(validUser.Role),
		StandardClaims: jwt.StandardClaims{
			NotBefore: int64(time.Now().Unix() - 1000), // 签名生效时间
			ExpiresAt: int64(time.Now().Unix() + 3600), // 签名过期时间
			IssuedAt:  int64(time.Now().Unix()),
			Issuer:    "签名颁发者",
		},
	}
 
	token, err := wt.CreatToken(claims)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": -1,
			"msg":  "token生成失败",
		})
		return
	}
 
	c.JSON(http.StatusOK, map[string]interface{}{"code": 0, "msg": "success", "data": gin.H{"token": token}})
}

如何在中间件验证 token

 
// JWTAuth define the midWare to check whether the JWT is valid
func JWTAuth(c *gin.Context) {
	if c.Request.URL.Path == "/api/login" {
		c.Next()
		return
	}
 
	comm.Log.Infof("JWTAuth: %s", c.Request.URL.Path)
 
	//从头部信息的Authorization中的到tocken
	tokenString := c.GetHeader("Authorization")
	//如果信息为空
	if tokenString == "" {
		c.JSON(http.StatusUnauthorized, gin.H{
			"status": -1,
			"msg":    "Authorization header not provided",
		})
		c.Abort()
		return
	}
	//将tokenString解析成想要的*MyClaims格式,自定义的函数解析,下面内容有讲解
	jwt := NewJWT()
	claims, err := jwt.ParseToken(tokenString)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"status": -1,
			"msg":    err.Error(),
		})
		c.Abort()
		return
	}
	c.Set("claims", claims)
}

中间件的使用方法

	router := gin.Default()
	router.Use(cors())
	router.Use(JWTAuth)

前端 Vue

使用 Pinia 维护 Token

import { defineStore } from 'pinia'
import request from '@/utils/request';
import router from '@/router'
import { ElMessage } from 'element-plus';
import { UserFormDataInter } from '@/types/user';
import { jwtDecode } from 'jwt-decode'
 
export const useUserStore = defineStore('user', {
  state: () => {
    return {
      token: ***'token') || "",
      username: window.localStorage.getItem('username') || "",
    }
  },
  actions: {
    async login(user: UserFormDataInter) {
      try {
        request.post("/login", user).then(res => {
          if (res) {
            ElMessage({
              message: '登录成功',
              type: 'success',
            })
 
            this.token = ***                       
            let token_parsed = jwtDecode(res.data.token) as { name: string };
            this.username = token_parsed.name
 
            window.localStorage.setItem("token", res.data.token);
            window.localStorage.setItem("username", this.username);
 
            console.log(`token_parsed`, token_parsed)
            
            router.push('/config/status');
          } else {
            ElMessage.error('账号或密码错误!!!登录失败!!!')
          }
        });
      } catch (error) {
        ElMessage.error('登录失败')
        throw error;
      }
    },
    logout() {
      this.token = ""
      this.username = ""
 
      router.push('/login');
    },
  }
});

路由进行拦截验证

import { jwtDecode } from 'jwt-decode'
 
//路由守卫    
//全局守卫,登录拦截
//进行路由拦截:当没有登陆标识,直接打回登陆页面,如何避免退回到 登陆页呢?
router.beforeEach((to, from, next) => {
  // 从本地缓存中获取保存的 token 信息
  if (to.path === "/login") {
    next()
    return
  }
 
  let token_str = window.localStorage.getItem('token') || ""
 
  // 如果没有token,强制跳转到登录页面;如果有,则判断token时间是否过期
  if (token_str === "") {
    ElMessage({
      message: h('p', null, [
        h('span', null, '没有找到登录信息'),
        h('i', { style: 'color: teal' }, '请登录后继续使用本系统!'),
      ]),
    })
    next('/login')
  } else {
    let tokenObj = jwtDecode(token_str) as { name: string, exp: number };
 
    let date = new Date().getTime();
 
    // 当前时间 - token中的登录时间 > 设置的过期时间,为过期;则清除token,并强制跳转至登录页
    // 反之为有效期,则放行
    if (date > tokenObj.exp * 1000) {
      console.log(`${new Date(date)} > ${new Date(tokenObj.exp*1000)}`)
      window.localStorage.removeItem('username');
      window.localStorage.removeItem('token');
 
      next('/login')
      ElMessage({
        message: h('p', null, [
          h('span', null, '登录状态过期'),
          h('i', { style: 'color: teal' }, '请重新登录!'),
        ]),
      })
    } else {
      next();
    }
  }
});

Request 封装 Token

import axios from 'axios'
import { useUserStore } from '@/store/user'
 
const request = axios.create({
	// 这里是全局统一加上了 '/api' 前缀,也就是说所有接口都会加上'/api'前缀在,页面里面写接口的时候就不要加 '/api'了
    baseURL: 'http://xxx.xx.xx.xxx:8080/api',  
    timeout: 5000
})
 
// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
    config.headers['Content-Type'] = 'application/json;charset=utf-8';
    config.headers['Authorization'] = window.localStorage.getItem('token') || "";
    return config
}, error => {
    return Promise.reject(error)
});
 
// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
    response => {
        let res = response.data;
        // 如果是返回的文件
        if (response.config.responseType === 'blob') {
            return res
        }
        // 兼容服务端返回的字符串数据
        if (typeof res === 'string') {
            res = res ? JSON.parse(res) : res
        }
        return res;
    },
    error => {
        console.log('' + error) // for debug
        return Promise.reject(error)
    }
)
 
 
// 封装get请求方法
export const $get = async (url:string, params:object={}) => {
    let {data} = await request.get(url, {params});
    return data
  }
  
  // 封装post请求方法
  export const $post = async (url:string, params:object={}) => {
    let {data} =  await request.post(url, params);
    return data
  }
 
export default request