• 14、用户web层服务(二)


    一、分页的优化

    • 分页的优化:之前我们分页的pn和psize是写死的,现在需要优化成能通过页面参数请求来展示分页数据
    • user_web\api\api_user.go
    func GetUserList(ctx *gin.Context) {
    
    	//拨号连接用户grpc服务器
    	userConn, err := grpc.Dial(fmt.Sprintf("%s:%d",
    		global.ServerConfig.UserSrvInfo.Host,
    		global.ServerConfig.UserSrvInfo.Port),
    		grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		zap.S().Errorw("[GetUserList] 连接 【用户服务失败】", "msg", err.Error())
    	}
    	//生成grpc的client并调用接口
    	userSrvClient := proto.NewUserClient(userConn)
    
    	pn := ctx.DefaultQuery("pn", "0")
    	pnInt, _ := strconv.Atoi(pn)
    	pSize := ctx.DefaultQuery("psize", "10")
    	pSizeInt, _ := strconv.Atoi(pSize)
    
    	rsp, err := userSrvClient.GetUserList(context.Background(), &proto.PageInfo{
    		Pn:    uint32(pnInt),
    		PSize: uint32(pSizeInt),
    	})
    	if err != nil {
    		zap.S().Errorw("[GetUserList] 查询 【用户列表】 失败")
    		HandleGrpcErrorToHttp(err, ctx)
    		return
    	}
    
    	result := make([]interface{}, 0)
    	for _, value := range rsp.Data {
    		user := response.UserResponse{
    			Id:       value.Id,
    			NickName: value.NickName,
    			//Birthday: time.Time(time.Unix(int64(value.BirthDay), 0)).Format("2006-01-02"),
    			Birthday: response.JsonTime(time.Unix(int64(value.BirthDay), 0)),
    			Gender:   value.Gender,
    			Mobile:   value.Mobile,
    		}
    		result = append(result, user)
    	}
    
    	ctx.JSON(http.StatusOK, result)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 分页优化测试
      在这里插入图片描述
      在这里插入图片描述

    二、登录+表单验证

    1 - 登录实现

    • user_web\global\global.go:添加全局变量Trans
    package global
    
    import (
    	ut "github.com/go-playground/universal-translator"
    
    	"web_api/user_web/config"
    )
    
    var (
    	Trans        ut.Translator
    	ServerConfig *config.ServerConfig = &config.ServerConfig{}
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • user_web\forms\form_user.go:添加用户表单
    package forms
    
    type PassWordLoginForm struct {
    	Mobile   string `form:"mobile" json:"mobile" binding:"required"` //手机号码格式有规范可寻, 自定义validator
    	PassWord string `form:"password" json:"password" binding:"required,min=3,max=20"`
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • user_web\initialize\init_validator.go:初始化翻译器
    package initialize
    
    import (
    	"fmt"
    	"reflect"
    	"strings"
    
    	"github.com/gin-gonic/gin/binding"
    	"github.com/go-playground/locales/en"
    	"github.com/go-playground/locales/zh"
    	ut "github.com/go-playground/universal-translator"
    	"github.com/go-playground/validator/v10"
    	en_translations "github.com/go-playground/validator/v10/translations/en"
    	zh_translations "github.com/go-playground/validator/v10/translations/zh"
    
    	"web_api/user_web/global"
    )
    
    func InitTrans(locale string) (err error) {
    	//修改gin框架中的validator引擎属性, 实现定制
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    		//注册一个获取json的tag的自定义方法
    		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
    			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
    			if name == "-" {
    				return ""
    			}
    			return name
    		})
    
    		zhT := zh.New() //中文翻译器
    		enT := en.New() //英文翻译器
    		//第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
    		uni := ut.New(enT, zhT, enT)
    		global.Trans, ok = uni.GetTranslator(locale)
    		if !ok {
    			return fmt.Errorf("uni.GetTranslator(%s)", locale)
    		}
    
    		switch locale {
    		case "en":
    			en_translations.RegisterDefaultTranslations(v, global.Trans)
    		case "zh":
    			zh_translations.RegisterDefaultTranslations(v, global.Trans)
    		default:
    			en_translations.RegisterDefaultTranslations(v, global.Trans)
    		}
    		return
    	}
    	return
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • user_web\router\router_user.go:添加登录的router
    package router
    
    import (
    	"web_api/user_web/api"
    
    	"github.com/gin-gonic/gin"
    	"go.uber.org/zap"
    )
    
    func InitUserRouter(Router *gin.RouterGroup) {
    	UserRouter := Router.Group("user")
    	zap.S().Info("配置用户相关的url")
    	{
    		UserRouter.GET("list", api.GetUserList)
    		UserRouter.POST("pwd_login", api.PassWordLogin)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • user_web\api\api_user.go:添加登录api
    func PassWordLogin(c *gin.Context) {
    	//表单验证
    	passwordLoginForm := forms.PassWordLoginForm{}
    	if err := c.ShouldBind(&passwordLoginForm); err != nil {
    		HandleValidatorError(c, err)
    		return
    	}
    }
    func HandleValidatorError(c *gin.Context, err error) {
    	errs, ok := err.(validator.ValidationErrors)
    	if !ok {
    		c.JSON(http.StatusOK, gin.H{
    			"msg": err.Error(),
    		})
    	}
    	c.JSON(http.StatusBadRequest, gin.H{
    		"error": removeTopStruct(errs.Translate(global.Trans)),
    	})
    	return
    }
    
    func removeTopStruct(fileds map[string]string) map[string]string {
    	rsp := map[string]string{}
    	for field, err := range fileds {
    		rsp[field[strings.Index(field, ".")+1:]] = err
    	}
    	return rsp
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • user_web\main.go:添加调用初始化翻译器
    package main
    
    import (
    	"fmt"
    	"web_api/user_web/global"
    	"web_api/user_web/initialize"
    
    	"go.uber.org/zap"
    )
    
    func main() {
    	//1. 初始化logger
    	initialize.InitLogger()
    	//2. 初始化配置文件
    	initialize.InitConfig()
    	//3. 初始化routers
    	Router := initialize.Routers()
    	//4. 初始化翻译
    	if err := initialize.InitTrans("zh"); err != nil {
    		panic(err)
    	}
    	/*
    		1. S()可以获取一个全局的sugar,可以让我们自己设置一个全局的logger
    		2. 日志是分级别的,debug, info , warn, error, fetal
    			debug最低,fetal最高,如果配置成info,所有比info低的都不会输出
    			NewProduction默认日志级别为info
    			NewDevelopment默认日志级别为debug
    		3. S函数和L函数很有用, 提供了一个全局的安全访问logger的途径
    	*/
    	zap.S().Debugf("启动服务器, 端口: %d", global.ServerConfig.Port)
    
    	if err := Router.Run(fmt.Sprintf(":%d", global.ServerConfig.Port)); err != nil {
    		zap.S().Panic("启动失败:", err.Error())
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • YApi:添加登录接口
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    2 - 自定义mobile验证器

    注意 -> validator要import v10的版本:github.com/go-playground/validator/v10

    • user_web\validator\validators.go
    package validator
    
    import (
    	"regexp"
    
    	"github.com/go-playground/validator/v10"
    )
    
    func ValidateMobile(fl validator.FieldLevel) bool {
    	mobile := fl.Field().String()
    	//使用正则表达式判断是否合法
    	ok, _ := regexp.MatchString(`^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`, mobile)
    	return ok
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • user_web\forms\form_user.go:添加自定义验证器名
    package forms
    
    type PassWordLoginForm struct {
    	Mobile   string `form:"mobile" json:"mobile" binding:"required,mobile"` //手机号码格式有规范可寻, 自定义validator
    	PassWord string `form:"password" json:"password" binding:"required,min=3,max=20"`
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • user_web\main.go:注册自定义验证器
    package main
    
    import (
    	"fmt"
    	"web_api/user_web/global"
    	"web_api/user_web/initialize"
    
    	"github.com/gin-gonic/gin/binding"
    	ut "github.com/go-playground/universal-translator"
    	"github.com/go-playground/validator/v10"
    	"go.uber.org/zap"
    
    	myvalidator "web_api/user_web/validator"
    )
    
    func main() {
    	//1. 初始化logger
    	initialize.InitLogger()
    	//2. 初始化配置文件
    	initialize.InitConfig()
    	//3. 初始化routers
    	Router := initialize.Routers()
    	//4. 初始化翻译
    	if err := initialize.InitTrans("zh"); err != nil {
    		panic(err)
    	}
    
    	//注册验证器
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    		_ = v.RegisterValidation("mobile", myvalidator.ValidateMobile)
    		_ = v.RegisterTranslation("mobile", global.Trans, func(ut ut.Translator) error {
    			return ut.Add("mobile", "{0} 非法的手机号码!", true) // see universal-translator for details
    		}, func(ut ut.Translator, fe validator.FieldError) string {
    			t, _ := ut.T("mobile", fe.Field())
    			return t
    		})
    	}
    
    	/*
    		1. S()可以获取一个全局的sugar,可以让我们自己设置一个全局的logger
    		2. 日志是分级别的,debug, info , warn, error, fetal
    			debug最低,fetal最高,如果配置成info,所有比info低的都不会输出
    			NewProduction默认日志级别为info
    			NewDevelopment默认日志级别为debug
    		3. S函数和L函数很有用, 提供了一个全局的安全访问logger的途径
    	*/
    	zap.S().Debugf("启动服务器, 端口: %d", global.ServerConfig.Port)
    
    	if err := Router.Run(fmt.Sprintf(":%d", global.ServerConfig.Port)); err != nil {
    		zap.S().Panic("启动失败:", err.Error())
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    3 - 完善登录逻辑

    • 完善登录业务:之前的登录表单成功后的逻辑并没有处理
    • user_web\api\api_user.go:这里调用了rpc的userSrvClient.CheckPassWord,登录需要真实的数据库手机号和密码
    func PassWordLogin(c *gin.Context) {
    	//表单验证
    	passwordLoginForm := forms.PassWordLoginForm{}
    	if err := c.ShouldBind(&passwordLoginForm); err != nil {
    		HandleValidatorError(c, err)
    		return
    	}
    
    	//拨号连接用户grpc服务器
    	userConn, err := grpc.Dial(fmt.Sprintf("%s:%d",
    		global.ServerConfig.UserSrvInfo.Host,
    		global.ServerConfig.UserSrvInfo.Port),
    		grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		zap.S().Errorw("[GetUserList] 连接 【用户服务失败】", "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 {
    		//只是查询到用户了而已,并没有检查密码
    		if passRsp, pasErr := userSrvClient.CheckPassWord(context.Background(), &proto.PasswordCheckInfo{
    			Password:          passwordLoginForm.PassWord,
    			EncryptedPassword: rsp.PassWord,
    		}); pasErr != nil {
    			c.JSON(http.StatusInternalServerError, map[string]string{
    				"password": "登录失败",
    			})
    		} else {
    			if passRsp.Success {
    				c.JSON(http.StatusOK, map[string]string{
    					"msg": "登录成功",
    				})
    			} else {
    				c.JSON(http.StatusBadRequest, map[string]string{
    					"msg": "登录失败",
    				})
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    在这里插入图片描述

    三、json web token

    1 - session在单体架构中的应用

    在这里插入图片描述

    2 - session机制在微服务下的问题

    • 微服务中的session问题:在微服务中,每个服务是独立的,数据库也是独立的,因此需要有一个公用的数仓来存储session才能在微服务中使用session机制
    • 微服务中session解决方案分析
      • redis集群:用户服务将session保存在redis中,其他服务根据sessionid到redis中查询获取用户
        在这里插入图片描述

    3 - json web token

    • JWT官网https://jwt.io/
    • 什么是json web token:JSON Web Token 是一个开放标准协议,它定义了一种“紧凑”和“自包含”的方式,它用于各方之间作为JSON对象安全地传输信息
    • json web token使用场景
      • Authorization(授权):这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用
      • Information Exchange(信息交换):对于安全的在各方之间传输信息而言,json web tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是他们说的那个人。另外,由于签名是使用头和有效负载计算的,你还可以验证内容有没有被篡改
    • JWT 数据结构:JSON Web Tokens 由用点 ( .)分隔的三个部分组成,它们是
      • Header(头部)
      • Payload(负载)
      • Signature(签名)
    • 一个典型的JWTxxxxx.yyyyy.zzzzz

    在这里插入图片描述

    • JWT在微服务中的应用:只要保证各个服务之间使用的是相同的密钥加密解密,那么各个服务之间就可以通过sessionid来传递
    • JWT工作流程
      在这里插入图片描述
    • JWT优点
      • 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
      • 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
      • 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
      • 它不需要在服务端保存会话信息, 所以它易于应用的扩展
    • JWT安全
      • 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分
      • 保护好secret私钥,该私钥非常重要
      • 如果可以,请使用https协议

    4 - gin集成JWT

    //user_web\config_debug.yaml
    name: 'user-web'
    port: '8081'
    user_srv:
      host: '127.0.0.1'
      port: '50051'
    jwt:
      key: 'VYLDYq3&hGWjWqF$K1ih'
    
    //user_web\config_pro.yaml
    name: 'user-web'
    port: '8031'
    user_srv:
      host: '127.0.0.1'
      port: '50052'
    jwt:
      key: 'VYLDYq3&hGWjWqF$K1ih'
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • user_web\middlewares\jwt.go:中间件添加jwt
    package middlewares
    
    import (
    	"errors"
    	"net/http"
    	"time"
    
    	"github.com/dgrijalva/jwt-go"
    	"github.com/gin-gonic/gin"
    
    	"web_api/user_web/global"
    	"web_api/user_web/models"
    )
    
    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:")
    )
    
    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
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • user_web\models\request.go:CustomClaims结构
    package models
    
    import (
    	"github.com/dgrijalva/jwt-go"
    )
    
    type CustomClaims struct {
    	ID          uint
    	NickName    string
    	AuthorityId uint
    	jwt.StandardClaims
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • user_web\config\config.go:添加JWTConfig结构
    package config
    
    type UserSrvConfig struct {
    	Host string `mapstructure:"host" json:"host"`
    	Port int    `mapstructure:"port" json:"port"`
    }
    
    type JWTConfig struct {
    	SigningKey string `mapstructure:"key" json:"key"`
    }
    
    type ServerConfig struct {
    	Name        string        `mapstructure:"name" json:"name"`
    	Port        int           `mapstructure:"port" json:"port"`
    	UserSrvInfo UserSrvConfig `mapstructure:"user_srv" json:"user_srv"`
    	JWTInfo     JWTConfig     `mapstructure:"jwt" json:"jwt"`
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • user_web\api\api_user.go:登录添加jwt
    func PassWordLogin(c *gin.Context) {
    	//表单验证
    	passwordLoginForm := forms.PassWordLoginForm{}
    	if err := c.ShouldBind(&passwordLoginForm); err != nil {
    		HandleValidatorError(c, err)
    		return
    	}
    
    	//拨号连接用户grpc服务器
    	userConn, err := grpc.Dial(fmt.Sprintf("%s:%d",
    		global.ServerConfig.UserSrvInfo.Host,
    		global.ServerConfig.UserSrvInfo.Port),
    		grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		zap.S().Errorw("[GetUserList] 连接 【用户服务失败】", "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 {
    		//只是查询到用户了而已,并没有检查密码
    		if passRsp, pasErr := userSrvClient.CheckPassWord(context.Background(), &proto.PasswordCheckInfo{
    			Password:          passwordLoginForm.PassWord,
    			EncryptedPassword: rsp.PassWord,
    		}); pasErr != nil {
    			c.JSON(http.StatusInternalServerError, map[string]string{
    				"password": "登录失败",
    			})
    		} 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, //30天过期
    						Issuer:    "imooc",
    					},
    				}
    				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": "登录失败",
    				})
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    在这里插入图片描述
    在这里插入图片描述

    四、接口登录权限验证

    1 - GetUserList添加登录验证

    • user_web\router\router_user.go
    package router
    
    import (
    	"web_api/user_web/api"
    	"web_api/user_web/middlewares"
    
    	"github.com/gin-gonic/gin"
    	"go.uber.org/zap"
    )
    
    func InitUserRouter(Router *gin.RouterGroup) {
    	UserRouter := Router.Group("user")
    	//UserRouter := Router.Group("user").Use(middlewares.JWTAuth()) //给整个user都添加登录验证
    	zap.S().Info("配置用户相关的url")
    	{
    		UserRouter.GET("list", middlewares.JWTAuth(), api.GetUserList)
    		UserRouter.POST("pwd_login", api.PassWordLogin)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    2 - 带token的请求

    为什么在headers中添加的是 x-token?
    这个要注意在user_web\middlewares\jwt.go中,JWTAuth方法里面填的参数就是 x-token
    所以我们使用jwt的话,就需要在headers中添加 x-token

    在这里插入图片描述

    • 先登录复制出token
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    3 - 获取登录用户信息

    • 在jwt的JWTAuth中我们看到代码向gin的context set了值
      • c.Set("claims", claims)
      • c.Set("userId", claims.ID)
    • user_web\api\api_user.go:使用set的值来获取登录用户信息
    func GetUserList(ctx *gin.Context) {
    
    	//拨号连接用户grpc服务器
    	userConn, err := grpc.Dial(fmt.Sprintf("%s:%d",
    		global.ServerConfig.UserSrvInfo.Host,
    		global.ServerConfig.UserSrvInfo.Port),
    		grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		zap.S().Errorw("[GetUserList] 连接 【用户服务失败】", "msg", err.Error())
    	}
    
    	claims, _ := ctx.Get("claims")
    	currentUser := claims.(*models.CustomClaims)
    	zap.S().Infof("访问用户: %d", currentUser.ID)
    
    	//。。。省略
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    4 - YApi全局header设置

    • 之前我们在GetUserList中的header添加了x-tocken:如果每个请求都这样设置太繁琐了,YApi中可以使用全局的设置
      在这里插入图片描述
      在这里插入图片描述

    5 - 管理员验证中间件

    • 需求分析:我们希望GetUserList只能管理员调用,普通用户是无权限调用的
    • user_web\middlewares\admin.go
    package router
    
    import (
    	"web_api/user_web/api"
    	"web_api/user_web/middlewares"
    
    	"github.com/gin-gonic/gin"
    	"go.uber.org/zap"
    )
    
    func InitUserRouter(Router *gin.RouterGroup) {
    	UserRouter := Router.Group("user")
    	//UserRouter := Router.Group("user").Use(middlewares.JWTAuth()) //给整个user都添加登录验证
    	zap.S().Info("配置用户相关的url")
    	{
    		UserRouter.GET("list", middlewares.JWTAuth(), middlewares.IsAdminAuth(), api.GetUserList)
    		UserRouter.POST("pwd_login", api.PassWordLogin)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • user_web\router\router_user.go:router添加管理员判断中间件
    package router
    
    import (
    	"web_api/user_web/api"
    	"web_api/user_web/middlewares"
    
    	"github.com/gin-gonic/gin"
    	"go.uber.org/zap"
    )
    
    func InitUserRouter(Router *gin.RouterGroup) {
    	UserRouter := Router.Group("user")
    	//UserRouter := Router.Group("user").Use(middlewares.JWTAuth()) //给整个user都添加登录验证
    	zap.S().Info("配置用户相关的url")
    	{
    		UserRouter.GET("list", middlewares.JWTAuth(), middlewares.IsAdminAuth(), api.GetUserList)
    		UserRouter.POST("pwd_login", api.PassWordLogin)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述


    五、跨域问题

    1 - 跨域问题分析

    • 浏览器在什么情况下会发起options预检请求:在非简单请求且跨域的情况下,浏览器会发起option预检请求
    • 什么是跨域:当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域
    当前页面url被请求页面url是否跨域原因
    http://www.test.comhttp://www.test.com/index.html同源(协议、域名、端口号相同)
    http://www.test.comhttps://www.test.com/index.html跨域协议不同(http/https)
    http://www.test.comhttp://www.baidu.com/跨域主域名不同(test/baidu)
    http://www.test.comhttp://blog.baidu.com/跨域子域名不同(www/blog)
    http://www.test.com:8080/http://www.test.com:7001/跨域端口号不同(8080/7001)
    • 什么是简单请求:简单请求需要满足以下两个条件
      • 请求方法是以下三种方法之一:HEAD、GET、POST
      • HTTP的头信息不超出以下几种字段
        • Accept
        • Accept-Language
        • Content-Language
        • Last-Event-ID
        • Content-Type:只限于(application/x-www-form-urlencoded、multipart/form-data、text/plain)
    • 什么非简单请求:非简单请求即是复杂请求
      • 请求方法为PUT或DELETE
      • Content-Type字段类型为 application/json
      • 添加额外的http header 比如 access_token
    • 非简单请求在跨域的情况下:非简单请求会先发起一次空body的OPTIONS请求,称为预检请求,用于向服务器请求权限信息,等预检请求被成功响应后,才发起真正的http请求

    浏览器的预检请求结果可以通过设置Access-Control-Max-Age进行缓存

    2 - 跨域问题演示

    • user_web\index.html
    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Titletitle>
        <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js">script>
    head>
    <body>
        <button type="button" id="query">请求数据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:8081/u/v1/user/list",
                    dataType: "json",
                    type: "get",
                    beforeSend: function(request) {
                      request.setRequestHeader("x-token", "	eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MSwiTmlja05hbWUiOiJib2JieTAiLCJBdXRob3JpdHlJZCI6MSwiZXhwIjoxNjYxNTAwNTkzLCJpc3MiOiJpbW9vYyIsIm5iZiI6MTY1ODkwODU5M30.d0We9xsTWIJdUNYYDWtxLJABnjBdTle6M5t5ezjLUEU")
                    },
                    success: function (result) {
                        console.log(result.data);
                        $("#content").text(result.data)
                    },
                    error: function (data) {
                        alert("请求出错")
                    }
                }
            );
        });
    script>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在这里插入图片描述

    3 - 跨域问题解决

    • user_web\middlewares\cors.go:添加中间件解决跨域
    package middlewares
    
    import (
    	"net/http"
    
    	"github.com/gin-gonic/gin"
    )
    
    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, OPTIONS, DELETE, PATCH, PUT")
    		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
    		c.Header("Access-Control-Allow-Credentials", "true")
    
    		if method == "OPTIONS" {
    			c.AbortWithStatus(http.StatusNoContent)
    		}
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • user_web\initialize\init_router.go:为router添加跨域中间件
    package initialize
    
    import (
    	"github.com/gin-gonic/gin"
    
    	"web_api/user_web/middlewares"
    	"web_api/user_web/router"
    )
    
    func Routers() *gin.Engine {
    	Router := gin.Default()
    
    	//配置跨域
    	Router.Use(middlewares.Cors())
    	ApiGroup := Router.Group("/u/v1")
    
    	router.InitUserRouter(ApiGroup)
    	return Router
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    六、完整源码

    mxshop_srvsV4.1.rar

  • 相关阅读:
    整体网络架构p22
    Windows 10上安装Docker
    宇宙的尽头是编制?七成毕业生进体制,清北2021届学子就业报告出炉
    VScode中js关闭烦人的ts检查
    P1809 过河问题
    Elasticsearch:在你的数据上训练大型语言模型 (LLM)
    【学习笔记】AGC008
    Vue React大屏可视化进阶
    java:使用Jedis操作redis
    [附源码]java毕业设计教师业绩考核系统
  • 原文地址:https://blog.csdn.net/qq23001186/article/details/125996807