(十五)密码加密和jwt认证
- 分类
- 未分类
文章目录 7 节
开始前的准备
$ go get golang.org/x/crypto
$ go get github.com/golang-jwt/jwt
开始编码
密码加密
pkg/hash/hash.go
package hash
import (
"go-api-practice/pkg/logger"
"golang.org/x/crypto/bcrypt"
)
func BcryptHash(password string) string {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
logger.LogIf(err)
return string(bytes)
}
func BcryptCheck(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func BcryptIsHashed(str string) bool {
return len(str) == 60
}
这里要注意的是BcryptCheck()函数的参数位置不能错误,否则函数将无法正常运行
添加钩子函数
app/models/user/user_hooks.go
package user
import (
"go-api-practice/pkg/hash"
"gorm.io/gorm"
)
func (userModel *User) BeforeSave(tx *gorm.DB) (err error) {
if !hash.BcryptIsHashed(userModel.Password) {
userModel.Password = hash.BcryptHash(userModel.Password)
}
return
}
这里我们使用了gorm的钩子函数,注意参数和函数名都不要出错
验证密码
app/models/user/user_model.go
func (userModel *User) ComparePassword(_password string) bool {
return hash.BcryptCheck(_password, userModel.Password)
}
测试密码加密


可以看到密码已经被加密

jwt授权
pkg/jwt/jwt.go
package jwt
import (
"errors"
"github.com/gin-gonic/gin"
jwtpkg "github.com/golang-jwt/jwt"
"go-api-practice/pkg/app"
"go-api-practice/pkg/config"
"go-api-practice/pkg/logger"
"strings"
"time"
)
var (
ErrTokenExpired error = errors.New("令牌已过期")
ErrTokenExpiredMaxRefresh error = errors.New("令牌已过最大刷新时间")
ErrTokenMalformed error = errors.New("请求令牌格式有误")
ErrTokenInvalid error = errors.New("请求令牌无效")
ErrHeaderEmpty error = errors.New("需要认证才能访问!")
ErrHeaderMalformed error = errors.New("请求头中 Authorization 格式有误")
)
// JWT 定义一个jwt对象
type JWT struct {
// 秘钥,用以加密 JWT,读取配置信息 app.key
SignKey []byte
// 刷新 Token 的最大过期时间
MaxRefresh time.Duration
}
// JWTCustomClaims 自定义载荷
type JWTCustomClaims struct {
UserID string `json:"user_id"`
UserName string `json:"user_name"`
ExpireAtTime int64 `json:"expire_time"`
// StandardClaims 结构体实现了 Claims 接口继承了 Valid() 方法
// JWT 规定了7个官方字段,提供使用:
// - iss (issuer):发布者
// - sub (subject):主题
// - iat (Issued At):生成签名的时间
// - exp (expiration time):签名过期时间
// - aud (audience):观众,相当于接受者
// - nbf (Not Before):生效时间
// - jti (JWT ID):编号
jwtpkg.StandardClaims
}
func NewJWT() *JWT {
return &JWT{
SignKey: []byte(config.GetString("app.key")),
MaxRefresh: time.Duration(config.GetInt64("jwt.max_refresh_time")) * time.Minute,
}
}
// ParserToken 解析 Token,中间件中调用
func (jwt *JWT) ParserToken(c *gin.Context) (*JWTCustomClaims, error) {
tokenString, parseErr := jwt.getTokenFromHeader(c)
if parseErr != nil {
return nil, parseErr
}
// 1. 调用 jwt 库解析用户传参的 Token
token, err := jwt.parseTokenString(tokenString)
// 2. 解析出错
if err != nil {
validationErr, ok := err.(*jwtpkg.ValidationError)
if ok {
if validationErr.Errors == jwtpkg.ValidationErrorMalformed {
return nil, ErrTokenMalformed
} else if validationErr.Errors == jwtpkg.ValidationErrorExpired {
return nil, ErrTokenExpired
}
}
return nil, ErrTokenInvalid
}
// 3. 将 token 中的 claims 信息解析出来和 JWTCustomClaims 数据结构进行校验
if claims, ok := token.Claims.(*JWTCustomClaims); ok && token.Valid {
return claims, nil
}
return nil, ErrTokenInvalid
}
// RefreshToken 更新 Token,用以提供 refresh token 接口
func (jwt *JWT) RefreshToken(c *gin.Context) (string, error) {
// 1. 从 Header 里获取 token
tokenString, parseErr := jwt.getTokenFromHeader(c)
if parseErr != nil {
return "", parseErr
}
// 2. 调用 jwt 库解析用户传参的 Token
token, err := jwt.parseTokenString(tokenString)
// 3. 解析出错,未报错证明是合法的 Token(甚至未到过期时间)
if err != nil {
validationErr, ok := err.(*jwtpkg.ValidationError)
// 满足 refresh 的条件:只是单一的报错 ValidationErrorExpired
if !ok || validationErr.Errors != jwtpkg.ValidationErrorExpired {
return "", err
}
}
// 4. 解析 JWTCustomClaims 的数据
claims := token.Claims.(*JWTCustomClaims)
// 5. 检查是否过了『最大允许刷新的时间』
x := app.TimenowInTimezone().Add(-jwt.MaxRefresh).Unix()
if claims.IssuedAt > x {
// 修改过期时间
claims.StandardClaims.ExpiresAt = jwt.expireAtTime()
return jwt.createToken(*claims)
}
return "", ErrTokenExpiredMaxRefresh
}
// IssueToken 生成 Token,在登录成功时调用
func (jwt *JWT) IssueToken(userID string, userName string) string {
// 1. 构造用户 claims 信息(负荷)
expireAtTime := jwt.expireAtTime()
claims := JWTCustomClaims{
userID,
userName,
expireAtTime,
jwtpkg.StandardClaims{
NotBefore: app.TimenowInTimezone().Unix(), // 签名生效时间
IssuedAt: app.TimenowInTimezone().Unix(), // 首次签名时间(后续刷新 Token 不会更新)
ExpiresAt: expireAtTime, // 签名过期时间
Issuer: config.GetString("app.name"), // 签名颁发者
},
}
// 2. 根据 claims 生成token对象
token, err := jwt.createToken(claims)
if err != nil {
logger.LogIf(err)
return ""
}
return token
}
// createToken 创建 Token,内部使用,外部请调用 IssueToken
func (jwt *JWT) createToken(claims JWTCustomClaims) (string, error) {
// 使用HS256算法进行token生成
token := jwtpkg.NewWithClaims(jwtpkg.SigningMethodHS256, claims)
return token.SignedString(jwt.SignKey)
}
// expireAtTime 过期时间
func (jwt *JWT) expireAtTime() int64 {
timenow := app.TimenowInTimezone()
var expireTime int64
if config.GetBool("app.debug") {
expireTime = config.GetInt64("jwt.debug_expire_time")
} else {
expireTime = config.GetInt64("jwt.expire_time")
}
expire := time.Duration(expireTime) * time.Minute
return timenow.Add(expire).Unix()
}
// parseTokenString 使用 jwtpkg.ParseWithClaims 解析 Token
func (jwt *JWT) parseTokenString(tokenString string) (*jwtpkg.Token, error) {
return jwtpkg.ParseWithClaims(tokenString, &JWTCustomClaims{}, func(token *jwtpkg.Token) (interface{}, error) {
return jwt.SignKey, nil
})
}
// getTokenFromHeader 使用 jwtpkg.ParseWithClaims 解析 Token
// Authorization:Bearer xxxxx
func (jwt *JWT) getTokenFromHeader(c *gin.Context) (string, error) {
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" {
return "", ErrHeaderEmpty
}
// 按空格分割
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
return "", ErrHeaderMalformed
}
return parts[1], nil
}
这是工具类
时区
pkg/app/app.go
func TimenowInTimezone() time.Time {
chinaTimezone, _ := time.LoadLocation(config.GetString("app.timezone"))
return time.Now().In(chinaTimezone)
}
配置信息
config/jwt.go
package config
import "go-api-practice/pkg/config"
func init() {
config.Add("jwt", func() map[string]interface{} {
return map[string]interface{}{
// 使用 config.GetString("app.key")
// "signing_key":
// 过期时间,单位是分钟,一般不超过两个小时
"expire_time": config.Env("JWT_EXPIRE_TIME", 120),
// 允许刷新时间,单位分钟,86400 为两个月,从 Token 的签名时间算起
"max_refresh_time": config.Env("JWT_MAX_REFRESH_TIME", 86400),
// debug 模式下的过期时间,方便本地开发调试
"debug_expire_time": 86400,
}
})
}
获得字符串ID
app/models/model.go
func (a BaseModel) GetStringID() string {
return cast.ToString(a.ID)
}
在控制器中调用
app/http/controllers/api/v1/auth/signup_controller.go
func (sc *SignupController) SignupUsingPhone(c *gin.Context) {
request := requests.SignupUsingPhoneRequest{}
if ok := requests.Validate(c, &request, requests.SignupUsingPhone); !ok {
return
}
userModel := user.User{
Name: request.Name,
Phone: request.Phone,
Password: request.Password,
}
userModel.Create()
if userModel.ID > 0 {
token := jwt.NewJWT().IssueToken(userModel.GetStringID(), userModel.Name)
response.CreatedJSON(c, gin.H{
"token": token,
"data": userModel,
})
} else {
response.Abort500(c, "创建用户失败")
}
}
func (sc *SignupController) SignupUsingEmail(c *gin.Context) {
// 1. 验证表单
request := requests.SignupUsingEmailRequest{}
if ok := requests.Validate(c, &request, requests.SignupUsingEmail); !ok {
return
}
// 2. 验证成功,创建数据
userModel := user.User{
Name: request.Name,
Email: request.Email,
Password: request.Password,
}
userModel.Create()
if userModel.ID > 0 {
token := jwt.NewJWT().IssueToken(userModel.GetStringID(), userModel.Name)
response.CreatedJSON(c, gin.H{
"token": token,
"data": userModel,
})
} else {
response.Abort500(c, "创建用户失败,请稍后尝试~")
}
}
测试jwt
我们用之前的API在来注册一个用户,成功如下
