这一篇将使用 jwt-go[1] 包来完成登录接口,颁发 token
令牌,并编写 jwt 中间件对 token
统一鉴权,避免在各个 controller
重复编写鉴权逻辑
go get -u github.com/dgrijalva/jwt-go
新建 config/jwt.go
文件,编写配置
package config
type Jwt struct {
Secret string `mapstructure:"secret" json:"secret" yaml:"secret"`
JwtTtl int64 `mapstructure:"jwt_ttl" json:"jwt_ttl" yaml:"jwt_ttl"` // token 有效期(秒)
}
在 config/config.go
中,添加 Jwt
属性
package config
type Configuration struct {
App App `mapstructure:"app" json:"app" yaml:"app"`
Log Log `mapstructure:"log" json:"log" yaml:"log"`
Database Database `mapstructure:"database" json:"database" yaml:"database"`
Jwt Jwt `mapstructure:"jwt" json:"jwt" yaml:"jwt"`
}
config.yaml
添加对应配置
jwt:
secret: 3Bde3BGEbYqtqyEUzW3ry8jKFcaPH17fRmTmqE7MDr05Lwj95uruRKrrkb44TJ4s
jwt_ttl: 43200
新建 app/services/jwt.go
文件,编写
package services
import (
"github.com/dgrijalva/jwt-go"
"jassue-gin/global"
"time"
)
type jwtService struct {
}
var JwtService = new(jwtService)
// 所有需要颁发 token 的用户模型必须实现这个接口
type JwtUser interface {
GetUid() string
}
// CustomClaims 自定义 Claims
type CustomClaims struct {
jwt.StandardClaims
}
const (
TokenType = "bearer"
AppGuardName = "app"
)
type TokenOutPut struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}
// CreateToken 生成 Token
func (jwtService *jwtService) CreateToken(GuardName string, user JwtUser) (tokenData TokenOutPut, err error, token *jwt.Token) {
token = jwt.NewWithClaims(
jwt.SigningMethodHS256,
CustomClaims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Unix() + global.App.Config.Jwt.JwtTtl,
Id: user.GetUid(),
Issuer: GuardName, // 用于在中间件中区分不同客户端颁发的 token,避免 token 跨端使用
NotBefore: time.Now().Unix() - 1000,
},
},
)
tokenStr, err := token.SignedString([]byte(global.App.Config.Jwt.Secret))
tokenData = TokenOutPut{
tokenStr,
int(global.App.Config.Jwt.JwtTtl),
TokenType,
}
return
}
CreateToken
方法需要接收一个 JwtUser
实例对象,我们需要将 app/models/user.go
用户模型实现 JwtUser
接口, 后续其他的用户模型都可以通过实现 JwtUser
接口,来调用 CreateToken()
颁发 Token
package models
import "strconv"
type User struct {
ID
Name string `json:"name" gorm:"not null;comment:用户名称"`
Mobile string `json:"mobile" gorm:"not null;index;comment:用户手机号"`
Password string `json:"-" gorm:"not null;default:'';comment:用户密码"`
Timestamps
SoftDeletes
}
func (user User) GetUid() string {
return strconv.Itoa(int(user.ID.ID))
}
在 app/common/request/user.go
中,新增 Login
验证器结构体
type Login struct {
Mobile string `form:"mobile" json:"mobile" binding:"required,mobile"`
Password string `form:"password" json:"password" binding:"required"`
}
func (login Login) GetMessages() ValidatorMessages {
return ValidatorMessages{
"mobile.required": "手机号码不能为空",
"mobile.mobile": "手机号码格式不正确",
"password.required": "用户密码不能为空",
}
}
在 app/services/user.go
中,编写 Login()
登录逻辑
// Login 登录
func (userService *userService) Login(params request.Login) (err error, user *models.User) {
err = global.App.DB.Where("mobile = ?", params.Mobile).First(&user).Error
if err != nil || !utils.BcryptMakeCheck([]byte(params.Password), user.Password) {
err = errors.New("用户名不存在或密码错误")
}
return
}
新建 app/controllers/app/auth.go
文件,编写 Login()
进行入参校验,并调用 UserService
和 JwtService
服务,颁发 Token
package app
import (
"github.com/gin-gonic/gin"
"jassue-gin/app/common/request"
"jassue-gin/app/common/response"
"jassue-gin/app/services"
)
func Login(c *gin.Context) {
var form request.Login
if err := c.ShouldBindJSON(&form); err != nil {
response.ValidateFail(c, request.GetErrorMsg(form, err))
return
}
if err, user := services.UserService.Login(form); err != nil {
response.BusinessFail(c, err.Error())
} else {
tokenData, err, _ := services.JwtService.CreateToken(services.AppGuardName, user)
if err != nil {
response.BusinessFail(c, err.Error())
return
}
response.Success(c, tokenData)
}
}
在 routes/api.go
中,添加路由
router.POST("/auth/login", app.Login)
使用 Apifox
调用http://localhost:8888/api/auth/login ,如下图,成功返回 Token
,登录成功
在 global/error.go
中,定义 TokenError
错误
type CustomErrors struct {
// ...
TokenError CustomError
}
var Errors = CustomErrors{
// ...
TokenError: CustomError{40100, "登录授权失效"},
}
在 app/common/response/response.go
中,编写 TokenFail()
,用于 token 鉴权失败统一返回
func TokenFail(c *gin.Context) {
FailByError(c, global.Errors.TokenError)
}
新建 app/middleware/jwt.go
文件,编写
package middleware
import (
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"jassue-gin/app/common/response"
"jassue-gin/app/services"
"jassue-gin/global"
)
func JWTAuth(GuardName string) gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr := c.Request.Header.Get("Authorization")
if tokenStr == "" {
response.TokenFail(c)
c.Abort()
return
}
tokenStr = tokenStr[len(services.TokenType)+1:]
// Token 解析校验
token, err := jwt.ParseWithClaims(tokenStr, &services.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(global.App.Config.Jwt.Secret), nil
})
if err != nil {
response.TokenFail(c)
c.Abort()
return
}
claims := token.Claims.(*services.CustomClaims)
// Token 发布者校验
if claims.Issuer != GuardName {
response.TokenFail(c)
c.Abort()
return
}
c.Set("token", token)
c.Set("id", claims.Id)
}
}
在 routes/api.go
中,使用 JWTAuth
中间件,这样一来,客户端需要使用正确的 Token
才能访问在 authRouter
分组下的路由
func SetApiGroupRoutes(router *gin.RouterGroup) {
router.POST("/auth/register", app.Register)
router.POST("/auth/login", app.Login)
authRouter := router.Group("").Use(middleware.JWTAuth(services.AppGuardName))
{
authRouter.POST("/auth/info", app.Info)
}
}
在 app/services/user.go
中,编写
// GetUserInfo 获取用户信息
func (userService *userService) GetUserInfo(id string) (err error, user models.User) {
intId, err := strconv.Atoi(id)
err = global.App.DB.First(&user, intId).Error
if err != nil {
err = errors.New("数据不存在")
}
return
}
在 app/controllers/auth.go
中,编写 Info()
,通过 JWTAuth
中间件校验 Token
识别的用户 ID 来获取用户信息
func Info(c *gin.Context) {
err, user := services.UserService.GetUserInfo(c.Keys["id"].(string))
if err != nil {
response.BusinessFail(c, err.Error())
return
}
response.Success(c, user)
}
使用 Apifox
,先将调用登录接口获取 Token
放入 Authorization
头,再调用接口 http://localhost:8888/api/auth/info