网站首页 > 技术文章 正文
上一节,我们简单介绍了JWT。本节让我们使用它完成用户登录的代码编写。
还是先从models/user.go文件开始编写,写入一个结构体JwtClaims:
// JwtClaims用于生成Jwt token
type JwtClaims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
jwt.StandardClaims
}
以上代码定义了一个名为 JwtClaims 的结构体,该结构体通常用于在 Go 应用程序中生成 JSON Web 令牌 (JWT)
- User、Username 这两个字段就不再赘述了。
- jwt.StandardClaims: 这行代码嵌入了来自 jwt 包(github.com/dgrijalva/jwt-go)的 StandardClaims 结构体。嵌入允许您在自己的结构体内重用另一个结构体的字段和方法。
总的来说 JwtClaims 结构体旨在保存生成 JWT 相关的信息。它包括用户身份验证详细信息(UserID 和 Username),也包括来自 jwt 包定义的标准声明。
再写入一个验证密码的函数CheckPassword:
// 验证密码
func CheckPassword(hashedPassword, plainPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))
return err == nil
}
CheckPassword 的函数接收两个字符串参数:
- hashedPassword: 存储的哈希密码 (来自数据库)
- plainPassword: 用户输入的密码 (未加密)
函数返回一个布尔值 (bool),表示密码匹配 (true) 或不匹配 (false)。
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))这行代码使用 bcrypt 包的 CompareHashAndPassword 函数进行密码比较。 它将两个切片 ([]byte) 作为参数:
- 第一个切片 ([]byte(hashedPassword)) 将存储的哈希密码转换为字节切片。
- 第二个切片 ([]byte(plainPassword)) 将用户输入的密码转换为字节切片。
CompareHashAndPassword 函数比较两个密码的哈希值之后将其结果存储在 err 变量中。
return err == nil 通过比较 err 是否为 nil 来判断密码是否匹配,并返回相应的布尔值 (true 或 false)。
当有了存储JWT的结构体和验证密码的函数后,我们就可以去写生成用户令牌和用户登录的的代码了。
在utils目录中新建文件jwt.go,并写入如下内容:
package utils
import (
"time"
"xblog/models"
"github.com/dgrijalva/jwt-go"
)
var jwtSecret = []byte("my_secret_key")
// GenerateToken生成用户令牌
func GenerateToken(userID uint, username string) (string, error) {
now := time.Now()
claims := models.JwtClaims{
UserID: userID,
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: now.Add(time.Hour * 24 * 7).Unix(), // 一周有效期
Issuer: "xblog",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
此文件目前只包含了一个函数(GenerateToken),也是我要重点介绍的。
- var jwtSecret = []byte("my_secret_key") 定义了一个名为 jwtSecret 的全局变量,用于存储 JWT 签名使用的密钥。注意,这是一个示例密钥,请勿将其用于生产环境中。
- func GenerateToken(userID uint, username string) (string, error) 定义了一个名为 GenerateToken 的函数。函数接收两个参数:userID: 用户 ID (类型为 uint) username: 用户名 (类型为 string)。该函数返回两个值:生成的 JWT 令牌 (类型为 string) 发生错误时返回的错误对象 (类型为 error)
- now := time.Now(): 获取当前时间。
- claims := models.JwtClaims{ ... } 创建一个 models.JwtClaims 结构体的实例 (models.JwtClaims 已经在 xblog/models 包中完成)。设置结构体字段的值: UserID: 使用传入的 userID 参数。Username: 使用传入的 username 参数。StandardClaims: 嵌入 jwt.StandardClaims 结构体,并设置以下字段: ExpiresAt: 令牌的过期时间为当前时间加上 7 天 (使用 time.Hour * 24 * 7)。 Issuer: 令牌的颁发者为 "xblog"。
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims): 使用 jwt-go 库创建新的 JWT 令牌。第一个参数指定签名方法为 HMAC SHA-256 ( jwt.SigningMethodHS256 )。第二个参数是之前创建的 claims 结构体。
- return token.SignedString(jwtSecret): 使用预先定义的 jwtSecret 密钥对令牌进行签名,并返回签名的字符串
总的来说 GenerateToken 函数用于生成一个包含用户 ID、用户名、过期时间和颁发者信息的 JWT 令牌。这个令牌可以用来验证用户身份并授予访问权限。
在utils目录中新建文件auth.go,也许只看文件名你就能想到它的用途。写入如下内容:
package utils
import (
"errors"
"net/http"
"strings"
"xblog/models"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// Login 用户登陆
func Login(username, password string) (user *models.User, err error) {
user = &models.User{}
// 直接查询用户。如果存在,则加载详细信息;如果不存在,则返回错误信息
result := models.DB.Where("username = ?", username).First(user)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, errors.New("用户名或密码错误") // 返回通用错误信息
} else if result.Error != nil {
return nil, result.Error // 返回其它数据库查询错误
}
// 验证密码
if !models.CheckPassword(user.Password, password) {
return nil, errors.New("用户名或密码错误") // 返回通用错误信息
}
return user, nil
}
// RequireAuth是验证Jwt的中间件
func RequireAuth(c *gin.Context) {
authToken := c.GetHeader("Authorization")
if authToken == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
c.Abort()
return
}
parts := strings.Fields(authToken)
if len(parts) < 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "认证令牌格式错误"})
c.Abort()
return
}
tokenStr := parts[1]
claims := &models.JwtClaims{}
token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "认证令牌无效"})
c.Abort()
return
}
// 验证通过后,将用户信息设置到Context中,供后续处理函数使用
c.Set("currentUser", claims)
c.Next()
}
Login 该函数用于用户登录,验证用户名和密码是否正确,并返回用户信息。它接收两个参数:username: 用户名 password: 密码 (类型是 string)。同样返回两值:user: 登录成功后返回的用户信息 (类型为 *models.User) err: 登录失败时返回的错误信息 (类型为 error)
函数内部逻辑:
- 使用 models.DB 查询是否存在指定用户名的用户。
- 如果用户不存在,则返回 errors.New("用户名或密码错误") 错误。
- 如果用户存在,则加载用户详细信息。
- 使用 models.CheckPassword 函数验证用户输入的密码是否正确。
- 如果密码错误,则返回 errors.New("用户名或密码错误") 错误。
- 如果验证通过,则返回用户信息和 nil 错误。
RequireAuth 该函数用于验证用户是否携带并拥有有效的 JWT 令牌。它只接收一个类型为 *gin.Context的参数(Gin 框架的上下文)
函数内部逻辑:
- 从请求头中获取 Authorization 字段的值,即 JWT 令牌。
- 如果 Authorization 字段为空,则返回 http.StatusUnauthorized 状态码和错误信息 "未提供认证令牌"。
- 将 Authorization 字段的值分割成两部分,并检查格式是否正确。
- 如果格式不正确,则返回 http.StatusUnauthorized 状态码和错误信息 "认证令牌格式错误"。
- 使用 jwt.ParseWithClaims 函数解析 JWT 令牌,并将其结果存储在 claims 结构体中。
- 如果解析失败或令牌无效,则返回 http.StatusUnauthorized 状态码和错误信息 "认证令牌无效"。
- 如果验证通过,则将用户信息设置到 Gin 上下文中,供后续处理函数使用。
总结:这两个函数共同实现了用户认证功能,确保只有拥有有效令牌的用户才能访问需要权限的资源。
接下来就是API接口和路由了。
打开api/user.go文件,编写如下代码:
func UserLogin(c *gin.Context) {
var u models.User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
user, err := utils.Login(u.Username, u.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": err.Error(),
})
return
}
token, err := utils.GenerateToken(user.ID, user.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
})
}
UserLogin 该函数用于处理用户登录请求。让我们来详细分解一下:
1. var u models.User
- 声明了一个名变量 u,类型为 models.User。
2. if err := c.ShouldBindJSON(&u); err != nil {
- 尝试从请求体中解析 JSON 数据并将其绑定到变量 u 上。
- 如果解析过程中出现错误 (err != nil),则执行后续代码块。
3. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- 解析 JSON 数据出错时执行到这里。
- 向客户端返回 JSON 响应,状态码为 http.StatusBadRequest (请求格式错误)。
- 响应体包含 "error" 键,值为解析错误信息 err.Error()。
4. user, err := utils.Login(u.Username, u.Password)
- 调用 utils.Login 函数尝试登录用户,使用 u.Username 和 u.Password 进行验证。
- 登录成功后,user 变量会存储返回的用户信息 (类型为 models.User)。
5. if err != nil {
- 检查 utils.Login 函数返回的错误 (err)。
- 如果登录失败 (err != nil) 执行后续代码块。
6. c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
- 向客户端返回 JSON 响应,状态码为 http.StatusUnauthorized (未授权)。
- 响应体包含 "error" 键,值为登录失败信息 err.Error()。
7. token, err := utils.GenerateToken(user.ID, user.Username)
- 如果登录成功,调用 utils.GenerateToken 函数生成 JWT 令牌。
- 使用登录成功返回的用户信息 (user.ID 和 user.Username) 作为参数生成令牌。
- 生成的 JWT 令牌会存储在 token 变量中。
8. if err != nil {
- 检查 utils.GenerateToken 函数返回的错误 (err)。
- 生成令牌失败 (err != nil) 时执行后续代码块。
9. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- 向客户端返回 JSON 响应,状态码为 http.StatusInternalServerError (服务器内部错误)。
- 响应体包含 "error" 键,值为生成令牌失败信息 err.Error()。
10. c.JSON(http.StatusOK, gin.H{"token": token})
- 如果登录和生成令牌都成功,使用 c.JSON 方法向客户端返回 JSON 响应。
- 状态码为 http.StatusOK (请求成功)。
- 响应体包含 "token" 键,值为生成的 JWT 令牌 (token)。
完成上述代码后保存,如果IDE不能自动添加"xblog/utils",我们在import中手动添加它。
最后一步。在router.go文件(路由)中写入用户登录的路由:
public := r.Group("api/v1")
{
public.POST("user/add", api.UserAdd)
public.POST("user/login", api.UserLogin)
}
好,接下来我们使用如下CURL测试一下:
# 测试Login
curl -X POST \
-H "Content-Type: application/json" \
-d '{"username":"user1","password":"passwd123"}' \
http://10.0.0.185:8000/api/v1/user/login; echo
由图片可知,程序成功返回用户登录后的token。
猜你喜欢
- 2024-09-09 混合云资产管理项目(二)(混合云存储产品有哪些)
- 2024-09-09 Go语言进阶之Go语言高性能Web框架Iris项目实战-完善用户管理EP04
- 2024-09-09 数据库与 Go 的交互(go数据库和kegg数据库)
- 2024-09-09 七爪源码:N+1 查询如何烧毁您的数据库
- 2024-09-09 Go的安全编程和防御性编程(防止代码注入)
- 2024-09-09 Vue3+Go 仿抖音项目架构设计与实现
- 2024-09-09 腾讯Go安全指南(腾讯官网最新安全公告)
- 2024-09-09 Grails指南24查询高阶(grails中文参考手册)
- 2024-09-09 Redis优化高并发下的秒杀性能(redis秒杀高并发代码)
- 2024-09-09 10.Go语言编写个人博客 文章分类(基于golang的个人博客系统)
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)