在第四节 Gin集成与Service层用户API开发中,我们实现了一些用户模块的Service层API。但并不是所有人都可以调用这些API,而是需要有一定的权限才能使用User API。具体在我们的项目中,该权限就是用户是否登录,也就是说,未登录的用户并不能接触到除了登录、注册外的任何API;而已登录用户就可以使用所有的API。
在本项目中,我们使用JWT授权与鉴权来实现登录权限的授予与检验。
JWT的简要介绍
- JWT是一种以json格式颁发的Web服务令牌,持有该令牌就可以获得一定权限,访问一些被保护的资源(如登录状态与游客状态)
- JWT授权、鉴权在中间件(middleware)中进行
- 对于加密算法而言,加密方式被分为:
- 对称加密(授权与鉴权使用同一份秘钥)
- 非对称加密(私钥生成token,公钥进行验证)
- 而JWT使用的加密算法是一种非对称加密
项目集成JWT
1. 拉取依赖
go get github.com/dgrijalva/jwt-go
2. 授权
授权需要生成一个特定于用户的token,并携带在服务器的返回信息中告知用户,往后用户的所有API请求都应该添加上token参数。我们在 middleware/jwt.go 下实现 token 的生成代码:
// Claims carry some information about users, such as user_id, expiredTime and issuer
type Claim struct {
UserId uint `json:"user_id"`
jwt.StandardClaims
}
// GenerateToken return a token generate by the user and issuer information
func GenerateToken(userId uint, issuer string) (string, error) {
// Set effective time of token
curTime := time.Now()
expiredTime := curTime.Add(5 * time.Minute)
// Set token information
claim := Claim{
UserId: userId,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expiredTime.Unix(),
Issuer: issuer,
},
}
// Get string token
tokenClaim := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
token, err := tokenClaim.SignedString(JwtSecret)
return token, err
}
在 /service/user.go 的用户登录API UserLoginByNameAndPwd 中需要添加生成token并传递给用户的代码:
// JWT identify
token, err := middleware.GenerateToken(Rsp.ID, "xy")
if err != nil {
zap.S().Info("Failed to generate token")
common.SendErrorResp(ctx.Writer, http.StatusInternalServerError, "Failed to generate token", nil)
return
}
data := make(map[string]string)
data["token"] = token
data["userId"] = strconv.Itoa(int(Rsp.ID))
common.SendNormalResp(ctx.Writer, "Success to Login in", data, user, 1)
3. 鉴权
鉴权的核心逻辑是从token中解析出Claim结构体以提取出用户信息
// ParseToken get the claim information from input token
func ParseToken(token string) (*Claim, error) {
tokenClaim, err := jwt.ParseWithClaims(token, &Claim{}, func(token *jwt.Token) (interface{}, error) {
return JwtSecret, nil
})
if tokenClaim != nil {
// exchange type: *Token -> *Claim
if claim, ok := tokenClaim.Claims.(*Claim); ok && tokenClaim.Valid {
return claim, nil
}
}
return nil, err
}
鉴权的流程:
- 从请求行参数中获取token与id
- 将id转为uint类型,判断id是否合法
- 然后判断token是否为空,不为空则尝试解析token
- 最后判断token是否过期
注意:在鉴权函数中必须调用 ctx.next() 以将控制权交给下一个中间件或者handler函数,否则会一直停在该中间件中
// Authentication used in middleware to check if token validate
func Authentication() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := ctx.Request.PostFormValue("token")
id := ctx.Request.PostFormValue("id")
userId, err := strconv.Atoi(id)
if err != nil {
zap.S().Info("Illegal UserId")
ctx.JSON(http.StatusUnauthorized, gin.H{
"msg": "Illegal UserId",
})
ctx.Abort()
return
}
if token == "" {
zap.S().Info("Not Login in yet")
ctx.JSON(http.StatusUnauthorized, gin.H{
"msg": "Please Login in",
})
ctx.Abort()
return
} else {
claim, err := ParseToken(token)
if err != nil {
zap.S().Info("token invalidity")
ctx.JSON(http.StatusUnauthorized, gin.H{
"msg": "token invalidity, Please Login in again",
})
ctx.Abort()
return
} else if claim.ExpiresAt < time.Now().Unix() {
zap.S().Info("token expired")
ctx.JSON(http.StatusUnauthorized, gin.H{
"msg": "token expired, Please Login in again",
})
ctx.Abort()
return
}
if claim.UserId != uint(userId) {
zap.S().Info("Illegal Login in")
ctx.JSON(http.StatusUnauthorized, gin.H{
"msg": "Login Illegal",
})
ctx.Abort()
return
}
fmt.Print("Success to Login in")
ctx.Next()
}
}
}
- 在路由中添加鉴权步骤
- 注意在登录API与注册API不应该添加鉴权
user.GET("/list", middleware.Authentication(), service.UserList)
user.POST("/login", service.UserLoginByNameAndPwd)
user.POST("/new", service.UserRegister)
user.POST("/update", middleware.Authentication(), service.UpdateUserInformation)
user.DELETE("/delete", middleware.Authentication(), service.DeleteUser)
注意在Gin框架中,鉴权函数更推荐以下格式:
func func_name() gin.HandlerFunc{
return func(ctx *gin.Context) {...}
}
MD5加密
由于用户的登录密码并不建议使用明文存储在数据库中,因此需要对密码进行加密,并存储在数据库中。在本项目中,我们使用MD5算法与加盐算法对用户密码进行加密操作。具体信息可以查阅CSDN文章:加密 / MD5算法 /盐值
盐值
在用户注册时,服务器会自动生成一个随机的字符串作为该用户的盐值
// Generate Salt value
salt := fmt.Sprintf("%d", rand.Int31())
user.Salt = salt
Md5加密函数
在 common/password_encoder.go 下实现md5加密函数:
import (
"crypto/md5"
"encoding/hex"
"io"
)
// Md5Encoder Encode the PassWord by Md5
func Md5Encoder(code string) string {
m := md5.New()
io.WriteString(m, code)
return hex.EncodeToString(m.Sum(nil))
}
加盐密码生成和验证
同样在 common/password_encoder.go 下添加加盐密码生成和验证函数:
// CheckPassword Encode the input password and compare the password in DB
func CheckPassword(curPwd string, salt string, dbPwd string) bool {
pwd := SaltPassword(curPwd, salt)
return pwd == dbPwd
}
// SaltPassword return the password with salt
func SaltPassword(pwd string, salt string) string {
saltPwd := fmt.Sprintf("%s%s", Md5Encoder(pwd), salt)
return saltPwd
}