上一步中,生成图片验证码接口已经好了。现在完善登录和注册的业务逻辑。这里准备使用 token 的方式来维护用户的状态,选用 jwt 。所以登录或注册成功后,只需返回用户一个 token 就可以了。过期时间暂时先不要。所以登录和注册的返回需要修改一番。
登录注册输出重新定义一下
internal/model/admin/user.go 中定义输入和输出的结构。
type SignInRes struct {
Token string `json:"token"`
}
type SignUpRes struct {
Token string `json:"token"`
}
api/user/admin/user.go 中组合输入输出
package admin
import (
model "goSimpleAdmin/internal/model/admin"
"github.com/gogf/gf/v2/frame/g"
)
type SignInRes struct {
model.SignInRes
}
type SignUpRes struct {
model.SignUpRes
}
因为 api 定义的时候已经定义好了输出。所以 ctrl 和 service 这些不需要再重新生一次了。
但是在 logic 和 service 中没有定义 res。所以需要补上,再重新生一次。 service 就是接口的定义,实现还是在 logic 中。
internal/logic/user/user.go 修改
func (s *sUser) SignIn(ctx context.Context, in model.SignInReq) (res model.SignInRes, err error) {
err = gerror.New("我的天呀")
return res, err
}
func (s *sUser) SignUp(ctx context.Context, in model.SignUpReq) (res model.SignUpRes, err error) {
err = gerror.New("我的地呀")
return res, err
}
然后再重新生成 service。
gf gen service
这样在 ctrl 中就可以接到 res 或 err 了。就能给到输出了。
登录控制器
先处理 controller,因为 controller 几乎也是死的,灵活的都留给 logic 了。
编辑 internal/controller/user/user_admin_sign_in.go
package user
import (
"context"
"goSimpleAdmin/api/user/admin"
model "goSimpleAdmin/internal/model/admin"
"goSimpleAdmin/internal/service"
)
func (c *ControllerAdmin) SignIn(ctx context.Context, req *admin.SignInReq) (res *admin.SignInRes, err error) {
data, err := service.User().SignIn(ctx, model.SignInReq{
Passport: req.Passport,
Password: req.Passport,
})
if err != nil {
return
}
res = new(admin.SignInRes)
res.SignInRes = data
return
}
这里有点别扭的地方就是每次都要对 logic 的 res 要转换一下。还有就是返回给请求端的总是会包裹一层。包裹一层这个问题啰嗦了好久,后边肯定要搞定的。
剩下的逻辑全在 logic 这里了。修改完善 internal/logic/user/user.go 中的 SignIn 方法。
登录业务逻辑
当前登录是账号密码的登录方式。需要处理三个事情。
- 查找用户
- 比较用户的密码
- 生成 token
查找用户通过 orm 就可以。
比较用户的密码就得看密码的来由。通常密码都会是加密的。这里想用 Bcrypt 作为驱动来加密,不用官方封装那几种加密方式。这个加密和判断密码是否正确都有自己的函数,也就是比较密码是否正确不是通过比较两个密码的字符串是否一样。并且可以设置加密的加密系数。
好开干。
# 下载
go get golang.org/x/crypto/bcrypt
# 封装 新建 internal/pkg/hash/hash.go
package hash
import (
"golang.org/x/crypto/bcrypt"
)
// BcryptHash 使用 bcrypt 对密码进行加密
func BcryptHash(password string) (value string, err error) {
// GenerateFromPassword 的第二个参数是 cost 值。建议大于 12,数值越大耗费时间越长
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
return string(bytes), err
}
// BcryptCheck 对比明文密码和数据库的哈希值
func BcryptCheck(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// BcryptIsHashed 判断字符串是否是哈希过的数据
func BcryptIsHashed(str string) bool {
// bcrypt 加密后的长度等于 60
return len(str) == 60
}
然后,在 logic 中使用上。(先获取用户,如果用户存在,再比较密码)
var user *entity.User
err = dao.User.Ctx(ctx).Where(do.User{
Passport: in.Passport,
}).WhereOr(do.User{
Email: in.Passport,
}).WhereOr(do.User{
Phone: in.Passport,
}).Scan(&user)
if err != nil {
return res, err
}
if user == nil {
return res, gerror.New("账号不存在")
}
check := hash.BcryptCheck(in.Password, user.Password)
if !check {
return res, gerror.New("账号或密码错误")
}
第一个提示对于坏人就有点友好。所以需要在中间件中对请求频率进行控制。这个后边再说。
生成 token,还是使用 jwt 方案。go 的 jwt 封装比较多。这里使用 goframe 官方自己封装的 jwt。
https://goframe.org/display/jwt/gf-jwt
# 先下载
go get github.com/gogf/gf-jwt/v2
再次封装。
package jwt
import (
"context"
"sync"
"time"
jwtv2 "github.com/gogf/gf-jwt/v2"
"github.com/gogf/gf/v2/frame/g"
)
type JwtConfig struct {
Realm string
Key string
Timeout int
MaxRefresh int
}
var authService *jwtv2.GfJWTMiddleware
var once sync.Once
func NewJwt() *jwtv2.GfJWTMiddleware {
once.Do(func() {
config := JwtConfig{
Realm: "go simple admin",
Key: "this is key",
Timeout: 60 * 24 * 7,
MaxRefresh: 60 * 24 * 10,
}
auth := jwtv2.New(&jwtv2.GfJWTMiddleware{
Realm: config.Realm,
Key: []byte(config.Key),
Timeout: time.Minute * time.Duration(config.Timeout),
MaxRefresh: time.Minute * time.Duration(config.MaxRefresh),
IdentityKey: "id",
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
Authenticator: Authenticator,
Unauthorized: Unauthorized,
PayloadFunc: PayloadFunc,
IdentityHandler: IdentityHandler,
})
authService = auth
})
return authService
}
func PayloadFunc(data interface{}) jwtv2.MapClaims {
claims := jwtv2.MapClaims{}
params := data.(map[string]interface{})
if len(params) > 0 {
for k, v := range params {
claims[k] = v
}
}
return claims
}
func IdentityHandler(ctx context.Context) interface{} {
claims := jwtv2.ExtractClaims(ctx)
return claims[authService.IdentityKey]
}
func Unauthorized(ctx context.Context, code int, message string) {
r := g.RequestFromCtx(ctx)
r.Response.WriteJson(g.Map{
"code": code,
"message": message,
})
r.ExitAll()
}
// Authenticator is used to validate login parameters.
// It must return user data as user identifier, it will be stored in Claim Array.
// if your identityKey is 'id', your user data must have 'id'
// Check error (e) to determine the appropriate error message.
func Authenticator(ctx context.Context) (user interface{}, err error) {
return user, nil
}
本来,是想弄个配置文件的。可是 g.Cfg 获取配置信息需要 context。某些时候,不方便传递 context。暂时就这样,先实现功能。还有这里不需要 Authenticator 。觉得 jwt 就单纯做 jwt 的事情。发放 token 和验证 token。在 jwt 里边搞请求用户数据就有点不太好。没事,不用它,用 TokenGenerator 方法生成 token 就好。
既然这里封装好了 jwt。那么就接着上边的用起来。不是验证用户密码通过后,就可以给用户颁发 token了。
func (s *sUser) SignIn(ctx context.Context, in model.SignInReq) (res model.SignInRes, err error) {
var user *entity.User
err = dao.User.Ctx(ctx).Where(do.User{
Passport: in.Passport,
}).WhereOr(do.User{
Email: in.Passport,
}).WhereOr(do.User{
Phone: in.Passport,
}).Scan(&user)
if err != nil {
return res, err
}
if user == nil {
return res, gerror.New("账号不存在")
}
check := hash.BcryptCheck(in.Password, user.Password)
if !check {
return res, gerror.New("账号或密码错误")
}
data := g.Map{
"id": user.Id,
"nickname": user.Nickname,
}
res.Token, res.Expire, err = jwt.NewJwt().TokenGenerator(data)
return res, err
}
TokenGenerator 方法中的 data,必须至少有一个 id。这个是用户的唯一识别。和 jwt 配置项里的 IdentityKey 对应。一般,都不会去更改。
好运行起来,用 postman 请求看看。返回了以下数据,看起来,是 ok 的。
{
"code": 0,
"message": "",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ3ODQwNTE4OTgsImlkIjo4LCJuaWNrbmFtZSI6IuWGgOWzsCIsIm9yaWdfaWF0IjoxNjk0MTc5MjUxODk4fQ.UJMIiJhBGLo2cuVQ5ibXgTDSN2upxdh7D0ahgdshGZg",
"expire": "2023-09-15T13:20:51.898854Z"
}
}
将 token 复制到 https://jwt.io 中解析看看。可以看到 paylod 的数据是。
{
"exp": 1694784051898,
"id": 8,
"nickname": "冀峰",
"orig_iat": 1694179251898
}
看来是真有用了。
再来搞个 auth 中间件
internal/logic/middleware/middleware.go 自定义中间件放这里。
package middleware
import (
"github.com/gogf/gf/v2/net/ghttp"
"goSimpleAdmin/internal/pkg/jwt"
"goSimpleAdmin/internal/service"
)
type (
sMiddleware struct{}
)
func init() {
service.RegisterMiddleware(New())
}
func New() service.IMiddleware {
return &sMiddleware{}
}
func (s *sMiddleware) Auth(r *ghttp.Request) {
jwt.NewJwt().MiddlewareFunc()(r)
r.Middleware.Next()
}
func (s *sMiddleware) CORS(r *ghttp.Request) {
r.Response.CORSDefault()
r.Middleware.Next()
}
然后使用 gf gen service
生成对应的 service 文件,就是接口文件。并在 logic.go 中自动注册绑定。
最后,在 cmd 中使用中间件。可以看到所有中间件都共同实现了相同的接口的。
s.Group("/api/admin", func(group *ghttp.RouterGroup) {
group.Middleware(
ghttp.MiddlewareCORS,
)
group.ALLMap(g.Map{
"/captcha": user.NewAdmin().Captcha,
"/user/sign-in": user.NewAdmin().SignIn,
"/user/sign-up": user.NewAdmin().SignUp,
})
group.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(service.Middleware().Auth)
group.ALLMap(g.Map{
"/user/info": user.NewAdmin().UserInfo,
})
})
})
这里把路由绑定稍微改了下,不再让自动绑定,改为具体绑定了。中间件也加上了。现在可以试试刚才生成的 token 是否有用。
在 postman 中加入 user/info 的请求。在请求头中增加 Authorization 的配置。类别选择 Bearea Token,并把上边的 token 值复制进去。然后请求看看结果。
主要看图片上 1,2,3,4 这四个地方的设置。{{}} 是通过变量实现的。
获取用户信息的实现之前没加,现在加进去。其实,是已经加了,要不也请求不出数据来。
internal/logic/user/user.go 中
func (s *sUser) UserInfo(ctx context.Context, in model.UserInfoReq) (res model.UserInfoRes, err error) {
var user *entity.User
id := jwt.NewJwt().GetIdentity(ctx)
err = dao.User.Ctx(ctx).Where(do.User{
Id: id,
}).Scan(&user)
if err != nil {
return res, err
}
res.User = *user
return res, err
}
看这个逻辑,可以看到通过 jwt 获取到 id,再通过 id 获取用户信息的。其实再返回给用户的用户信息时,password 这些信息是不应该有的。后边再处理。
通过 https://goframe.org/pages/viewpage.action?pageId=1114226 这个应该可以。还是没 laravel 好用。