Gin实现论坛
一、用户建表
1、Model层创建User模型
type User struct{
gorm.Model
UserID string `gorm:"not null"`
UserName string `gorm:"not null"`
PassWord string `gorm:"not null"`
Name string `gorm:"not null"`
Email string `gorm:"not null"`
Gender int `gorm:"not null;default:1"` //gender=0 is admin
}
2、Dao层创建表,可进行业务处理
//Create Init Tables eg:User,Create User Can Add use ',' Split
if err:=db.AutoMigrate(&Model.User{});err!=nil{
zap.L().Error("AutoMigrate Err",zap.Error(err))
}
//If Not Have User Will Add Admin
//if res:=db.Find(&Model.User{});res.RowsAffected==0&&res.Error==nil{
// db.Create(&Model.User{
// UserName: "root",
// PassWord: "root",
// Name:"admin",
// Email:"1102394156@qq.com",
// })
//}
二、雪花算法go的实现
package main
import (
"fmt"
"github.com/bwmarrin/snowflake"
"log"
"time"
)
//轻量级的雪花算法实现
var node *snowflake.Node
func Init(startTime string, machineID int64) (err error) {
var st time.Time
st, err = time.Parse("2006-01-02", startTime)
if err != nil {
log.Fatal(err)
return
}
snowflake.Epoch = st.UnixNano() / 1000000
node, err = snowflake.NewNode(machineID)
return
}
func GetId() int64 {
return node.Generate().Int64()
}
func main() {
if err := Init("2021-07-13", 1); err != nil {
fmt.Println("ERR Init")
return
}
for i := 0; i < 10; i++ {
id := GetId()
fmt.Println(id)
}
}
三、CLD分层逻辑
1、首先是router包下的函数定义路由(Login)到controller中
r.POST("/Login",Controller.LoginHandler)
2、然后Controller进行参数获取,参数验证,业务处理,返回值
func LoginHandler(c *gin.Context){
//Get Params And Check
//c.GetString()...GetParams
//Make Work
Logic.Login()
//Return Response
c.JSON(http.StatusOK,gin.H{
"msg":"ok",
})
}
3、在处理业务逻辑Login的时候转换到logic层处理
package Logic
func Login(){
//Juge User is have (Use Dao)
//Snowflake Uuid
//Save DB(Use Dao)
}
4、Logic层将会对数据库增删查改,所以需要操作Dao层
package mysql
func QueryUserByID(){
//select * from......
}
四、validator库参数校验
1、直接在结构体中使用bind参数校验参数,使用shouldbind的时候自动校验
RePassWord string `json:"re_password" binding:"required,eqfield=PassWord"`
2、将错误翻译为中文
(1)设置翻译器Translator
package Controller
import (
"fmt"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTranslations "github.com/go-playground/validator/v10/translations/en"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
)
// 定义一个全局翻译器T
var trans ut.Translator
// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性,实现自定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
zhT := zh.New() // 中文翻译器
enT := en.New() // 英文翻译器
// 第一个参数是备用(fallback)的语言环境
// 后面的参数是应该支持的语言环境(支持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
// locale 通常取决于 http 请求头的 'Accept-Language'
var ok bool
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
}
// 注册翻译器
switch locale {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, trans)
case "zh":
err = zhTranslations.RegisterDefaultTranslations(v, trans)
default:
err = enTranslations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}
(2)主函数main初始化翻译器
//Init Translate
if err := Controller.InitTrans("zh"); err != nil {
zap.L().Error("init trans failed",zap.Error(err))
return
}
(3)在bind的err中进行类型断言判断,如果是bind错误就输出翻译后的信息
if err:=c.ShouldBindJSON(&newUser);err!=nil{
errs,ok:=err.(validator.ValidationErrors)
if !ok{
zap.L().Error("ShouldBind Err",zap.Error(err))
c.JSON(http.StatusOK,gin.H{
"msg":err.Error(),
})
}
c.JSON(http.StatusOK, gin.H{
"msg":errs.Translate(trans),
})
return
}
3、由于接受返回的json是前端,所以需要和前端的json标签相同,但是翻译器默认是与后端结构体的名字相同,所以在初始化InitTrans的时候注册jsontag方法
// 注册一个获取json tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
4、翻译器的获取的key值的结构是:结构体.字段(后端),但是前端接受数据是不需要后端的,所以需要进行切割
func removeTopStruct(fields map[string]string) map[string]string {
res := map[string]string{}
for field, err := range fields {
res[field[strings.Index(field, ".")+1:]] = err
}
return res
}
调用方法:
c.JSON(http.StatusOK, gin.H{
"msg":removeTopStruct(errs.Translate(trans)),
})
五、定义错误码与响应封装
1、定义错误码,制定错误码返回错误信息方法
package Model
type RespCode int64
const (
CodeSuccess RespCode=1000+iota
CodeInvalidParam
CodeUserExitst
CodeUserNotExitst
CodeInvalidPassword
CodeServerBusy
)
var CodeMsgMap =map[RespCode]string{
CodeSuccess: "Success",
CodeInvalidParam: "Param Err",
CodeUserExitst: "User Exitst",
CodeUserNotExitst: "Not have User",
CodeInvalidPassword: "PassWord Err",
CodeServerBusy: "Busy 404 Server",
}
func (r RespCode)Msg()string{
msg,ok:=CodeMsgMap[r];
if !ok{
msg=CodeMsgMap[CodeServerBusy]
}
return msg
}
2、定义响应结构并且绑定方法
package Controller
import (
"GinProject/Model"
"github.com/gin-gonic/gin"
"net/http"
)
type Response struct {
Code Model.RespCode `json:"code"`
Msg interface{} `json:"msg"`
Data interface{} `json:"data"`
}
func ResponseErr(c *gin.Context,code Model.RespCode){
c.JSON(http.StatusOK,&Response{
Code:code,
Msg: code.Msg(),
Data: nil,
})
}
func ResponseSuccess(c *gin.Context,data interface{}){
c.JSON(http.StatusOK,&Response{
Code:Model.CodeSuccess,
Msg: Model.CodeSuccess.Msg(),
Data: data,
})
}
func ResponseErrWithMsg(c *gin.Context,code Model.RespCode,msg interface{}){
c.JSON(http.StatusOK,&Response{
Code:code,
Msg: msg,
Data: nil,
})
}
六、用户注册与登录
1、注册
(1)使用bcrypt加密密码
//Get ErrHash
func HashAdd(passWord string)string{
hash,err:=bcrypt.GenerateFromPassword([]byte(passWord),0)
if err!=nil{
zap.L().Error("bcrypt Err", zap.Error(err))
}
return string(hash)
}
(2)注册无非与数据库交互,自己梳理逻辑
2、登录
(1)加密密码比较方法
//HashCompare
func HashCompare(hash ,passWord string) bool{
return bcrypt.CompareHashAndPassword([]byte(hash),[]byte(passWord))==nil
}
(2)登录逻辑自己梳理
七、用户认证JWT
1、session和cookie的问题
(1)储存的session资源大;
(2)创建session服务器和验证session的服务器可能不是同一台;
(3)跨域需要兼容性处理,难以防范CSRF攻击;
2、Token认证模式
(1)无状态,不需要在服务器储存Session,只需要解析客户端发来的Token;
(2)不需要Cookie,解决了CSRF问题;
(3)使用CORS解决跨域问题;
3、JWT
定义:Json Web Token 是基于Token的会话管理规则;
结构:分成三部分,头部,负载,签名;
头部:定义加密算法和类型,是一个JSON对象;
负载:规定了官方字段,比如签发人,过期时间等;(也可以自己增加字段);
签名:防止数据被篡改;
缺点:Token被盗的话,在有效期内能一直访问;
(1)使用jwt-go库获得设置Token
package Jwt
import (
"errors"
"github.com/dgrijalva/jwt-go"
"time"
)
//定义JWT的过期时间2小时
const TokenExpireDuration = time.Hour * 2
// Add Nacl
var MySecret = []byte("Cas")
// MyClaims 自定义声明结构体并内嵌jwt.StandardClaims
// jwt包自带的jwt.StandardClaims只包含了官方字段
// 我们这里需要额外记录一个username字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type MyClaims struct {
UserId int64 `json:"userid"`
Username string `json:"username"`
jwt.StandardClaims
}
// GenToken 生成JWT
func GenToken(userid int64,username string) (string, error) {
// 创建一个我们自己的声明
c := MyClaims{
userid ,
username , // 自定义字段
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
Issuer: "Cas", // 签发人
},
}
// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 使用指定的secret签名并获得完整的编码后的字符串token
return token.SignedString(MySecret)
}
// ParseToken 解析JWT
func ParseToken(tokenString string) (*MyClaims, error) {
// 解析token
var mc =new(MyClaims)
token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (i interface{}, err error) {
return MySecret, nil
})
if err != nil {
return nil, err
}
if token.Valid { // 校验token
return mc, nil
}
return nil, errors.New("invalid token")
}
(2)在路由中增加中间件判断是否有合法的Token
r.GET("/ping", middlewares.JWTAuthMiddleware(),func(c *gin.Context) {
c.JSON(http.StatusOK,gin.H{
"msg":"pong",
})
})
(3)通过Token获得userid
func GetCurrentUser(c *gin.Context)(userid int64,err error){
uid,ok:=c.Get("userid")
if !ok{
err=errors.New("User Not Login")
return
}
userid,ok=uid.(int64)
if !ok{
err=errors.New("User Not Login")
return
}
return userid,nil
}
4、Refresh Token
大致流程:首先获得AccessToken和RefreshToken,然后判断AccessToken解析是否有值,如果没有值就使用RefreshToken来获取新的AccessToken
思考:同一账号只能登录一台设备;
方案:通过时间维度的不同,使用redis储存用户ID和Token的对应关系
如果看完对自己有所帮助,请点赞支持,谢谢大家