代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/24-gin-learning
一:主要功能
- 用户可以注册、登录和注销登录
- 用户登录后可以创建、修改、查看和删除自己的待办事项
- 用户对待办事项的增删改查有身份验证的过程
工作中,实际进入开发前一般都会先出技术方案,其中基本就会包含整体架构图,服务链路拓扑图,特定功能时序图或执行流程图等,画好图有如下好处
- 显示代码的执行流程,包括各个模块之间的调用顺序和控制流程
- 说明代码中各个模块之间的关系和依赖
- 帮助开发人员更好的理解和调试代码
- 对代码进行分析和优化
- 提高代码的可读性、可维护性和可复用性
二:model实体
从主要功能可以看出,主要需要用户实体和待办事项实体
type User struct {
gorm.Model
Username string `json:"username" gorm:"unique"` // 唯一键
Password string `json:"password"`
}
// 待办事项结构体
type Todo struct {
gorm.Model
Title string `json:"title"`
Status string `json:"status"`
UserId int `json:"user_id"`
}
三:数据库初始化
func initDB() (*gorm.DB, error) {
dsn := "root:root@(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=true&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(any("failed to connect to db"))
}
// 建表
err = db.AutoMigrate(&User{}, &Todo{})
if err != nil {
return nil, err
}
return db, nil
}
四:用户注册与登录
注册:
登录:
// 用户注册
func signUp(c *gin.Context, db *gorm.DB) {
var user User
if err := c.BindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 用户名为唯一键,不允许重复,所以根据用户名查询是否已经存在同名的用户
var existingUser User
err := db.Where("username = ?", user.Username).First(existingUser).Error
if err != nil && err != gorm.ErrRecordNotFound {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if existingUser.ID != 0 {
c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在!"})
return
}
// 用户密码加密入库
password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// hash加密后的密码赋值给用户,从而入库
user.Password = string(password)
err = db.Create(&user).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, user)
}
// 用户登录
func signIn(c *gin.Context, db *gorm.DB) {
var user User
if err := c.BindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 查询用户是否存在,存在则查出用户,然后对比加密的密码是否一致
var existingUser User
err := db.Where("username = ?", user.Username).First(&existingUser).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if existingUser.ID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误!"})
return
}
// 比对密码
err = bcrypt.CompareHashAndPassword([]byte(existingUser.Password), []byte(user.Password))
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误!"})
return
}
// 用户名和token都校验通过了,生成token返回
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": existingUser.ID,
"username": existingUser.Username,
})
// 对token进行加盐操作
signedToken, err := token.SignedString([]byte(secretKey))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 加盐后的token返回
c.JSON(http.StatusOK, gin.H{"token": signedToken})
}
五:待办事项增删改查
用于操作待办事项前的鉴权中间件:
创建:
查询:
更新:
删除:
// 用于操作待办事项前的鉴权中间件
func authenticationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("token")
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "权限校验未通过,请检查token是否传递"})
c.Abort()
return
}
// 解析token,拿到token中的用户名和id等信息
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil
})
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
c.Abort()
return
}
// token无效或者过期了
if !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
c.Abort()
return
}
claims := token.Claims.(jwt.MapClaims)
// 将用户id设置在context上下文中,断言为float64后转为int
c.Set("userID", int(claims["id"].(float64)))
c.Next() // 执行下一个handler
}
}
// 创建待办事项
func createTodo(c *gin.Context, db *gorm.DB) {
var todo Todo
if err := c.BindJSON(&todo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
todo.UserId = c.GetInt("userID")
db.Create(&todo)
c.JSON(http.StatusOK, todo)
}
// 查询待办事项
func getTodos(c *gin.Context, db *gorm.DB) {
var todos []Todo
db.Where("user_id = ?", c.GetInt("userID")).Find(&todos)
c.JSON(http.StatusOK, todos)
}
// 更新待办事项
func updateTodo(c *gin.Context, db *gorm.DB) {
id, err := strconv.Atoi(c.Param("id")) // 从路径参数中取出需要更新的待办事项id
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var updateTodo Todo
if err := c.BindJSON(&updateTodo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 更新操作需要指明model,从而知道是哪个表
db.Model(&Todo{}).Where("id = ? and user_id = ?", id, c.GetInt("userID")).Updates(updateTodo)
c.JSON(http.StatusOK, updateTodo)
}
// 删除待办事项
func deleteTodo(c *gin.Context, db *gorm.DB) {
id, err := strconv.Atoi(c.Param("id")) // 从路径参数中取出需要更新的待办事项id
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var todo Todo
// 根据待办事项id和用户id查询指定帖子是否存在,存在才删除
db.Where("id = ? and user_id = ?", id, c.GetInt("userID")).First(&todo)
if todo.ID == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"})
return
}
db.Delete(&todo)
c.JSON(http.StatusOK, gin.H{"message": "Todo deleted"})
}
六:路由注册
func main() {
db, err := initDB()
if err != nil {
panic(any("create table err"))
}
r := gin.Default()
r.POST("/signUp", func(c *gin.Context) {
signUp(c, db)
})
r.POST("/signIn", func(c *gin.Context) {
signIn(c, db)
})
authorized := r.Group("/")
// 作者操作待办事项前需要先鉴权,所以对该路由分组使用鉴权中间件
authorized.Use(authenticationMiddleware())
// 用大括号将该路由组下各路由包起来,方便管理(后期会介绍如果进行结构化管理)
{
authorized.POST("/todos", func(c *gin.Context) {
createTodo(c, db)
})
authorized.GET("/todos", func(c *gin.Context) {
getTodos(c, db)
})
authorized.PUT("/todos/:id", func(c *gin.Context) {
updateTodo(c, db)
})
authorized.DELETE("/todos/:id", func(c *gin.Context) {
deleteTodo(c, db)
})
// 退出登录,之所以写在该路由组下,是因为该路由组添加了鉴权中间件,退出登录前需要先鉴权是已经登录的状态
// 这里退出登录只是简单模拟,打印输出而已,实际退出登录,应该对token等进行销毁
authorized.POST("/signOut", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Logged out"})
})
}
// 启动服务
r.Run(":8080")
}
七:完整代码
package main
import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"net/http"
"strconv"
)
const secretKey = "abc123"
type User struct {
gorm.Model
Username string `json:"username" gorm:"unique"` // 唯一键
Password string `json:"password"`
}
// 待办事项结构体
type Todo struct {
gorm.Model
Title string `json:"title"`
Status string `json:"status"`
UserId int `json:"user_id"`
}
func initDB() (*gorm.DB, error) {
dsn := "root:root@(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=true&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(any("failed to connect to db"))
}
// 建表
err = db.AutoMigrate(&User{}, &Todo{})
if err != nil {
return nil, err
}
return db, nil
}
// 用户注册
func signUp(c *gin.Context, db *gorm.DB) {
var user User
if err := c.BindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 用户名为唯一键,不允许重复,所以根据用户名查询是否已经存在同名的用户
var existingUser User
err := db.Where("username = ?", user.Username).First(existingUser).Error
if err != nil && err != gorm.ErrRecordNotFound {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if existingUser.ID != 0 {
c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在!"})
return
}
// 用户密码加密入库
password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// hash加密后的密码赋值给用户,从而入库
user.Password = string(password)
err = db.Create(&user).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, user)
}
// 用户登录
func signIn(c *gin.Context, db *gorm.DB) {
var user User
if err := c.BindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 查询用户是否存在,存在则查出用户,然后对比加密的密码是否一致
var existingUser User
err := db.Where("username = ?", user.Username).First(&existingUser).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if existingUser.ID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误!"})
return
}
// 比对密码
err = bcrypt.CompareHashAndPassword([]byte(existingUser.Password), []byte(user.Password))
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误!"})
return
}
// 用户名和token都校验通过了,生成token返回
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": existingUser.ID,
"username": existingUser.Username,
})
// 对token进行加盐操作
signedToken, err := token.SignedString([]byte(secretKey))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 加盐后的token返回
c.JSON(http.StatusOK, gin.H{"token": signedToken})
}
// 用于操作待办事项前的鉴权中间件
func authenticationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("token")
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "权限校验未通过,请检查token是否传递"})
c.Abort()
return
}
// 解析token,拿到token中的用户名和id等信息
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil
})
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
c.Abort()
return
}
// token无效或者过期了
if !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
c.Abort()
return
}
claims := token.Claims.(jwt.MapClaims)
// 将用户id设置在context上下文中,断言为float64后转为int
c.Set("userID", int(claims["id"].(float64)))
c.Next() // 执行下一个handler
}
}
// 创建待办事项
func createTodo(c *gin.Context, db *gorm.DB) {
var todo Todo
if err := c.BindJSON(&todo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
todo.UserId = c.GetInt("userID")
db.Create(&todo)
c.JSON(http.StatusOK, todo)
}
// 查询待办事项
func getTodos(c *gin.Context, db *gorm.DB) {
var todos []Todo
db.Where("user_id = ?", c.GetInt("userID")).Find(&todos)
c.JSON(http.StatusOK, todos)
}
// 更新待办事项
func updateTodo(c *gin.Context, db *gorm.DB) {
id, err := strconv.Atoi(c.Param("id")) // 从路径参数中取出需要更新的待办事项id
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var updateTodo Todo
if err := c.BindJSON(&updateTodo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 更新操作需要指明model,从而知道是哪个表
db.Model(&Todo{}).Where("id = ? and user_id = ?", id, c.GetInt("userID")).Updates(updateTodo)
c.JSON(http.StatusOK, updateTodo)
}
// 删除待办事项
func deleteTodo(c *gin.Context, db *gorm.DB) {
id, err := strconv.Atoi(c.Param("id")) // 从路径参数中取出需要更新的待办事项id
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var todo Todo
// 根据待办事项id和用户id查询指定帖子是否存在,存在才删除
db.Where("id = ? and user_id = ?", id, c.GetInt("userID")).First(&todo)
if todo.ID == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"})
return
}
db.Delete(&todo)
c.JSON(http.StatusOK, gin.H{"message": "Todo deleted"})
}
func main() {
db, err := initDB()
if err != nil {
panic(any("create table err"))
}
r := gin.Default()
r.POST("/signUp", func(c *gin.Context) {
signUp(c, db)
})
r.POST("/signIn", func(c *gin.Context) {
signIn(c, db)
})
authorized := r.Group("/")
// 作者操作待办事项前需要先鉴权,所以对该路由分组使用鉴权中间件
authorized.Use(authenticationMiddleware())
// 用大括号将该路由组下各路由包起来,方便管理(后期会介绍如果进行结构化管理)
{
authorized.POST("/todos", func(c *gin.Context) {
createTodo(c, db)
})
authorized.GET("/todos", func(c *gin.Context) {
getTodos(c, db)
})
authorized.PUT("/todos/:id", func(c *gin.Context) {
updateTodo(c, db)
})
authorized.DELETE("/todos/:id", func(c *gin.Context) {
deleteTodo(c, db)
})
// 退出登录,之所以写在该路由组下,是因为该路由组添加了鉴权中间件,退出登录前需要先鉴权是已经登录的状态
// 这里退出登录只是简单模拟,打印输出而已,实际退出登录,应该对token等进行销毁
authorized.POST("/signOut", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Logged out"})
})
}
// 启动服务
r.Run(":8080")
}
八:测试
1、启动服务,查看注册的路由
2、注册
注册用户root和admin两个用户,可以看到注册成功,且密码进行了加密处理
查看DB
3、登录
登录成功,返回了token
,后续对待办事项的请求,需要在请求头中带上该token
,约定放到名为”token“
的请求头中
4、创建待办事项
未在请求头中携带token
时请求如下
携带token
请求头后请求
5、获取待办事项列表
记得在请求头中携带token
哦,获取前我创建了吃饭和睡觉两个待办事项
6、更新待办事项
更新吃饭的状态为已完成
7、删除待办事项
删除吃饭待办事项