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)
}


package global
import (
ut "github.com/go-playground/universal-translator"
"web_api/user_web/config"
)
var (
Trans ut.Translator
ServerConfig *config.ServerConfig = &config.ServerConfig{}
)
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"`
}
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
}
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)
}
}
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
}
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())
}
}



注意 -> validator要import v10的版本:
github.com/go-playground/validator/v10
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
}
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"`
}
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())
}
}
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": "登录失败",
})
}
}
}
}



xxxxx.yyyyy.zzzzz

//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'
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
}
package models
import (
"github.com/dgrijalva/jwt-go"
)
type CustomClaims struct {
ID uint
NickName string
AuthorityId uint
jwt.StandardClaims
}
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"`
}
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": "登录失败",
})
}
}
}
}


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)
}
}

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




c.Set("claims", claims)c.Set("userId", claims.ID)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)
//。。。省略



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)
}
}
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)
}
}

| 当前页面url | 被请求页面url | 是否跨域 | 原因 |
|---|---|---|---|
| http://www.test.com | http://www.test.com/index.html | 否 | 同源(协议、域名、端口号相同) |
| http://www.test.com | https://www.test.com/index.html | 跨域 | 协议不同(http/https) |
| http://www.test.com | http://www.baidu.com/ | 跨域 | 主域名不同(test/baidu) |
| http://www.test.com | http://blog.baidu.com/ | 跨域 | 子域名不同(www/blog) |
| http://www.test.com:8080/ | http://www.test.com:7001/ | 跨域 | 端口号不同(8080/7001) |
浏览器的预检请求结果可以通过设置Access-Control-Max-Age进行缓存
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>

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)
}
}
}
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
}
