网站首页 > 技术教程 正文
上一节,我们简单介绍了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-10-22 3. 使用Go语言编写个人博客 编写数据库连接与用户模型
- 2024-10-22 如何搭建WordPress个人博客网站 wordpress搭建个人博客教程
- 2024-10-22 修改ZBLOG后台地址如此重要 zblog主题修改
- 2024-10-22 如何搭建个人博客网站?角点科技教你搭建博客网站
- 2024-10-22 4. 使用Go语言编写个人博客 新增用户并使用CURL测试
- 2024-10-22 2. 使用Go语言编写个人博客 目录结构概述与创建
- 2024-10-22 个人独立博客还有存在的必要吗? 知名的独立博客
- 2024-10-22 作为一个程序员居然还没有自己的博客?
- 2024-10-22 Python自动化运维系列:Django搭建个人博客之前端篇
- 2024-10-22 0基础新手小白个人博客建站之三:网站源码上传、主机调试
你 发表评论:
欢迎- 最近发表
-
- 函数公式的7大潜规则,这次给你讲透了
- 数据逆向查找不止有vlookup,你该知道的三种逆向查询操作
- Vlookup函数怎么一次查找能返回多个结果?
- vlookup函数的嵌套你用过吗?一次可以引用3个表格的数据
- Vlookup函数的新用法,查询合并单元格,很多Excel高手都不知道
- 分明有数据,公式也没错,为什么vlookup还是会返回错误值
- 条件判断还在用if函数就out了,vlookup函数模糊查询一键完成
- EXCEL函数 VLOOKUP函数 HLOOKUP函数
- excel中vlookup函数的用法(excel中vlookup函数公式)
- 自动获取vlookup函数的第三参数,再也不用一列一列的数了
- 标签列表
-
- sd分区 (65)
- raid5数据恢复 (81)
- 地址转换 (73)
- 手机存储卡根目录 (55)
- tcp端口 (74)
- project server (59)
- 双击ctrl (55)
- 鼠标 单击变双击 (67)
- debugview (59)
- 字符动画 (65)
- flushdns (57)
- ps复制快捷键 (57)
- 清除系统垃圾代码 (58)
- web服务器的架设 (67)
- 16进制转换 (69)
- xclient (55)
- ps源文件 (67)
- filezilla server (59)
- 句柄无效 (56)
- word页眉页脚设置 (59)
- ansys实例 (56)
- 6 1 3固件 (59)
- sqlserver2000挂起 (59)
- vm虚拟主机 (55)
- config (61)
本文暂时没有评论,来添加一个吧(●'◡'●)