• 【Go】十三、TOKEN机制与跨域处理方式


    身份校验

    对于微服务场景来说,使用 cookie + session 来进行身份校验是一种不合适的做法,因为 生成的sessionid 是不存储任何信息的,这样我们如果要在其他模块中进行身份校验就是做不到的,例如:我们无法在商品模块中筛选出对应登录用户上传的商品,因为我们无法通过sessionid 知道当前登录的是哪个用户。

    由此,登录逻辑中产生了 TOKEN 的做法,TOKEN 中会携带一定的用户信息,这样即使不处于用户模块,也可以获取用户的相关信息 - 利用 TOKEN 中的用户信息去另外的模块进行查找。

    JSON WEB TOKEN(JWT)

    这里使用 jwt 来对 TOKEN进行构建

    jwt 的基本结构:

    xxxxx.yyyyy.zzzzz

    其中:xxxxx为加密算法

    yyyyy为TOKEN中保存的信息

    zzzzz为签名信息

    例如:

    这是一串典型的JWT-TOKEN
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    
    下面是他的内容
    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    
    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      your-256-bit-secret
    ) 
    

    JWT 在GIN 中的实际使用

    构建新目录 models

    user-web

    models

    request.go

    middlewares

    jwt.go

    config

    config.go

    request.go:

    package models
    
    import "github.com/dgrijalva/jwt-go"
    
    // 这里是 给 jwt 构建的信息
    type CustomClaims struct {
    	ID                 uint   // 用户ID
    	NickName           string // 用户昵称
    	AuthorityID        uint   // 用户角色 role
    	jwt.StandardClaims        // JWT 基本元素
    }
    
    

    jwt.go

    注意此时的 jwt.go 可以直接粘贴,但需要对应进行修改其他引用文件

    package middlewares
    
    import (
    	"errors"
    	"mxshop-api/user-web/global"
    	"mxshop-api/user-web/models"
    	"net/http"
    	"time"
    
    	"github.com/dgrijalva/jwt-go"
    	"github.com/gin-gonic/gin"
    )
    
    // TOKEN 验证方法
    func JWTAuth() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localSstorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
    		token := c.Request.Header.Get("x-token")
    		if token == "" {
    			c.JSON(http.StatusUnauthorized, map[string]string{
    				"msg": "请登录",
    			})
    			c.Abort()
    			return
    		}
    		j := NewJWT()
    		// parseToken 解析token包含的信息
    		claims, err := j.ParseToken(token)
    		if err != nil {
    			if err == TokenExpired {
    				if err == TokenExpired {
    					c.JSON(http.StatusUnauthorized, map[string]string{
    						"msg": "授权已过期",
    					})
    					c.Abort()
    					return
    				}
    			}
    
    			c.JSON(http.StatusUnauthorized, "未登陆")
    			c.Abort()
    			return
    		}
    		c.Set("claims", claims)
    		c.Set("userId", claims.ID)
    		c.Next()
    	}
    }
    
    type JWT struct {
    	SigningKey []byte
    }
    
    // 错误信息
    var (
    	TokenExpired     = errors.New("Token is expired")
    	TokenNotValidYet = errors.New("Token not active yet")
    	TokenMalformed   = errors.New("That's not even a token")
    	TokenInvalid     = errors.New("Couldn't handle this token:")
    )
    
    // 在这里进行细节的配置,需要配合 global 中的内容配合进行操作
    // 取出秘钥
    func NewJWT() *JWT {
    	return &JWT{
    		[]byte(global.ServerConfig.JWTInfo.SigningKey), //可以设置过期时间
    	}
    }
    
    // 创建一个token
    func (j *JWT) CreateToken(claims models.CustomClaims) (string, error) {
    	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    	return token.SignedString(j.SigningKey)
    }
    
    // 解析 token
    func (j *JWT) ParseToken(tokenString string) (*models.CustomClaims, error) {
    	token, err := jwt.ParseWithClaims(tokenString, &models.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) {
    		return j.SigningKey, nil
    	})
    	if err != nil {
    		if ve, ok := err.(*jwt.ValidationError); ok {
    			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
    				return nil, TokenMalformed
    			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
    				// Token is expired
    				return nil, TokenExpired
    			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
    				return nil, TokenNotValidYet
    			} else {
    				return nil, TokenInvalid
    			}
    		}
    	}
    	if token != nil {
    		if claims, ok := token.Claims.(*models.CustomClaims); ok && token.Valid {
    			return claims, nil
    		}
    		return nil, TokenInvalid
    
    	} else {
    		return nil, TokenInvalid
    
    	}
    
    }
    
    // 更新token
    func (j *JWT) RefreshToken(tokenString string) (string, error) {
    	jwt.TimeFunc = func() time.Time {
    		return time.Unix(0, 0)
    	}
    	token, err := jwt.ParseWithClaims(tokenString, &models.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
    		return j.SigningKey, nil
    	})
    	if err != nil {
    		return "", err
    	}
    	if claims, ok := token.Claims.(*models.CustomClaims); ok && token.Valid {
    		jwt.TimeFunc = time.Now
    		claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
    		return j.CreateToken(*claims)
    	}
    	return "", TokenInvalid
    }
    
    

    注意引用文件,将引用文件进行配置:配置 config/config.go

    在 config.go 中添加如下内容:

    type JWTConfig struct {
    	SigningKey string `mapstructure:"key"`
    }
    

    在配置文件中添加配置 config-debug.yaml

    这个 key 是随机生成的

    jwt:
      key: "$Wv?[cf@v0]i.A5v:08V"
    

    继续修改config.go,令其可以取出key:

    type ServerConfig struct {
    	Name        string        `mapstructure:"name"`
    	Port        int32         `mapstructure:"port"`
    	UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
    	JWTInfo     JWTConfig     `mapstructure:"jwt"`
    }
    
    type JWTConfig struct {
    	SigningKey string `mapstructure:"key"`
    }
    
    

    改到这种程度,jwt.go 应该就不会报错了,

    下一步是生成TOKEN,进业务逻辑:api/user.go

    修改登陆成功后的代码,这里直接提供登录的所有代码,以便翻看:

    // 用户登录模块
    func PassWordLogin(c *gin.Context) {
    	// 绑定请求参数
    	passwordLoginForm := forms.PassWordLoginForm{}
    	if err := c.ShouldBind(&passwordLoginForm); err != nil {
    		HandleValidatorError(c, err)
    	}
    
    	// 将请求参数转发给 rpc 服务器
    	//ip := "127.0.0.1"
    	//port := 50051
    	// 拨号连接用户 GRPC 服务
    	userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", global.ServerConfig.UserSrvInfo.Host, global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure())
    	if err != nil {
    		zap.L().Error("[PassWordLogin] 连接 【用户服务失败】",
    			zap.String("msg", err.Error()))
    	}
    	// 生成 grpc 的 client 并调用接口
    	userSrvClient := proto.NewUserClient(userConn)
    
    	if rsp, err := userSrvClient.GetUserByMobile(context.Background(), &proto.MobileRequest{
    		Mobile: passwordLoginForm.Mobile,
    	}); err != nil {
    		if e, ok := status.FromError(err); ok {
    			switch e.Code() {
    			case codes.NotFound:
    				c.JSON(http.StatusBadRequest, map[string]string{
    					"mobile": "用户不存在",
    				})
    			default:
    				c.JSON(http.StatusInternalServerError, map[string]string{
    					"mobile": "登录失败",
    				})
    			}
    			return
    		}
    	} else {
    		// TODO 这里只能验证找到了用户,后面继续验证密码是否正确的功能
    		// 这里是调用的 Srv 中的服务进行登录逻辑的判断,传入的 第二个参数是需要的参数,这个参数的对应取值在之前已经取到
    		// 这里仅仅演示简单的登录密码验证机制,完全的登录逻辑在之后展示
    		if passRsp, passErr := userSrvClient.CheckPassWord(context.Background(), &proto.PassWordCheckInfo{
    			Password:          passwordLoginForm.PassWord,
    			EncryptedPassword: rsp.Password,
    		}); passErr != nil {
    			c.JSON(http.StatusInternalServerError, map[string]string{
    				"msg": "登录失败",
    			})
    		} else {
    			// 这里代表未发生其他错误,继续验证密码
    			if passRsp.Success {
    				// 登录成功,生成TOKEN
    				j := middlewares.NewJWT() // 获得签名
    				// 构建传递的信息以及签名信息
    				claims := models.CustomClaims{
    					ID:          uint(rsp.Id),
    					NickName:    rsp.NickName,
    					AuthorityID: uint(rsp.Role),
    					StandardClaims: jwt.StandardClaims{ // 签名相关信息
    						NotBefore: time.Now().Unix(),               // 签名生效时间:现在
    						ExpiresAt: time.Now().Unix() + 60*60*24*30, // 签名过期时间,从现在开始一个月
    						Issuer:    "BaiLu",                         // 签名 对象 (公司)
    					},
    				}
    				// 真正创建 TOKEN:
    				token, err := j.CreateToken(claims)
    				if err != nil {
    					c.JSON(http.StatusInternalServerError, gin.H{
    						"msg": "TOKEN生成失败",
    					})
    					return
    				}
    
    				c.JSON(http.StatusOK, gin.H{
    					"id":         rsp.Id,
    					"nick_name":  rsp.NickName,
    					"token":      token,
    					"expired_at": (time.Now().Unix() + 60*60*24*30) * 1000,
    				})
    			} else {
    				// 这里代表登录密码验证错误
    				c.JSON(http.StatusBadRequest, map[string]string{
    					"msg": "密码错误",
    				})
    			}
    		}
    	}
    
    }
    

    修改完成之后,模拟请求就可以生成 TOKEN 了,下面是一个 TOKEN 实例:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MSwiTmlja05hbWUiOiJIdVRhbyIsIkF1dGhvcml0eUlEIjoxLCJleHAiOjE3MjA2ODgzNDksImlzcyI6IkJhaUx1IiwibmJmIjoxNzE4MDk2MzQ5fQ.xVs31ToQJ5TjgjXn2omBXM2i-bw5sVC5Mf6NtN8bw7Y
    

    其会被解析:

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
    {
      "ID": 1,
      "NickName": "HuTao",
      "AuthorityID": 1,
      "exp": 1720688349,
      "iss": "BaiLu",
      "nbf": 1718096349
    }
    

    但是其签名校验无法通过,因为其没有对应的签名信息,也就是之前生成的这个:

    $Wv?[cf@v0]i.A5v:08V
    

    配置 GetUserList 模块登陆才能访问

    在 JWT.go 中 已经配置好了 JWTAuth 的拦截器,只需要在 Router 的位置进行配置即可

    将路由中添加拦截器:

    	// 这样就需要 /user/list 才可以进行访问了
    	UserRouter := Router.Group("user")
    	{
    		// 在这里添加拦截器的作用响应位置
    		UserRouter.GET("list", middlewares.JWTAuth(), api.GetUserList)
    		UserRouter.POST("pwd_login", api.PassWordLogin)
    	}
    

    注意拦截器中用到了两个方法c.Set(“userID”, userID),举例这样就可以在 c 中的后续使用 c.Get 再获取刚刚 Set 的信息,对于传递信息十分有效第二个方法是 c.Next() 这个方法会令 gin 调用执行链中的下一个拦截器或者下一个方法等

    此时再发送 GetUserList 的请求就会直接返回请登录

    之后只要将合法的 token 写入 header 的 x-token 参数中,就可以正确匹配

    之后由于我们在 jwt token写入的末尾 有 c.Set(“claims”, claims) 将对应的对象存入上下文,故而我们可以:

    采用如下方式调用到我们需要的数据

    	// 测试 ID 是否可以取到
    	claims, _ := ctx.Get("claims")
    	currentUser := claims.(*models.CustomClaims)
    	zap.S().Infof("访问用户: %d", currentUser.ID)
    

    鉴权配置,配置 GetUserList 接口只有管理员才能访问

    在 middlewares 目录下创建 admin.go 文件 用于鉴权:

    package middlewares
    
    import (
    	"github.com/gin-gonic/gin"
    	"mxshop-api/user-web/models"
    	"net/http"
    )
    
    // 手写一个拦截器
    func IsAdminAuth() gin.HandlerFunc {
    	return func(ctx *gin.Context) {
    		claims, _ := ctx.Get("claims")
    		// 拿到对应的角色
    		currentUser := claims.(*models.CustomClaims)
    		// 若用户权限为 2 ,视为管理员
    		if currentUser.AuthorityID == 2 {
    			ctx.Next()
    		} else {
    			ctx.JSON(http.StatusForbidden, gin.H{
    				"msg": "权限不足,禁止访问",
    			})
    			ctx.Abort()
    			return
    		}
    	}
    }
    
    

    在router.go 中进行修改:

    UserRouter.GET("list", middlewares.JWTAuth(), middlewares.IsAdminAuth(), api.GetUserList)
    

    跨域的处理

    假设我们生成一个前端页面,这个页面可以发送 GetUserList 请求:

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <title>TTTTTTTtitle>
        <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js">script>
    head>
    <body>
        <button type="button" id="query">请求发送Biu~button>
        <div id="content" style="background-color: aquamarine;width: 300px;height: 500px">div>
    body>
    <script type="text/javascript">
        $("#query").click(function() {
            $.ajax(
                {
                    url: "http://127.0.0.1:8021/u/v1/user/list",
                    dataType: "json",
                    type: "get",
                    beforeSend: function(request) {
                      request.setRequestHeader("x-token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MSwiTmlja05hbWUiOiJIdVRhbyIsIkF1dGhvcml0eUlEIjoxLCJleHAiOjE3MjA4MDAyNDMsImlzcyI6IkJhaUx1IiwibmJmIjoxNzE4MjA4MjQzfQ.KuzJSLNbZrk37u4BgT6s-9JtbW8JwA23QB2ktGShRfY")
                    },
                    success: function (result) {
                        console.log(result.data)
                        $("#content").text(result.data)
                    },
                    error: function (data) {
                        alert("请求出错")
                    }
                }
            );
        });
    script>
    html>
    

    这里我们发送请求就会出现 OPTIONS 预检报错,这个问题一般由发起复杂请求引起,复杂请求一般包括三种情况:

    • 请求方法为:PUT / DELETE
    • 请求头中带有:application/json
    • 有自定义添加的请求头,例如上面这样

    并且我们的请求地址与响应地址的 ip、端口 不同时就会引起跨域:

    这时,我们就需要进行跨域处理:

    在middlewares 目录下创建 cors.go 文件:

    package middlewares
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func Cors() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		// 获取请求方法
    		method := c.Request.Method
    		// 允许所有来源,也就是传统意义上的跨域
    		c.Header("Access-Control-Allow-Origin", "*")
    		// 允许头中带有这些
    		c.Header("Access-Control-Allow-Headers", "Content-Type, AccessToken, X-CSRF-TOKEN, Authorization, Token, x-token")
    		// 允许这些方法
    		c.Header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS, PATCH")
    		// 允许客户端访问的哪些请求头部
    		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
    		// 允许携带Cookie
    		c.Header("Access-Control-Allow-Credentials", "true")
    
    		if method == "OPTIONS" {
    			c.AbortWithStatus(http.StatusNoContent)
    		}
    	}
    }
    
    

    将配置的拦截器放在 Initialize/router.go 下,令所有请求都可以穿过跨域

    func Routers() *gin.Engine {
    	Router := gin.Default()
    	
    	// 配置跨域的拦截器
    	Router.Use(middlewares.Cors())
    
    	ApiGroup := Router.Group("/u/v1")
    	router2.InitUserRouter(ApiGroup)
    	return Router
    }
    

    这样子跨域就处理完成了

  • 相关阅读:
    centos7虚拟机部署苍穹私有云环境记录
    使用NSSM将.exe程序安装成windows服务
    【ManageEngine】局域网监控软件是什么,有什么作用
    每日优鲜深陷“破产风波”,生鲜电商路在何方?
    实现数字化转型的解决方案
    Java进阶总结——集合
    基于HTML制作的闲置交易网站设计
    simple_bypass
    Python开发运维:PyMongo 连接操作 MongoDB
    【​毕业季·进击的技术er​】--毕业到工作小结
  • 原文地址:https://blog.csdn.net/weixin_41365204/article/details/139654457