2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 13:47:15 +08:00

feat: captcha and login

This commit is contained in:
耗子
2023-06-22 00:22:50 +08:00
parent c14f4de880
commit 6481c2e65e
14 changed files with 373 additions and 3 deletions

View File

@@ -9,6 +9,7 @@ import (
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"panel/app/models"
"panel/packages/helpers"
)

View File

@@ -3,6 +3,7 @@ package console
import (
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/schedule"
"panel/app/console/commands"
)

View File

@@ -3,8 +3,9 @@ package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
models "panel/app/Models"
"panel/app/http/requests"
"panel/app/models"
)
type UserController struct {
@@ -88,7 +89,21 @@ func (r *UserController) Login(ctx http.Context) {
}
func (r *UserController) Info(ctx http.Context) {
user, ok := ctx.Value("user").(models.User)
if !ok {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"code": 401,
"message": "登录已过期",
})
return
}
ctx.Response().Success().Json(http.Json{
"Hello": "Goravel",
"code": 0,
"message": "获取用户信息成功",
"data": http.Json{
"username": user.Username,
"email": user.Email,
},
})
}

View File

@@ -8,6 +8,9 @@ import (
type LoginRequest struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
CaptchaID string `json:"captcha_id" form:"captcha_id"`
Captcha string `json:"captcha" form:"captcha"`
}
func (r *LoginRequest) Authorize(ctx http.Context) error {
@@ -18,6 +21,7 @@ func (r *LoginRequest) Rules(ctx http.Context) map[string]string {
return map[string]string{
"login": "required",
"password": "required|min_len:8",
"captcha": "captcha:true",
}
}
@@ -26,6 +30,7 @@ func (r *LoginRequest) Messages(ctx http.Context) map[string]string {
"login.required": "登录名不能为空",
"password.required": "密码不能为空",
"password.min_len": "密码长度不能小于 8 位",
"captcha.captcha": "验证码错误",
}
}

View File

@@ -4,6 +4,8 @@ import (
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/contracts/validation"
"github.com/goravel/framework/facades"
"panel/app/rules"
)
type ValidationServiceProvider struct {
@@ -20,5 +22,9 @@ func (receiver *ValidationServiceProvider) Boot(app foundation.Application) {
}
func (receiver *ValidationServiceProvider) rules() []validation.Rule {
return []validation.Rule{}
return []validation.Rule{
&rules.Exists{},
&rules.NotExists{},
&rules.Captcha{},
}
}

46
app/rules/captcha.go Normal file
View File

@@ -0,0 +1,46 @@
package rules
import (
"strconv"
"github.com/goravel/framework/contracts/validation"
"panel/packages/captcha"
)
type Captcha struct {
}
// Signature The name of the rule.
func (receiver *Captcha) Signature() string {
return "captcha"
}
// Passes Determine if the validation rule passes.
func (receiver *Captcha) Passes(data validation.Data, val any, options ...any) bool {
captchaID, exist := data.Get("captcha_id")
if !exist {
return false
}
// 第一个参数(如果有),是否清除验证码,如 false
clear := true
var err error
if len(options) > 0 {
clear, err = strconv.ParseBool(options[0].(string))
if err != nil {
clear = true
}
}
if !captcha.NewCaptcha().VerifyCaptcha(captchaID.(string), val.(string), clear) {
return false
}
return true
}
// Message Get the validation error message.
func (receiver *Captcha) Message() string {
return ""
}

54
app/rules/exists.go Normal file
View File

@@ -0,0 +1,54 @@
package rules
import (
"github.com/goravel/framework/contracts/validation"
"github.com/goravel/framework/facades"
)
type Exists struct {
}
// Signature The name of the rule.
func (receiver *Exists) Signature() string {
return "exists"
}
// Passes Determine if the validation rule passes.
func (receiver *Exists) Passes(_ validation.Data, val any, options ...any) bool {
// 第一个参数,表名称,如 categories
tableName := options[0].(string)
// 第二个参数,字段名称,如 id
fieldName := options[1].(string)
// 用户请求过来的数据
requestValue, ok := val.(string)
if !ok {
return false
}
// 判断是否为空
if len(requestValue) == 0 {
return false
}
// 判断是否存在
var count int64
query := facades.Orm().Query().Table(tableName).Where(fieldName, requestValue)
// 判断第三个参数及之后的参数是否存在
if len(options) > 2 {
for i := 2; i < len(options); i++ {
query = query.OrWhere(options[i].(string), requestValue)
}
}
err := query.Count(&count)
if err != nil {
return false
}
return count != 0
}
// Message Get the validation error message.
func (receiver *Exists) Message() string {
return "记录不存在"
}

54
app/rules/not_exists.go Normal file
View File

@@ -0,0 +1,54 @@
package rules
import (
"github.com/goravel/framework/contracts/validation"
"github.com/goravel/framework/facades"
)
type NotExists struct {
}
// Signature The name of the rule.
func (receiver *NotExists) Signature() string {
return "not_exists"
}
// Passes Determine if the validation rule passes.
func (receiver *NotExists) Passes(_ validation.Data, val any, options ...any) bool {
// 第一个参数,表名称,如 categories
tableName := options[0].(string)
// 第二个参数,字段名称,如 id
fieldName := options[1].(string)
// 用户请求过来的数据
requestValue, ok := val.(string)
if !ok {
return false
}
// 判断是否为空
if len(requestValue) == 0 {
return false
}
// 判断是否存在
var count int64
query := facades.Orm().Query().Table(tableName).Where(fieldName, requestValue)
// 判断第三个参数及之后的参数是否存在
if len(options) > 2 {
for i := 2; i < len(options); i++ {
query = query.OrWhere(options[i].(string), requestValue)
}
}
err := query.Count(&count)
if err != nil {
return false
}
return count == 0
}
// Message Get the validation error message.
func (receiver *NotExists) Message() string {
return "记录已存在"
}

34
config/captcha.go Normal file
View File

@@ -0,0 +1,34 @@
package config
import (
"github.com/goravel/framework/facades"
)
func init() {
config := facades.Config()
config.Add("captcha", map[string]any{
// 验证码图片高度
"height": 60,
// 验证码图片宽度
"width": 120,
// 验证码的长度
"length": 6,
// 数字的最大倾斜角度
"maxskew": 0.6,
// 图片背景里的混淆点数量
"dotcount": 40,
// 过期时间,单位是分钟
"expire_time": 5,
// debug 模式下的过期时间,方便本地开发调试
"debug_expire_time": 3600,
// 非 production 环境,使用此 key 可跳过验证,方便测试
"testing_key": "captcha_skip_test",
})
}

5
go.mod
View File

@@ -39,6 +39,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gertd/go-pluralize v0.2.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-contrib/static v0.0.1 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
@@ -58,6 +59,7 @@ require (
github.com/golang-module/carbon/v2 v2.2.3 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
@@ -79,6 +81,7 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/iancoleman/strcase v0.2.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
@@ -101,6 +104,7 @@ require (
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mojocn/base64Captcha v1.3.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
@@ -143,6 +147,7 @@ require (
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect

10
go.sum
View File

@@ -153,6 +153,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
@@ -242,6 +244,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -362,6 +366,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
@@ -454,6 +460,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0=
github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@@ -665,6 +673,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=

View File

@@ -0,0 +1,62 @@
// Package captcha 处理图片验证码逻辑
package captcha
import (
"sync"
"github.com/goravel/framework/facades"
"github.com/mojocn/base64Captcha"
)
type Captcha struct {
Base64Captcha *base64Captcha.Captcha
}
// once 确保 internalCaptcha 对象只初始化一次
var once sync.Once
// internalCaptcha 内部使用的 Captcha 对象
var internalCaptcha *Captcha
// NewCaptcha 单例模式获取
func NewCaptcha() *Captcha {
once.Do(func() {
// 初始化 Captcha 对象
internalCaptcha = &Captcha{}
// 使用 Cache facade 进行存储,并配置存储 Key 的前缀
store := CacheStore{
KeyPrefix: facades.Config().GetString("app.name") + ":captcha:",
}
// 配置 base64Captcha 驱动信息
driver := base64Captcha.NewDriverDigit(
facades.Config().GetInt("captcha.height"), // 宽
facades.Config().GetInt("captcha.width"), // 高
facades.Config().GetInt("captcha.length"), // 长度
facades.Config().Get("captcha.maxskew").(float64), // 数字的最大倾斜角度
facades.Config().GetInt("captcha.dotcount"), // 图片背景里的混淆点数量
)
// 实例化 base64Captcha 并赋值给内部使用的 internalCaptcha 对象
internalCaptcha.Base64Captcha = base64Captcha.NewCaptcha(driver, &store)
})
return internalCaptcha
}
// GenerateCaptcha 生成图片验证码
func (c *Captcha) GenerateCaptcha() (id string, b64s string, err error) {
return c.Base64Captcha.Generate()
}
// VerifyCaptcha 验证验证码是否正确
func (c *Captcha) VerifyCaptcha(id string, answer string, clear bool) (match bool) {
// 方便本地和 API 自动测试
if facades.Config().GetBool("app.debug") && id == facades.Config().GetString("captcha.testing_key") {
return true
}
// 这样方便用户多次提交,防止表单提交错误需要多次输入图片验证码
return c.Base64Captcha.Verify(id, answer, clear)
}

View File

@@ -0,0 +1,45 @@
package captcha
import (
"time"
"github.com/goravel/framework/facades"
)
// CacheStore 实现 base64Captcha.Store interface
type CacheStore struct {
KeyPrefix string
}
// Set 实现 base64Captcha.Store interface 的 Set 方法
func (s *CacheStore) Set(key string, value string) error {
ExpireTime := time.Minute * time.Duration(facades.Config().GetInt("captcha.expire_time"))
// 方便本地开发调试
if facades.Config().GetBool("app.debug") {
ExpireTime = time.Minute * time.Duration(facades.Config().GetInt("captcha.debug_expire_time"))
}
err := facades.Cache().Put(s.KeyPrefix+key, value, ExpireTime)
if err != nil {
return err
}
return nil
}
// Get 实现 base64Captcha.Store interface 的 Get 方法
func (s *CacheStore) Get(key string, clear bool) string {
key = s.KeyPrefix + key
val := facades.Cache().Get(key, "").(string)
if clear {
facades.Cache().Forget(key)
}
return val
}
// Verify 实现 base64Captcha.Store interface 的 Verify 方法
func (s *CacheStore) Verify(key, answer string, clear bool) bool {
v := s.Get(key, clear)
return v == answer
}

32
packages/str/str.go Normal file
View File

@@ -0,0 +1,32 @@
// Package str 字符串辅助方法
package str
import (
"github.com/gertd/go-pluralize"
"github.com/iancoleman/strcase"
)
// Plural 转为复数 user -> users
func Plural(word string) string {
return pluralize.NewClient().Plural(word)
}
// Singular 转为单数 users -> user
func Singular(word string) string {
return pluralize.NewClient().Singular(word)
}
// Snake 转为 snake_case如 TopicComment -> topic_comment
func Snake(s string) string {
return strcase.ToSnake(s)
}
// Camel 转为 CamelCase如 topic_comment -> TopicComment
func Camel(s string) string {
return strcase.ToCamel(s)
}
// LowerCamel 转为 lowerCamelCase如 TopicComment -> topicComment
func LowerCamel(s string) string {
return strcase.ToLowerCamel(s)
}