From 5e0117e7c6d74d8b85a5d8503383b9aa7fc6a0ff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=A5=87=E6=B7=BC=EF=BC=88piexlmax?= <303176530@qq.com>
Date: Sun, 1 Jan 2023 01:26:57 +0800
Subject: [PATCH] Captcha (#1329)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: #807 自定义验证码开关,可以自行配置
* 添加数据库列表支持:SQL SERVER
* [user.vue]: 新增邮箱手机合法性校验,邮箱手机非必填
* [menu.js]: 修复错误的API注释
* 调整配置文件 是其可以支持oracle和mssql
Co-authored-by: Yexk
Co-authored-by: 逆光飞翔 <191180776@qq.com>
Co-authored-by: chenteng
---
server/api/v1/system/sys_captcha.go | 29 ++++++-
server/api/v1/system/sys_user.go | 25 +++++-
server/config.docker.yaml | 2 +
server/config.yaml | 36 ++++++--
server/config/captcha.go | 8 +-
server/config/config.go | 1 +
server/config/gorm_mssql.go | 13 +++
server/go.mod | 2 +-
server/initialize/db_list.go | 2 +
server/initialize/gorm.go | 4 +
server/initialize/gorm_mssql.go | 59 +++++++++++++
server/model/system/response/sys_captcha.go | 1 +
.../service/system/sys_auto_code_interface.go | 2 +
server/service/system/sys_auto_code_mssql.go | 58 +++++++++++++
server/utils/verify.go | 2 +-
web/src/api/menu.js | 2 +-
web/src/view/init/index.vue | 83 ++++++++++++-------
web/src/view/login/index.vue | 4 +-
web/src/view/superAdmin/user/user.vue | 6 ++
19 files changed, 293 insertions(+), 46 deletions(-)
create mode 100644 server/config/gorm_mssql.go
create mode 100644 server/initialize/gorm_mssql.go
create mode 100644 server/service/system/sys_auto_code_mssql.go
diff --git a/server/api/v1/system/sys_captcha.go b/server/api/v1/system/sys_captcha.go
index 5f468f1a0..c61714e59 100644
--- a/server/api/v1/system/sys_captcha.go
+++ b/server/api/v1/system/sys_captcha.go
@@ -1,6 +1,8 @@
package system
import (
+ "time"
+
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
@@ -21,9 +23,22 @@ type BaseApi struct{}
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
-// @Success 200 {object} response.Response{data=systemRes.SysCaptchaResponse,msg=string} "生成验证码,返回包括随机数id,base64,验证码长度"
+// @Success 200 {object} response.Response{data=systemRes.SysCaptchaResponse,msg=string} "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码"
// @Router /base/captcha [post]
func (b *BaseApi) Captcha(c *gin.Context) {
+ // 判断验证码是否开启
+ openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数
+ openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间
+ key := c.ClientIP()
+ v, ok := global.BlackCache.Get(key)
+ if !ok {
+ global.BlackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut))
+ }
+
+ var oc bool
+ if openCaptcha == 0 || openCaptcha < interfaceToInt(v) {
+ oc = true
+ }
// 字符,公式,验证码配置
// 生成默认数字的driver
driver := base64Captcha.NewDriverDigit(global.GVA_CONFIG.Captcha.ImgHeight, global.GVA_CONFIG.Captcha.ImgWidth, global.GVA_CONFIG.Captcha.KeyLong, 0.7, 80)
@@ -39,5 +54,17 @@ func (b *BaseApi) Captcha(c *gin.Context) {
CaptchaId: id,
PicPath: b64s,
CaptchaLength: global.GVA_CONFIG.Captcha.KeyLong,
+ OpenCaptcha: oc,
}, "验证码获取成功", c)
}
+
+// 类型转换
+func interfaceToInt(v interface{}) (i int) {
+ switch v := v.(type) {
+ case int:
+ i = v
+ default:
+ i = 0
+ }
+ return
+}
diff --git a/server/api/v1/system/sys_user.go b/server/api/v1/system/sys_user.go
index adac3341a..294a7e140 100644
--- a/server/api/v1/system/sys_user.go
+++ b/server/api/v1/system/sys_user.go
@@ -2,6 +2,7 @@ package system
import (
"strconv"
+ "time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
@@ -26,6 +27,8 @@ import (
func (b *BaseApi) Login(c *gin.Context) {
var l systemReq.Login
err := c.ShouldBindJSON(&l)
+ key := c.ClientIP()
+
if err != nil {
response.FailWithMessage(err.Error(), c)
return
@@ -35,22 +38,42 @@ func (b *BaseApi) Login(c *gin.Context) {
response.FailWithMessage(err.Error(), c)
return
}
- if store.Verify(l.CaptchaId, l.Captcha, true) {
+
+ // 判断验证码是否开启
+ openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数
+ openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间
+ v, ok := global.BlackCache.Get(key)
+ if !ok {
+ global.BlackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut))
+ }
+
+ var oc bool
+ if openCaptcha == 0 || openCaptcha < interfaceToInt(v) {
+ oc = true
+ }
+
+ if !oc || store.Verify(l.CaptchaId, l.Captcha, true) {
u := &system.SysUser{Username: l.Username, Password: l.Password}
user, err := userService.Login(u)
if err != nil {
global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err))
+ // 验证码次数+1
+ global.BlackCache.Increment(key, 1)
response.FailWithMessage("用户名不存在或者密码错误", c)
return
}
if user.Enable != 1 {
global.GVA_LOG.Error("登陆失败! 用户被禁止登录!")
+ // 验证码次数+1
+ global.BlackCache.Increment(key, 1)
response.FailWithMessage("用户被禁止登录", c)
return
}
b.TokenNext(c, *user)
return
}
+ // 验证码次数+1
+ global.BlackCache.Increment(key, 1)
response.FailWithMessage("验证码错误", c)
}
diff --git a/server/config.docker.yaml b/server/config.docker.yaml
index 71fbfa967..164c3be09 100644
--- a/server/config.docker.yaml
+++ b/server/config.docker.yaml
@@ -51,6 +51,8 @@ captcha:
key-long: 6
img-width: 240
img-height: 80
+ open-captcha: 0 # 0代表一直开启,大于0代表限制次数
+ open-captcha-timeout: 3600 # open-captcha大于0时才生效
# mysql connect configuration
# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master)
diff --git a/server/config.yaml b/server/config.yaml
index 518527c33..cd3924941 100644
--- a/server/config.yaml
+++ b/server/config.yaml
@@ -35,11 +35,11 @@ email:
# system configuration
system:
- env: public # Change to "develop" to skip authentication for development mode
+ env: public # Change to "develop" to skip authentication for development mode
addr: 8888
db-type: mysql
- oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置
- use-redis: false # 使用redis
+ oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置
+ use-redis: false # 使用redis
use-multipoint: false
# IP限制次数 一个小时15000次
iplimit-count: 15000
@@ -51,6 +51,8 @@ captcha:
key-long: 6
img-width: 240
img-height: 80
+ open-captcha: 0 # 0代表一直开启,大于0代表限制次数
+ open-captcha-timeout: 3600 # open-captcha大于0时才生效
# mysql connect configuration
# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master)
@@ -79,10 +81,31 @@ pgsql:
max-open-conns: 100
log-mode: ""
log-zap: false
-
+oracle:
+ path: ""
+ port: ""
+ config: ""
+ db-name: ""
+ username: ""
+ password: ""
+ max-idle-conns: 10
+ max-open-conns: 100
+ log-mode: ""
+ log-zap: false
+mssql:
+ path: ""
+ port: ""
+ config: ""
+ db-name: ""
+ username: ""
+ password: ""
+ max-idle-conns: 10
+ max-open-conns: 100
+ log-mode: ""
+ log-zap: false
db-list:
- disable: true # 是否禁用
- type: "" # 数据库的类型,目前支持mysql、pgsql
+ type: "" # 数据库的类型,目前支持mysql、pgsql、mssql、oracle
alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一
path: ""
port: ""
@@ -95,7 +118,6 @@ db-list:
log-mode: ""
log-zap: false
-
# local configuration
local:
path: uploads/file
@@ -175,7 +197,7 @@ excel:
# timer task db clear table
Timer:
start: true
- spec: "@daily" # 定时任务详细配置参考 https://pkg.go.dev/github.com/robfig/cron/v3
+ spec: "@daily" # 定时任务详细配置参考 https://pkg.go.dev/github.com/robfig/cron/v3
detail:
- tableName: sys_operation_records
compareField: created_at
diff --git a/server/config/captcha.go b/server/config/captcha.go
index f629c17f1..074a9bfad 100644
--- a/server/config/captcha.go
+++ b/server/config/captcha.go
@@ -1,7 +1,9 @@
package config
type Captcha struct {
- KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度
- ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度
- ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度
+ KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度
+ ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度
+ ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度
+ OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码
+ OpenCaptchaTimeOut int `mapstructure:"open-captcha-timeout" json:"open-captcha-timeout" yaml:"open-captcha-timeout"` // 防爆破验证码超时时间,单位:s(秒)
}
diff --git a/server/config/config.go b/server/config/config.go
index a9b2c2eaa..91642f7be 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -11,6 +11,7 @@ type Server struct {
AutoCode Autocode `mapstructure:"autocode" json:"autocode" yaml:"autocode"`
// gorm
Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"`
+ Mssql Mssql `mapstructure:"mssql" json:"mssql" yaml:"mssql"`
Pgsql Pgsql `mapstructure:"pgsql" json:"pgsql" yaml:"pgsql"`
Oracle Oracle `mapstructure:"oracle" json:"oracle" yaml:"oracle"`
DBList []SpecializedDB `mapstructure:"db-list" json:"db-list" yaml:"db-list"`
diff --git a/server/config/gorm_mssql.go b/server/config/gorm_mssql.go
new file mode 100644
index 000000000..db299e1e3
--- /dev/null
+++ b/server/config/gorm_mssql.go
@@ -0,0 +1,13 @@
+package config
+
+type Mssql struct {
+ GeneralDB `yaml:",inline" mapstructure:",squash"`
+}
+//dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
+func (m *Mssql) Dsn() string {
+ return "sqlserver://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "?database=" + m.Dbname + "&encrypt=disable"
+}
+
+func (m *Mssql) GetLogMode() string {
+ return m.LogMode
+}
diff --git a/server/go.mod b/server/go.mod
index 441477873..695511fbc 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -39,6 +39,7 @@ require (
golang.org/x/text v0.3.7
gorm.io/driver/mysql v1.3.3
gorm.io/driver/postgres v1.3.4
+ gorm.io/driver/sqlserver v1.3.2
gorm.io/gorm v1.23.4
nhooyr.io/websocket v1.8.6
)
@@ -124,7 +125,6 @@ require (
gopkg.in/ini.v1 v1.55.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
- gorm.io/driver/sqlserver v1.3.2 // indirect
gorm.io/plugin/dbresolver v1.1.0 // indirect
modernc.org/libc v1.15.1 // indirect
modernc.org/mathutil v1.4.1 // indirect
diff --git a/server/initialize/db_list.go b/server/initialize/db_list.go
index 575592079..90eef9ea1 100644
--- a/server/initialize/db_list.go
+++ b/server/initialize/db_list.go
@@ -17,6 +17,8 @@ func DBList() {
switch info.Type {
case "mysql":
dbMap[info.AliasName] = GormMysqlByConfig(config.Mysql{GeneralDB: info.GeneralDB})
+ case "mssql":
+ dbMap[info.AliasName] = GormMssqlByConfig(config.Mssql{GeneralDB: info.GeneralDB})
case "pgsql":
dbMap[info.AliasName] = GormPgSqlByConfig(config.Pgsql{GeneralDB: info.GeneralDB})
case "oracle":
diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go
index 9acb4937f..bb4d91661 100644
--- a/server/initialize/gorm.go
+++ b/server/initialize/gorm.go
@@ -19,6 +19,10 @@ func Gorm() *gorm.DB {
return GormMysql()
case "pgsql":
return GormPgSql()
+ case "oracle":
+ return GormOracle()
+ case "mssql":
+ return GormMssql()
default:
return GormMysql()
}
diff --git a/server/initialize/gorm_mssql.go b/server/initialize/gorm_mssql.go
new file mode 100644
index 000000000..0ec25a7f5
--- /dev/null
+++ b/server/initialize/gorm_mssql.go
@@ -0,0 +1,59 @@
+/*
+ * @Author: 逆光飞翔 191180776@qq.com
+ * @Date: 2022-12-08 17:25:49
+ * @LastEditors: 逆光飞翔 191180776@qq.com
+ * @LastEditTime: 2022-12-08 18:00:00
+ * @FilePath: \server\initialize\gorm_mssql.go
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+package initialize
+
+import (
+ "github.com/flipped-aurora/gin-vue-admin/server/config"
+ "github.com/flipped-aurora/gin-vue-admin/server/global"
+ "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal"
+ "gorm.io/driver/sqlserver"
+ "gorm.io/gorm"
+)
+
+// GormMssql 初始化Mssql数据库
+// Author [LouisZhang](191180776@qq.com)
+func GormMssql() *gorm.DB {
+ m := global.GVA_CONFIG.Mssql
+ if m.Dbname == "" {
+ return nil
+ }
+ mssqlConfig := sqlserver.Config{
+ DSN: m.Dsn(), // DSN data source name
+ DefaultStringSize: 191, // string 类型字段的默认长度
+ }
+ if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
+ return nil
+ } else {
+ db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)
+ sqlDB, _ := db.DB()
+ sqlDB.SetMaxIdleConns(m.MaxIdleConns)
+ sqlDB.SetMaxOpenConns(m.MaxOpenConns)
+ return db
+ }
+}
+
+// GormMssqlByConfig 初始化Mysql数据库用过传入配置
+func GormMssqlByConfig(m config.Mssql) *gorm.DB {
+ if m.Dbname == "" {
+ return nil
+ }
+ mssqlConfig := sqlserver.Config{
+ DSN: m.Dsn(), // DSN data source name
+ DefaultStringSize: 191, // string 类型字段的默认长度
+ }
+ if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
+ panic(err)
+ } else {
+ db.InstanceSet("gorm:table_options", "ENGINE=InnoDB")
+ sqlDB, _ := db.DB()
+ sqlDB.SetMaxIdleConns(m.MaxIdleConns)
+ sqlDB.SetMaxOpenConns(m.MaxOpenConns)
+ return db
+ }
+}
diff --git a/server/model/system/response/sys_captcha.go b/server/model/system/response/sys_captcha.go
index 0fc8b0694..0c3995a1d 100644
--- a/server/model/system/response/sys_captcha.go
+++ b/server/model/system/response/sys_captcha.go
@@ -4,4 +4,5 @@ type SysCaptchaResponse struct {
CaptchaId string `json:"captchaId"`
PicPath string `json:"picPath"`
CaptchaLength int `json:"captchaLength"`
+ OpenCaptcha bool `json:"openCaptcha"`
}
diff --git a/server/service/system/sys_auto_code_interface.go b/server/service/system/sys_auto_code_interface.go
index 3942860b8..d79092124 100644
--- a/server/service/system/sys_auto_code_interface.go
+++ b/server/service/system/sys_auto_code_interface.go
@@ -29,6 +29,8 @@ func (autoCodeService *AutoCodeService) Database(businessDB string) Database {
switch info.Type {
case "mysql":
return AutoCodeMysql
+ case "mssql":
+ return AutoCodeMssql
case "pgsql":
return AutoCodePgsql
case "oracle":
diff --git a/server/service/system/sys_auto_code_mssql.go b/server/service/system/sys_auto_code_mssql.go
new file mode 100644
index 000000000..834a499be
--- /dev/null
+++ b/server/service/system/sys_auto_code_mssql.go
@@ -0,0 +1,58 @@
+package system
+
+import (
+ "fmt"
+ "github.com/flipped-aurora/gin-vue-admin/server/global"
+ "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
+)
+
+var AutoCodeMssql = new(autoCodeMssql)
+
+type autoCodeMssql struct{}
+
+// GetDB 获取数据库的所有数据库名
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeMssql) GetDB(businessDB string) (data []response.Db, err error) {
+ var entities []response.Db
+ sql := "select name AS 'database' from sysdatabases;"
+ if businessDB == "" {
+ err = global.GVA_DB.Raw(sql).Scan(&entities).Error
+ } else {
+ err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
+ }
+ return entities, err
+}
+
+// GetTables 获取数据库的所有表名
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeMssql) GetTables(businessDB string, dbName string) (data []response.Table, err error) {
+ var entities []response.Table
+
+ sql := fmt.Sprintf(`select name as 'table_name' from %s.DBO.sysobjects where xtype='U'`, dbName)
+ if businessDB == "" {
+ err = global.GVA_DB.Raw(sql).Scan(&entities).Error
+ } else {
+ err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
+ }
+
+ return entities, err
+}
+
+// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等
+// Author [piexlmax](https://github.com/piexlmax)
+// Author [SliverHorn](https://github.com/SliverHorn)
+func (s *autoCodeMssql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) {
+ var entities []response.Column
+ sql := fmt.Sprintf(`select sc.name as column_name,st.name as data_type, sc.length as data_type_long
+ from %s.DBO.syscolumns sc,systypes st where sc.xtype=st.xtype and st.usertype=0 and sc.id in (select id from %s.DBO.sysobjects where xtype='U' and name='%s');`, dbName, dbName, tableName)
+
+ if businessDB == "" {
+ err = global.GVA_DB.Raw(sql).Scan(&entities).Error
+ } else {
+ err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error
+ }
+
+ return entities, err
+}
diff --git a/server/utils/verify.go b/server/utils/verify.go
index 7a6b622f3..3d34fe8b7 100644
--- a/server/utils/verify.go
+++ b/server/utils/verify.go
@@ -5,7 +5,7 @@ var (
ApiVerify = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}}
MenuVerify = Rules{"Path": {NotEmpty()}, "ParentId": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}}
MenuMetaVerify = Rules{"Title": {NotEmpty()}}
- LoginVerify = Rules{"CaptchaId": {NotEmpty()}, "Captcha": {NotEmpty()}, "Username": {NotEmpty()}, "Password": {NotEmpty()}}
+ LoginVerify = Rules{"CaptchaId": {NotEmpty()}, "Captcha": {}, "Username": {NotEmpty()}, "Password": {NotEmpty()}}
RegisterVerify = Rules{"Username": {NotEmpty()}, "NickName": {NotEmpty()}, "Password": {NotEmpty()}, "AuthorityId": {NotEmpty()}}
PageInfoVerify = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}}
CustomerVerify = Rules{"CustomerName": {NotEmpty()}, "CustomerPhoneData": {NotEmpty()}}
diff --git a/web/src/api/menu.js b/web/src/api/menu.js
index 54f363ab2..163b5a697 100644
--- a/web/src/api/menu.js
+++ b/web/src/api/menu.js
@@ -72,7 +72,7 @@ export const getMenuAuthority = (data) => {
})
}
-// @Summary 获取用户menu关联关系
+// @Summary 删除menu
// @Produce application/json
// @Param ID float64
// @Router /menu/deleteBaseMenu [post]
diff --git a/web/src/view/init/index.vue b/web/src/view/init/index.vue
index 8a27c0228..a3261ae82 100644
--- a/web/src/view/init/index.vue
+++ b/web/src/view/init/index.vue
@@ -19,12 +19,15 @@
-