这是我参与「第五届青训营 」伴学笔记创作活动的第 14 天
所用依赖:
- github.com/gin-gonic/gin
- github.com/jinzhu/gorm
- github.com/go-redis/redis
- github.com/dgrijalva/jwt-go
- golang.org/x/crypto/bcrypt
连接到MySQL和Redis数据库,设置Gin框架。定义两个路由,一个是登录路由,一个是需要身份验证的用户资料路由。路由函数之前定义一个authMiddleware中间件函数,以实现鉴权
package main
import (
"github.com/gin-gonic/gin"
"github.com/go-redis/redis"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
var (
db *gorm.DB
cache *redis.Client
)
func main() {
// 连接数据库
var err error
db, err = gorm.Open("mysql", "root:@tcp(127.0.0.1:3306)/users?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
defer db.Close()
// 连接 Redis
cache = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置 Gin
r := gin.Default()
// 注册路由
r.POST("/login", loginHandler)
r.GET("/profile", authMiddleware(), profileHandler)
// 启动服务器
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
这个处理程序将接收一个JSON请求体,其中包含username和password字段。检查这些凭据是否与数据库中的任何记录匹配。如果是,将生成一个JWT令牌,并将其存储在Redis中。使用generateToken函数来生成JWT令牌。
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis"
"github.com/jinzhu/gorm"
"github.com/dgrijalva/jwt-go"
"example.com/auth-example/models"
)
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
}
func loginHandler(c *gin.Context) {
var req LoginRequest
if err := c.BindJSON(&req); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user models.User
if err := db.Where("username = ?", req.Username).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !user.CheckPassword(req.Password) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
token, err := generateToken(user.ID)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
cache.Set(token, user.ID, 0)
c.JSON(http.StatusOK, LoginResponse{Token: token})
}
func generateToken(userID uint) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userID,
})
return token.SignedString([]byte("secret"))
}
中间件函数
package middlewares
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis"
"github.com/dgrijalva/jwt
type AuthenticatedUser struct {
ID uint
Username string
}
func authMiddleware(cache *redis.Client) gin.HandlerFunc {
return func(c *gin.Context) {
tokenHeader := c.GetHeader("Authorization")
if tokenHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"})
return
}
tokenParts := strings.Split(tokenHeader, " ")
if len(tokenParts) != 2 || strings.ToLower(tokenParts[0]) != "bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header"})
return
}
tokenString := tokenParts[1]
claims := jwt.MapClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return []byte("secret"), nil
})
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
if !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
userID := uint(claims["user_id"].(float64))
cachedID, err := cache.Get(tokenString).Uint64()
if err != nil || cachedID != uint64(userID) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
c.Set("user", AuthenticatedUser{ID: userID})
c.Next()
}
}
这个中间件函数将检查请求头中的授权标头,并将其解析为JWT令牌。如果令牌无效,中间件将中止请求并返回`401 Unauthorized`响应。否则,从令牌中提取用户ID,并检查该令牌是否存在于Redis缓存中,如果是,继续处理请求。