用户服务的web服务
1.基础项目架构
1.1新建项目和目录结构构建

1.2go高性能日志库-zap使用


1.3zap的文件输出


1.4集成zap和理由初始到gin的启动过程
1.在initalize目录下建立logger.go初始化日志
package initalize
import "go.uber.org/zap"
func InitLogger() {
logger, _ := zap.NewDevelopment()
zap.ReplaceGlobals(logger)
}
2.在router目录下建立user.go初始化路由
package router
import (
"github.com/gin-gonic/gin"
"mxshop_api/user-web/api"
)
func InitUserRouter(Router *gin.RouterGroup) {
UserRouter := Router.Group("user")
{
UserRouter.GET("list", api.GetUserList)
}
}
3.在initalize目录下建立router.go初始化路由
package initalize
import (
"github.com/gin-gonic/gin"
router2 "mxshop_api/user-web/router"
)
func Routers() *gin.Engine {
Router := gin.Default()
ApiGroup := Router.Group("/v1")
router2.InitUserRouter(ApiGroup)
return Router
}
4.在main.go调用初始化函数,并启动
package main
import (
"fmt"
"go.uber.org/zap"
"mxshop_api/user-web/initalize"
)
func main() {
port := 8021
//1.初始化logger
initalize.InitLogger()
//2.初始化routers
Router := initalize.Routers()
zap.S().Debugf("启动服务器,端口:%d", port)
if err := Router.Run(fmt.Sprintf(":%d", port)); err != nil {
zap.S().Panic("启动失败:", err.Error())
}
}
1.5gin调用grpc服务
-
apii目录下建立user.api文件,并创建调用获取用户列表方法
import ( "context" "fmt" "github.com/gin-gonic/gin" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "mxshop_api/user-web/global" "mxshop_api/user-web/global/response" "mxshop_api/user-web/proto" "net/http" "strconv" "time" ) func HandleGrpcErrorToHttp(err error, c *gin.Context) { //将grpc的code转换成http的状态码 if err != nil { if e, ok := status.FromError(err); ok { switch e.Code() { case codes.NotFound: c.JSON(http.StatusNotFound, gin.H{ "msg": e.Message(), }) case codes.Internal: c.JSON(http.StatusInternalServerError, gin.H{ "msg:": "内部错误", }) case codes.InvalidArgument: c.JSON(http.StatusBadRequest, gin.H{ "msg": "参数错误", }) case codes.Unavailable: c.JSON(http.StatusInternalServerError, gin.H{ "msg": "用户服务不可用", }) default: c.JSON(http.StatusInternalServerError, gin.H{ "msg": e.Code(), }) } return } } } func GetUserList(ctx *gin.Context) { userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", global.ServerConfig.UserSrvInfo.Host, global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure()) 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", "0") 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 } reMap := gin.H{ "total": rsp.Total, } result := make([]interface{}, 0) for _, value := range rsp.Data { user := response.UserResponse{ Id: value.Id, NickName: value.NickName, Birthday: response.JsonTime(time.Unix(int64(value.BirthDay), 0)), Gender: value.Gender, Mobile: value.Mobile, } result = append(result, user) } reMap["data"] = result ctx.JSON(http.StatusOK, reMap) } -
在global目录下建立response,并在这个目录下建立user.go
package response import ( "fmt" "time" ) type JsonTime time.Time func (j JsonTime) MarshalJSON() ([]byte, error) { var stmp = fmt.Sprintf("\"%s\"", time.Time(j).Format("2006-01-02")) return []byte(stmp), nil } type UserResponse struct { Id int32 `json:"id"` NickName string `json:"name"` //Birthday string `json:"birthday"` Birthday JsonTime `json:"birthday"` Gender string `json:"gender"` Mobile string `json:"mobile"` } -
启动 user_srv服务和user_web服务,并测试

1.6配置文件-viper
package main
import (
"fmt"
"github.com/spf13/viper"
)
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
}
func main() {
v := viper.New()
v.SetConfigFile("user-web/viptest/config.yaml")
if err := v.ReadInConfig(); err != nil {
panic(err)
}
serverConfig := ServerConfig{}
if err := v.Unmarshal(&serverConfig); err != nil {
panic(err)
}
fmt.Println(serverConfig)
fmt.Println(serverConfig.Port)
}
1.7viper的配置环境开发环境和生产环境隔离
1.7.1配置环境变量

1.7.2配置线上环境和开发环境文件
config-pro.yaml
name: user-web
port: 8021
mysql:
host: '127.0.0.1'
port: 3306
config-debug.yaml
name: user-web
port: 8022
mysql:
host: '127.0.0.1'
port: 3308
1.7.3读取配置文件
package main
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"time"
)
type MysqlConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
MysqlInfo MysqlConfig `mapstructure:"mysql"`
}
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
return viper.GetBool(env)
}
func main() {
debug := GetEnvInfo("MXSHOP_DEBUG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("user-web/viptest/%s-pro.yaml", configFilePrefix)
if debug {
configFileName = fmt.Sprintf("user-web/viptest/%s-debug.yaml", configFilePrefix)
}
v := viper.New()
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
panic(err)
}
serverConfig := ServerConfig{}
if err := v.Unmarshal(&serverConfig); err != nil {
panic(err)
}
fmt.Println(serverConfig)
fmt.Println(serverConfig.Port)
fmt.Println(serverConfig.MysqlInfo.Host)
//viper功能,动态监控变化
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("config file changed:", e.Name)
_ = v.ReadInConfig()
_ = v.Unmarshal(&serverConfig)
fmt.Println(serverConfig)
})
time.Sleep(time.Second * 300)
}
1.8viper集成到gin的web服务中
1.8.1建立配置文件
config-debug.yaml
name: 'user-web'
port: 8021
user_srv:
host: '127.0.0.1'
port: 50051
config-pro.yaml
name: 'user-web'
port: 8021
user_srv:
host: '127.0.0.1'
port: 50051
1.8.2建立映射文件
config/config.go
package config
type UserSrvConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
}
1.8.3初始化配置文件
initalize/config.go
package initalize
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"go.uber.org/zap"
"mxshop_api/user-web/global"
)
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
return viper.GetBool(env)
}
func InitConfig() {
debug := GetEnvInfo("MXSHOP_DEBUG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("user-web/%s-pro.yaml", configFilePrefix)
if debug {
configFileName = fmt.Sprintf("user-web/%s-debug.yaml", configFilePrefix)
}
v := viper.New()
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
panic(err)
}
if err := v.Unmarshal(global.ServerConfig); err != nil {
panic(err)
}
zap.S().Infof("配置信息:%v", global.ServerConfig)
//viper功能,动态监控变化
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
zap.S().Infof("配置信息产生变化:%v", e.Name)
_ = v.ReadInConfig()
_ = v.Unmarshal(global.ServerConfig)
fmt.Println(global.ServerConfig)
})
}
1.8.4整合入口文件
main.go
package main
import (
"fmt"
"go.uber.org/zap"
"mxshop_api/user-web/global"
"mxshop_api/user-web/initalize"
)
func main() {
//1.初始化logger
initalize.InitLogger()
//2. 初始化配置文件
initalize.InitConfig()
//3.初始化routers
Router := initalize.Routers()
zap.S().Debugf("启动服务器,端口:%d", global.ServerConfig.Port)
if err := Router.Run(fmt.Sprintf(":%d", global.ServerConfig.Port)); err != nil {
zap.S().Panic("启动失败:", err.Error())
}
}
2.web层接口开发
2.1表单验证的初始化
2.1.1在forms建立验证文件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"`
}
2.1.2建立初始化验器
在initalize这个目录上建立validator.go
package initalize
import (
"fmt"
"mxshop_api/user-web/global"
"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"
)
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
}
2.1.3定义全局变量trans
在global目录下的global.go
package global
import (
ut "github.com/go-playground/universal-translator"
"mxshop_api/user-web/config"
)
var (
Trans ut.Translator
ServerConfig *config.ServerConfig = &config.ServerConfig{}
)
2.1.4在 main.go文件调用初始化
func main() {
//1.初始化logger
initalize.InitLogger()
//2. 初始化配置文件
initalize.InitConfig()
//3.初始化routers
Router := initalize.Routers()
zap.S().Debugf("启动服务器,端口:%d", global.ServerConfig.Port)
//4. 初始化翻译
if err := initalize.InitTrans("zh"); err != nil {
panic(err)
}
if err := Router.Run(fmt.Sprintf(":%d", global.ServerConfig.Port)); err != nil {
zap.S().Panic("启动失败:", err.Error())
}
}
2.1.5在api目录下建立登录方法
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
}
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 PassWordLogin(c *gin.Context) {
//表单验证
passwordLoginForm := forms.PassWordLoginForm{}
if err := c.ShouldBindJSON(&passwordLoginForm); err != nil {
//如何返回错误信息
HandleValidatorError(c, err)
return
}
}
2.1.6建立相应的登录路由
package router
import (
"github.com/gin-gonic/gin"
"mxshop_api/user-web/api"
)
func InitUserRouter(Router *gin.RouterGroup) {
UserRouter := Router.Group("user")
{
UserRouter.GET("list", api.GetUserList)
UserRouter.POST("pwd_login", api.PassWordLogin)
}
}
2.2自定义mobile验证器
2.2.1在validator目录下建立文件validator.go
package validator
import (
"github.com/go-playground/validator/v10"
"regexp"
)
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)
if !ok {
return false
}
return true
}
2.2.2绑定验证器
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"`
}
2.2.3入口文件绑定
package main
import (
"fmt"
"github.com/gin-gonic/gin/binding"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
"mxshop_api/user-web/global"
"mxshop_api/user-web/initalize"
myvalidator "mxshop_api/user-web/validator"
)
func main() {
//1.初始化logger
initalize.InitLogger()
//2. 初始化配置文件
initalize.InitConfig()
//3.初始化routers
Router := initalize.Routers()
zap.S().Debugf("启动服务器,端口:%d", global.ServerConfig.Port)
//4. 初始化翻译
if err := initalize.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
})
}
if err := Router.Run(fmt.Sprintf(":%d", global.ServerConfig.Port)); err != nil {
zap.S().Panic("启动失败:", err.Error())
}
}
2.3登录逻辑完善
func PassWordLogin(c *gin.Context) {
//表单验证
passwordLoginForm := forms.PassWordLoginForm{}
if err := c.ShouldBindJSON(&passwordLoginForm); err != nil {
//如何返回错误信息
HandleValidatorError(c, err)
return
}
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", global.ServerConfig.UserSrvInfo.Host,
global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure())
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": "登录失败",
})
}
}
}
}
2.4session机制在微服务下的问题

2.5集成jwt到gin中
2.5.1增加目录models
在models目录下增加request.go文件
package models
import (
"github.com/dgrijalva/jwt-go"
)
type CustomClaims struct {
ID uint
NickName string
AuthorityId uint
jwt.StandardClaims
}
2.5.2在middlewares增加jwt.go
package middlewares
import (
"errors"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"mxshop_api/user-web/global"
"mxshop_api/user-web/models"
"net/http"
"time"
)
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), //可以设置过期时间
}
}
// CreateToken 创建一个token
func (j *JWT) CreateToken(claims models.CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}
// ParseToken 解析 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
}
}
// RefreshToken 更新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
}
2.5.3配置JWT
在config/config.go中增加JWT的配置
package config
//新增加的
type JWTConfig struct {
SigningKey string `mapstructure:"key" json:"key"`
}
type UserSrvConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
JWTInfo JWTConfig `mapstructure:"jwt" json:"jwt"`
}
2.5.4在yaml文件中增加配置
name: 'user-web'
port: 8021
user_srv:
host: '127.0.0.1'
port: 50051
// 新增加的
jwt:
key: 'OdpJe6U9GF5*H#JUyO2qzpaz69t1dojd'
2.5.5在登录中生成token
func PassWordLogin(c *gin.Context) {
//表单验证
passwordLoginForm := forms.PassWordLoginForm{}
if err := c.ShouldBindJSON(&passwordLoginForm); err != nil {
//如何返回错误信息
HandleValidatorError(c, err)
return
}
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", global.ServerConfig.UserSrvInfo.Host,
global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure())
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*14*30, //30天过期
Issuer: "lisus",
},
}
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*14*30) * 1000,
})
} else {
c.JSON(http.StatusBadRequest, map[string]string{
"msg": "登录失败",
})
}
}
}
}
2.6给url添加登录权限验证
2.6.1在路由上添加权限验证
在router/user.go
package router
import (
"github.com/gin-gonic/gin"
"mxshop_api/user-web/api"
"mxshop_api/user-web/middlewares"
)
func InitUserRouter(Router *gin.RouterGroup) {
UserRouter := Router.Group("user")
{
UserRouter.GET("list", middlewares.JWTAuth(), middlewares.IsAdminAuth(), api.GetUserList)
UserRouter.POST("pwd_login", api.PassWordLogin)
}
}
2.6.2在中间件middlewars中增加isadmin判断方法
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)
if currentUser.AuthorityId != 2 {
ctx.JSON(http.StatusForbidden, gin.H{
"msg": "无权限",
})
ctx.Abort()
return
} else {
ctx.Next()
}
}
}
2.7如何解决前后端的跨域问题
2.7.1增加中间件cor.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, 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)
}
}
}
2.7.2增加跨域
在initalize目录下的router.go修改
package initalize
import (
"github.com/gin-gonic/gin"
"mxshop_api/user-web/middlewares"
router2 "mxshop_api/user-web/router"
)
func Routers() *gin.Engine {
Router := gin.Default()
//配置跨域
Router.Use(middlewares.Cors())
ApiGroup := Router.Group("/u/v1")
router2.InitUserRouter(ApiGroup)
return Router
}
2.8获取图片验证码
2.8.1增加路由
在router目录下增加base.go
package router
import (
"github.com/gin-gonic/gin"
"mxshop_api/user-web/api"
)
func InitBaseRouter(Router *gin.RouterGroup) {
BaseRouter := Router.Group("base")
{
BaseRouter.GET("captcha", api.GetCaptcha)
}
}
2.8.2增加API
在api目录下增加captcha.go
package api
import (
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"go.uber.org/zap"
"net/http"
)
var store = base64Captcha.DefaultMemStore
func GetCaptcha(ctx *gin.Context) {
driver := base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80)
cp := base64Captcha.NewCaptcha(driver, store)
id, b64s, err := cp.Generate()
if err != nil {
zap.S().Errorf("生成验证码错误,: ", err.Error())
ctx.JSON(http.StatusInternalServerError, gin.H{
"msg": "生成验证码错误",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"captchaId": id,
"picPath": b64s,
})
}
2.8.3登录表单修改
在forms目录下的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"`
//增加的
Captcha string `form:"captcha" json:"captcha" binding:"required,min=5,max=5"`
CaptchaId string `form:"captcha_id" json:"captcha_id" binding:"required"`
}
2.8.4登录验证
func PassWordLogin(c *gin.Context) {
//表单验证
passwordLoginForm := forms.PassWordLoginForm{}
if err := c.ShouldBindJSON(&passwordLoginForm); err != nil {
//如何返回错误信息
HandleValidatorError(c, err)
return
}
//新增加的
if !store.Verify(passwordLoginForm.CaptchaId, passwordLoginForm.Captcha, true) {
c.JSON(http.StatusBadRequest, gin.H{
"captcha": "验证码错误",
})
return
}
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", global.ServerConfig.UserSrvInfo.Host,
global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure())
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*14*30, //30天过期
Issuer: "lisus",
},
}
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*14*30) * 1000,
})
} else {
c.JSON(http.StatusBadRequest, map[string]string{
"msg": "登录失败",
})
}
}
}
}
2.9阿里云发送短信
2.9.1增加路由
package router
import (
"github.com/gin-gonic/gin"
"mxshop_api/user-web/api"
)
func InitBaseRouter(Router *gin.RouterGroup) {
BaseRouter := Router.Group("base")
{
BaseRouter.GET("captcha", api.GetCaptcha)
//发送验证码,新增加的
BaseRouter.POST("send_sms", api.SendSms)
}
}
2.9.2增加API
在api目录增加sms.go
package api
import (
"fmt"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
"github.com/gin-gonic/gin"
"math/rand"
"mxshop_api/user-web/forms"
"mxshop_api/user-web/global"
"net/http"
"strings"
"time"
)
func GenerateSmsCode(witdh int) string {
//生成width长度的短信验证码
numeric := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
r := len(numeric)
rand.Seed(time.Now().UnixNano())
var sb strings.Builder
for i := 0; i < witdh; i++ {
fmt.Fprintf(&sb, "%d", numeric[rand.Intn(r)])
}
return sb.String()
}
func SendSms(ctx *gin.Context) {
sendSmsForm := forms.SendSmsForm{}
if err := ctx.ShouldBind(&sendSmsForm); err != nil {
HandleValidatorError(ctx, err)
return
}
client, err := dysmsapi.NewClientWithAccessKey("cn-beijing", global.ServerConfig.AliSmsInfo.ApiKey, global.ServerConfig.AliSmsInfo.ApiSecrect)
if err != nil {
panic(err)
}
smsCode := GenerateSmsCode(6)
request := requests.NewCommonRequest()
request.Method = "POST"
request.Scheme = "https" // https | http
request.Domain = "dysmsapi.aliyuncs.com"
request.Version = "2017-05-25"
request.ApiName = "SendSms"
request.QueryParams["RegionId"] = "cn-beijing"
request.QueryParams["PhoneNumbers"] = sendSmsForm.Mobile //手机号
request.QueryParams["SignName"] = "*********" //阿里云验证过的项目名 自己设置
request.QueryParams["TemplateCode"] = "*********" //阿里云的短信模板号 自己设置
request.QueryParams["TemplateParam"] = "{\"code\":" + smsCode + "}" //短信模板中的验证码内容 自己生成 之前试过直接返回,但是失败,加上code成功。
response, err := client.ProcessCommonRequest(request)
fmt.Println("err", err)
fmt.Println("mobile", sendSmsForm.Mobile)
fmt.Print(client.DoAction(request, response))
if err != nil {
fmt.Print(err.Error())
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "发送成功",
})
}
2.9.3增加配置
修改config/config.go
package config
type JWTConfig struct {
SigningKey string `mapstructure:"key" json:"key"`
}
//新增加的
type AliSmsConfig struct {
ApiKey string `mapstructure:"key" json:"key"`
ApiSecrect string `mapstructure:"secrect" json:"secrect"`
}
type UserSrvConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
JWTInfo JWTConfig `mapstructure:"jwt" json:"jwt"`
//新增加的
AliSmsInfo AliSmsConfig `mapstructure:"sms" json:"sms"`
}
2.9.4YAML配置
name: 'user-web'
port: 8021
user_srv:
host: '127.0.0.1'
port: 50051
jwt:
key: 'OdpJe6U9GF5*H#JUyO2qzpaz69t1dojd'
sms:
key: '******************'
secrect: '*********************'
2.10redis保存验证码
2.10.1增加config配置
在config/config.go中修改
package config
type JWTConfig struct {
SigningKey string `mapstructure:"key" json:"key"`
}
type AliSmsConfig struct {
ApiKey string `mapstructure:"key" json:"key"`
ApiSecrect string `mapstructure:"secrect" json:"secrect"`
}
// 新增加
type RedisConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
Expire int `mapstructure:"expire" json:"expire"`
}
type UserSrvConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
UserSrvInfo UserSrvConfig `mapstructure:"user_srv"`
JWTInfo JWTConfig `mapstructure:"jwt" json:"jwt"`
AliSmsInfo AliSmsConfig `mapstructure:"sms" json:"sms"`
RedisInfo RedisConfig `mapstructure:"redis" json:"redis"`
}
2.10.2增加yaml配置
name: 'user-web'
port: 8021
user_srv:
host: '127.0.0.1'
port: 50051
jwt:
key: 'OdpJe6U9GF5*H#JUyO2qzpaz69t1dojd'
sms:
key: '******************'
secrect: '*********************'
redis:
host: '127.0.0.1'
port: 6379
expire: 3600
2.10.3验证码写入缓存
package api
import (
"context"
"fmt"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"math/rand"
"mxshop_api/user-web/forms"
"mxshop_api/user-web/global"
"net/http"
"strings"
"time"
)
func GenerateSmsCode(witdh int) string {
//生成width长度的短信验证码
numeric := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
r := len(numeric)
rand.Seed(time.Now().UnixNano())
var sb strings.Builder
for i := 0; i < witdh; i++ {
fmt.Fprintf(&sb, "%d", numeric[rand.Intn(r)])
}
return sb.String()
}
func SendSms(ctx *gin.Context) {
sendSmsForm := forms.SendSmsForm{}
if err := ctx.ShouldBind(&sendSmsForm); err != nil {
HandleValidatorError(ctx, err)
return
}
client, err := dysmsapi.NewClientWithAccessKey("cn-beijing", global.ServerConfig.AliSmsInfo.ApiKey, global.ServerConfig.AliSmsInfo.ApiSecrect)
if err != nil {
panic(err)
}
smsCode := GenerateSmsCode(6)
request := requests.NewCommonRequest()
request.Method = "POST"
request.Scheme = "https" // https | http
request.Domain = "dysmsapi.aliyuncs.com"
request.Version = "2017-05-25"
request.ApiName = "SendSms"
request.QueryParams["RegionId"] = "cn-beijing"
request.QueryParams["PhoneNumbers"] = sendSmsForm.Mobile //手机号
request.QueryParams["SignName"] = "黑马旅游网" //阿里云验证过的项目名 自己设置
request.QueryParams["TemplateCode"] = "SMS_205126318" //阿里云的短信模板号 自己设置
request.QueryParams["TemplateParam"] = "{\"code\":" + smsCode + "}" //短信模板中的验证码内容 自己生成 之前试过直接返回,但是失败,加上code成功。
response, err := client.ProcessCommonRequest(request)
fmt.Print(client.DoAction(request, response))
if err != nil {
fmt.Print(err.Error())
}
//将验证码保存起来 - redis
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", global.ServerConfig.RedisInfo.Host, global.ServerConfig.RedisInfo.Port),
})
rdb.Set(context.Background(), sendSmsForm.Mobile, smsCode, time.Duration(global.ServerConfig.RedisInfo.Expire)*time.Second)
ctx.JSON(http.StatusOK, gin.H{
"msg": "发送成功",
})
}
2.11用户注册接口
2.11.1增加路由
在router目录下的user.go修改
package router
import (
"github.com/gin-gonic/gin"
"mxshop_api/user-web/api"
"mxshop_api/user-web/middlewares"
)
func InitUserRouter(Router *gin.RouterGroup) {
UserRouter := Router.Group("user")
{
UserRouter.GET("list", middlewares.JWTAuth(), middlewares.IsAdminAuth(), api.GetUserList)
UserRouter.POST("pwd_login", api.PassWordLogin)
//注册
UserRouter.POST("register", api.Register)
}
}
2.11.2增加API
func Register(c *gin.Context) {
//用户注册
registerForm := forms.RegisterForm{}
if err := c.ShouldBind(®isterForm); err != nil {
HandleValidatorError(c, err)
return
}
//验证码
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", global.ServerConfig.RedisInfo.Host, global.ServerConfig.RedisInfo.Port),
})
value, err := rdb.Get(context.Background(), registerForm.Mobile).Result()
if err == redis.Nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": "验证码错误",
})
return
} else {
if value != registerForm.Code {
c.JSON(http.StatusBadRequest, gin.H{
"code": "验证码错误",
})
return
}
}
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", global.ServerConfig.UserSrvInfo.Host,
global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure())
if err != nil {
zap.S().Errorw("[Register] 连接【用务服务失败】", "msg", err.Error())
}
//调用接口
//生成grpc的client并调用接口
userSrvClient := proto.NewUserClient(userConn)
user, err := userSrvClient.CreateUser(context.Background(), &proto.CreateUserInfo{
NickName: registerForm.Mobile,
PassWord: registerForm.PassWord,
Mobile: registerForm.Mobile,
})
if err != nil {
zap.S().Errorf("[Register] 查询 【新建用户失败】失败: %s", err.Error())
HandleGrpcErrorToHttp(err, c)
return
}
j := middlewares.NewJWT()
claims := models.CustomClaims{
ID: uint(user.Id),
NickName: user.NickName,
AuthorityId: uint(user.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": user.Id,
"nick_name": user.NickName,
"token": token,
"expired_at": (time.Now().Unix() + 60*60*24*30) * 1000,
})
}
2.11.3测试
在postman上运行,结果如下
输入的参数
{
"mobile":"13799378765",
"password":"123456",
"code":"487015"
}
返回的参数
{
"expired_at": 1699770928000,
"id": 12,
"nick_name": "13799378765",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MTIsIk5pY2tOYW1lIjoiMTM3OTkzNzg3NjUiLCJBdXRob3JpdHlJZCI6MSwiZXhwIjoxNjk5NzcwOTI4LCJpc3MiOiJpbW9vYyIsIm5iZiI6MTY5NzE3ODkyOH0.C8qrThtSVgGCS8qCVq_D1svqi0OPbHPwbagnCABubC0"
}
JSON(http.StatusBadRequest, gin.H{
"code": "验证码错误",
})
return
} else {
if value != registerForm.Code {
c.JSON(http.StatusBadRequest, gin.H{
"code": "验证码错误",
})
return
}
}
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", global.ServerConfig.UserSrvInfo.Host,
global.ServerConfig.UserSrvInfo.Port), grpc.WithInsecure())
if err != nil {
zap.S().Errorw("[Register] 连接【用务服务失败】", "msg", err.Error())
}
//调用接口
//生成grpc的client并调用接口
userSrvClient := proto.NewUserClient(userConn)
user, err := userSrvClient.CreateUser(context.Background(), &proto.CreateUserInfo{
NickName: registerForm.Mobile,
PassWord: registerForm.PassWord,
Mobile: registerForm.Mobile,
})
if err != nil {
zap.S().Errorf("[Register] 查询 【新建用户失败】失败: %s", err.Error())
HandleGrpcErrorToHttp(err, c)
return
}
j := middlewares.NewJWT()
claims := models.CustomClaims{
ID: uint(user.Id),
NickName: user.NickName,
AuthorityId: uint(user.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": user.Id,
"nick_name": user.NickName,
"token": token,
"expired_at": (time.Now().Unix() + 60*60*24*30) * 1000,
})
}
2.11.3测试
在postman上运行,结果如下
输入的参数
{
"mobile":"13799378765",
"password":"123456",
"code":"487015"
}
返回的参数
{
"expired_at": 1699770928000,
"id": 12,
"nick_name": "13799378765",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MTIsIk5pY2tOYW1lIjoiMTM3OTkzNzg3NjUiLCJBdXRob3JpdHlJZCI6MSwiZXhwIjoxNjk5NzcwOTI4LCJpc3MiOiJpbW9vYyIsIm5iZiI6MTY5NzE3ODkyOH0.C8qrThtSVgGCS8qCVq_D1svqi0OPbHPwbagnCABubC0"
}
本文围绕Golang微服务下的用户服务Web服务展开。先介绍基础项目架构,包括新建项目、使用zap日志库、集成到gin启动过程、viper配置文件管理等;接着阐述web层接口开发,如表单验证、自定义验证器、登录逻辑完善、集成JWT、解决跨域问题、获取验证码、短信发送、验证码缓存及用户注册接口测试等。
1528

被折叠的 条评论
为什么被折叠?



