「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」
前言
前面把后台功能完成了,今天准备把后端关于登录的接口写出来。
新建模型
需要做一个简单的后台用户表,做一个最简单的功能,用户名,密码,后台显示的头像。然后需要一个token字段,一般token存放在redis里面,为了简单易用,我准备直接放在后台用户表里面,然后设置一个过期时间。 在model.go里面加上ORM映射的结构体,在自动迁移的代码里加上这个结构体
type AdminUser struct {
ID uint `gorm:"primary_key"`
Name string
Avatar string
Password string
Token string
ExpirationAt time.Time `json:"created_at"`
}
复制代码
db.AutoMigrate(&UrlList{}, &UrlType{}, &AdminUser{}) //自动迁移
复制代码
启动项目自动会在数据库创建数据表,然后新建数据
新建中间件方法
在需要中间件验证的地方使用这个Token
中间件。每次经过中间件的时候查询admin_user
这张表是否存在这个token,且没有过期。然后把token携带的内容设置到上下文提供给某些接口使用。这里需要注意中间件的拦截需要使用c.about()
方法不能只用return。不然还是会继续走路由方法会有两个return。
import
...
"main/model"
...
func Token() gin.HandlerFunc {
return func(context *gin.Context) {
token := context.Request.Header.Get("X-Token")
token_exsits, err := model.TokenInfo(token)
if err != nil {
resospnseWithError(401, "非法请求", context)
return
}
fmt.Println(token_exsits)
if len(token_exsits) != 0 {
//先做时间判断
target_time, _ := time.ParseInLocation("2006-01-02 15:04:05", time.Time(token_exsits[0].ExpirationAt).Format("2006-01-02 15:04:05"), time.Local) //需要加上time.Local不然会自动加八小时
if target_time.Unix() <= time.Now().Unix() {
fmt.Println("过期报错")
//过期报错
resospnseWithError(401, "timeout", context)
return
}
//token没过期,更新到期时间
now := time.Unix(time.Now().Unix()+7200, 0).Format("2006-01-02 15:04:05")
err = model.UpdateTokenTime(token, now)
context.Set("name", token_exsits[0].Name)
context.Set("avatar", token_exsits[0].Avatar)
context.Set("token", token)
} else {
fmt.Println("没了")
resospnseWithError(401, "已退出", context)
return
}
context.Next()
}
}
type ResultCont struct {
Code int `json:"code"` //提示代码
Msg string `json:"msg"` //提示信息
Data interface{} `json:"data"` //数据
}
func resospnseWithError(code int, message string, c *gin.Context) {
var res ResultCont
res.Code = code
res.Msg = message
c.JSON(200, res) //前端返回也要返回200才能拦截
c.Abort()
}
复制代码
新建修改路由,使用中间件
主要有三个方法login
,info
,logout
三个方法都不需要走token中间件,info
和logout
和都是从get方法的param中获取token。login用于传递用户名密码就不需要中间件,涉及到的路由代码如下:
admin := router.Group("/admin")
admin.Use(middleware.Token())
{
// 路径映射
// api:=controller.NewDyController()
admin.GET("/getTypeList", controller.GetTypeList)
admin.POST("/DelType", controller.DelType)
admin.POST("/AddType", controller.AddType)
admin.POST("/EditType", controller.EditType)
admin.POST("/getUrlList", controller.GetUrlList)
admin.POST("/DelUrl", controller.DelUrl)
admin.POST("/AddUrl", controller.AddUrl)
admin.POST("/EditUrl", controller.EditUrl)
admin.POST("/user/logout", controller.Logout)
}
adminuser := router.Group("/admin/user")
{
adminuser.POST("/login", controller.Login)
adminuser.GET("/info", controller.Info)
}
复制代码
新建控制器
控制器获取服务方法的内容判断,还有获取中间件设置的上下文的内容交给服务方法,处理错误,规范返回
func Login(c *gin.Context) {
var json request.LoginRequest
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result := global.NewResult(c)
data, err := service.Login(json)
if err != nil {
result.Error(5201, err.Error(), "登录失败")
return
}
result.Success(data)
}
func Info(c *gin.Context) {
token := c.DefaultQuery("token", "")
result := global.NewResult(c)
data, err := service.Info(token)
if err != nil {
result.Error(5201, err.Error(), "登录失败")
return
}
result.Success(data)
}
func Logout(c *gin.Context) {
token := c.MustGet("token").(string)
result := global.NewResult(c)
err := service.Logout(token)
if err != nil {
result.Error(5201, err.Error(), "退出登录失败")
return
}
result.Success("退出登录成功")
}
复制代码
验证器
验证器的内容很少,主要是登录必须要字符串的用户名密码
type LoginRequest struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
复制代码
服务方法
文件位置是service/service.go
,今天逻辑最重要的地方,主要是登录,自己做了一些注释,判断有没有token,token过期时间,根据这些判断进行生成随机token或者直接返回操作,这里还引入了一些新的包,专门存放自己写的公共方法,util
包:
func Login(json request.LoginRequest) (data string, err error) {
//判断有没有这个用户密码
//没有报错,有了就判断token有没有,没有就创建返回,有就判断时间,时间没过期就返回,过期了就重新生成返回
var user []model.AdminUser
json.Password = util.FixMd5(json.Password + "boomxiakalaka")
user, err = model.Login(json.Username, json.Password)
if len(user) > 0 {
//成功
if user[0].Token == "" {
//token为空,创建更新返回
now := time.Unix(time.Now().Unix()+7200, 0).Format("2006-01-02 15:04:05")
token := util.GetRandomString(32)
err = model.LoginCreateToken(json.Username, json.Password, token, now)
return token, err
} else {
//token不为空 ,判断时间,时间不过期直接返回
target_time, _ := time.ParseInLocation("2006-01-02 15:04:05", time.Time(user[0].ExpirationAt).Format("2006-01-02 15:04:05"), time.Local) //需要加上time.Local不然会自动加八小时
if target_time.Unix() >= time.Now().Unix() {
return user[0].Token, nil
} else {
//时间过期
token := util.GetRandomString(32)
now := time.Unix(time.Now().Unix()+7200, 0).Format("2006-01-02 15:04:05")
err = model.LoginCreateToken(json.Username, json.Password, token, now)
return token, err
}
}
}
return "登陆失败", errors.New("登陆失败")
}
func Info(token string) (data interface{}, err error) {
list, err := model.Info(token)
return list, err
}
func Logout(token string) (err error) {
err = model.Logout(token)
return err
}
复制代码
公共方法
主要是生成随机的32未字符串,MD5混淆,在根目录下新建util/util.go文件,引入了两个内置包,一个是用于随机的包,和时间处理的包,这里就可以看到goalng的简陋了,随机竟然还需要获取时间来做一个随机种子来做成真正的随机;MD5混淆主要是用于密码保存,总所周知密码原文直接存放在数据库是非常不安全的,以下是整个文件内容:
package util
import (
"math/rand"
"time"
)
// 随机生成指定位数的大写字母和数字的组合
func GetRandomString(l int) string {
str := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
bytes := []byte(str)
result := []byte{}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < l; i++ {
result = append(result, bytes[r.Intn(len(bytes))])
}
return string(result)
}
//MD5混淆
func FixMd5(str string) string {
data := []byte(str)
has := md5.Sum(data)
md5str := fmt.Sprintf("%x", has)
return md5str
}
复制代码
模型方法
对于GORM操作不是很熟悉,用了很多方法实现了各种需求,获取用户信息,创建token,删除token。
func Login(name string, password string) (list []AdminUser, err error) {
var user []AdminUser
db.Debug().Where("name = ? and password = ?", name, password).First(&user)
return user, nil
}
func LoginCreateToken(name string, password string, token string, expiration_at string) (err error) {
return db.Debug().Table("admin_user").Where("name = ? and password = ?", name, password).Updates(map[string]interface{}{"token": token, "expiration_at": expiration_at}).Error
}
func TokenInfo(token string) (list []AdminUser, err error) {
var user []AdminUser
db.Debug().Where("token = ? ", token).First(&user)
return user, nil
}
func Info(token string) (data interface{}, err error) {
type Result struct {
Name string
Avatar string
}
var result Result
db.Debug().Table("admin_user").Select("name, avatar").Where("token = ? ", token).Scan(&result)
return result, nil
}
func Logout(token string) (err error) {
return db.Debug().Table("admin_user").Where("token = ? ", token).Updates(map[string]interface{}{"token": ""}).Error
}
func UpdateTokenTime(token string, expiration_at string) (err error) {
return db.Debug().Table("admin_user").Where("token = ? ", token).Updates(map[string]interface{}{"expiration_at": expiration_at}).Error
}
func TokenFind(token string) (status bool) {
var user []AdminUser
rowsAffects := db.Debug().Where("token = ? ", token).First(&user).RowsAffected
if rowsAffects == 0 {
return false
} else {
return true
}
}
复制代码
总结
主要登录逻辑最为复杂,对于简单的内容系统,一般是直接在数据库添加数据,不做账户注册,我们公司后台就是。先需要把密码混淆后对数据库对比,有了就登录成功了,成功之前还需要更新token给前端使用,还需要动态更新设置token的到期时间。代码比较多,下篇更新前端关于的设置。