diff --git a/go.mod b/go.mod index 8524b4e2..3754093b 100644 --- a/go.mod +++ b/go.mod @@ -19,13 +19,15 @@ require ( github.com/mattn/go-colorable v0.1.10 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-sqlite3 v1.14.8 // indirect + github.com/mojocn/base64Captcha v1.3.5 // indirect github.com/robertkrimen/otto v0.0.0-20210927222213-f9375a256948 // indirect github.com/spf13/cobra v1.2.1 // indirect github.com/stretchr/testify v1.7.0 - github.com/yaoapp/gou v0.0.0-20210929042348-e322f26e3a51 // indirect + github.com/yaoapp/gou v0.0.0-20210929100518-8de252e7bab8 // indirect github.com/yaoapp/kun v0.6.3 github.com/yaoapp/xun v0.5.2 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b // indirect golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect google.golang.org/genproto v0.0.0-20210928142010-c7af6a1a74c9 // indirect diff --git a/go.sum b/go.sum index 49dcc004..ff9d7ec7 100644 --- a/go.sum +++ b/go.sum @@ -117,6 +117,8 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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= @@ -292,6 +294,10 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/mojocn/base64Captcha v1.2.2 h1:NTFnThPVrb3tR66JO/N8/ZHsyFrNc7ho+xRpxBUEIlo= +github.com/mojocn/base64Captcha v1.2.2/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY= +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/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= @@ -359,6 +365,8 @@ github.com/yaoapp/gou v0.0.0-20210929002807-fbe45c391f18 h1:diVG7thFYgqhttHbiGxX github.com/yaoapp/gou v0.0.0-20210929002807-fbe45c391f18/go.mod h1:KwCRZDnEKLCxUNgMQ2acU7WI5zpt5SGOt1sBKqNAiaE= github.com/yaoapp/gou v0.0.0-20210929042348-e322f26e3a51 h1:LlWIuWmbEETkY91qP/yg4mP0VzGde5xO2DmgRGVJfxQ= github.com/yaoapp/gou v0.0.0-20210929042348-e322f26e3a51/go.mod h1:mJj2wa3oXVlk/WOHazW8eEZTyva98zDVjIur9lj8aiA= +github.com/yaoapp/gou v0.0.0-20210929100518-8de252e7bab8 h1:MPVIqqqF5PV3SEy9a4XNghWoErLp5MQDMDTqA2ikbvw= +github.com/yaoapp/gou v0.0.0-20210929100518-8de252e7bab8/go.mod h1:mJj2wa3oXVlk/WOHazW8eEZTyva98zDVjIur9lj8aiA= github.com/yaoapp/kun v0.6.1 h1:gbjP5wmAuJLe1RGZ2evH4rPCVWykf0iqbksaHikF/GI= github.com/yaoapp/kun v0.6.1/go.mod h1:igsTcWDnzpp0HtRN7sBP+XOEN2tzoC5hk2MyoPFs3xA= github.com/yaoapp/kun v0.6.2 h1:QbtdZpVIklRDGEvL5YYRf/eR05YFZVFk/8rX2vy2K0Q= @@ -409,7 +417,11 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 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/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 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= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/main.go b/main.go index 6a6956a5..5a2773d6 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/yaoapp/xiang/cmd" + _ "github.com/yaoapp/xiang/user" ) // 主程序 diff --git a/user/captcha.go b/user/captcha.go new file mode 100644 index 00000000..65f7ef7b --- /dev/null +++ b/user/captcha.go @@ -0,0 +1,131 @@ +package user + +import ( + "errors" + "image/color" + + "github.com/mojocn/base64Captcha" + "github.com/yaoapp/kun/exception" +) + +var captchaStore = base64Captcha.DefaultMemStore + +// CaptchaOption 验证码配置 +type CaptchaOption struct { + Type string + Height int + Width int + Length int + Lang string + Background string +} + +// NewCaptchaOption 创建验证码配置 +func NewCaptchaOption() CaptchaOption { + return CaptchaOption{ + Width: 240, + Height: 80, + Length: 4, + Lang: "zh", + Background: "#FFFFFF", + } +} + +// MakeCaptcha 制作验证码 +func MakeCaptcha(option CaptchaOption) (string, string) { + + if option.Width == 0 { + option.Width = 240 + } + + if option.Height == 0 { + option.Width = 80 + } + + if option.Length == 0 { + option.Length = 4 + } + + if option.Lang == "" { + option.Lang = "zh" + } + + var driver base64Captcha.Driver + switch option.Type { + case "audio": + driver = base64Captcha.NewDriverAudio(option.Length, option.Lang) + break + case "math": + background := background(option.Background) + driver = base64Captcha.NewDriverMath( + option.Height, option.Width, 3, + base64Captcha.OptionShowHollowLine, background, + base64Captcha.DefaultEmbeddedFonts, []string{}, + ) + break + default: + driver = base64Captcha.NewDriverDigit( + option.Height, option.Width, 5, + 0.7, 80, + ) + break + } + + c := base64Captcha.NewCaptcha(driver, captchaStore) + id, content, err := c.Generate() + if err != nil { + exception.New("生成验证码出错 %s", 500, err).Throw() + } + return id, content +} + +// ValidateCaptcha 校验验证码 +func ValidateCaptcha(id string, value string) bool { + return captchaStore.Verify(id, value, true) +} + +func background(s string) *color.RGBA { + if s == "" { + s = "#555555" + } + bg, err := parseHexColorFast(s) + if err != nil { + exception.New("背景色格式错误 %s", 400, s).Throw() + } + return &bg +} + +func parseHexColorFast(s string) (c color.RGBA, err error) { + c.A = 0xff + + if s[0] != '#' { + return c, errors.New("invalid format") + } + + hexToByte := func(b byte) byte { + switch { + case b >= '0' && b <= '9': + return b - '0' + case b >= 'a' && b <= 'f': + return b - 'a' + 10 + case b >= 'A' && b <= 'F': + return b - 'A' + 10 + } + err = errors.New("invalid format") + return 0 + } + + switch len(s) { + case 7: + c.R = hexToByte(s[1])<<4 + hexToByte(s[2]) + c.G = hexToByte(s[3])<<4 + hexToByte(s[4]) + c.B = hexToByte(s[5])<<4 + hexToByte(s[6]) + case 4: + c.R = hexToByte(s[1]) * 17 + c.G = hexToByte(s[2]) * 17 + c.B = hexToByte(s[3]) * 17 + default: + err = errors.New("invalid format") + } + return +} diff --git a/user/process.go b/user/process.go new file mode 100644 index 00000000..692030de --- /dev/null +++ b/user/process.go @@ -0,0 +1,50 @@ +package user + +import ( + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/kun/maps" +) + +func init() { + // 注册处理器 + gou.RegisterProcessHandler("xiang.user.Captcha", ProcessCaptcha) +} + +// ProcessLogin xiang.user.Login 用户登录 +func ProcessLogin(process *gou.Process) interface{} { + return nil +} + +// ProcessCaptcha xiang.user.Captcha 验证码 +func ProcessCaptcha(process *gou.Process) interface{} { + process.ValidateArgNums(1) + option := CaptchaOption{ + Width: any.Of(process.ArgsURLValue(0, "width", "240")).CInt(), + Height: any.Of(process.ArgsURLValue(0, "height", "80")).CInt(), + Length: any.Of(process.ArgsURLValue(0, "height", "4")).CInt(), + Type: process.ArgsURLValue(0, "type", "math"), + Background: process.ArgsURLValue(0, "background", "#FFFFFF"), + Lang: process.ArgsURLValue(0, "lang", "zh"), + } + id, content := MakeCaptcha(option) + return maps.Map{ + "id": id, + "content": content, + } +} + +// ProcessToken xiang.user.Token 使用 Key & Secret 换取 Token +func ProcessToken(process *gou.Process) interface{} { + return nil +} + +// ProcessTokenRefresh xiang.user.TokenRefresh 刷新Token +func ProcessTokenRefresh(process *gou.Process) interface{} { + return nil +} + +// ProcessInfo xiang.user.Info 读取当前用户资料 +func ProcessInfo(process *gou.Process) interface{} { + return nil +} diff --git a/user/user.go b/user/user.go new file mode 100644 index 00000000..a00006b6 --- /dev/null +++ b/user/user.go @@ -0,0 +1 @@ +package user diff --git a/user/user_test.go b/user/user_test.go new file mode 100644 index 00000000..7c5f604c --- /dev/null +++ b/user/user_test.go @@ -0,0 +1,45 @@ +package user + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCaptcha(t *testing.T) { + id, content := MakeCaptcha(CaptchaOption{ + Type: "audio", + Width: 240, + Height: 80, + Length: 4, + Lang: "zh", + }) + assert.IsType(t, "string", id) + assert.IsType(t, "string", content) + captchaStore.Get(id, false) + assert.True(t, ValidateCaptcha(id, captchaStore.Get(id, false))) + + id, content = MakeCaptcha(CaptchaOption{ + Type: "math", + Width: 240, + Height: 80, + Length: 4, + Lang: "zh", + }) + assert.IsType(t, "string", id) + assert.IsType(t, "string", content) + captchaStore.Get(id, false) + assert.True(t, ValidateCaptcha(id, captchaStore.Get(id, false))) + + id, content = MakeCaptcha(CaptchaOption{ + Type: "digit", + Width: 240, + Height: 80, + Length: 4, + Lang: "zh", + }) + assert.IsType(t, "string", id) + assert.IsType(t, "string", content) + captchaStore.Get(id, false) + assert.True(t, ValidateCaptcha(id, captchaStore.Get(id, false))) +} diff --git a/xiang/apis/user.http.json b/xiang/apis/user.http.json new file mode 100644 index 00000000..cb666614 --- /dev/null +++ b/xiang/apis/user.http.json @@ -0,0 +1,62 @@ +{ + "name": "系统用户接口", + "version": "1.0.0", + "description": "系统用户API", + "group": "xiang/user", + "guard": "bearer-jwt", + "paths": [ + { + "path": "/captcha", + "method": "GET", + "guard": "-", + "process": "xiang.user.Captcha", + "in": [":query"], + "out": { + "status": 200, + "type": "application/json" + } + }, + { + "path": "/login", + "method": "POST", + "guard": "-", + "process": "xiang.user.Login", + "in": ["$payload.email", "$payload.password", "$payload.captcha"], + "out": { + "status": 200, + "type": "application/json" + } + }, + { + "path": "/token", + "method": "POST", + "guard": "-", + "process": "xiang.user.Token", + "in": ["$payload.key", "$payload.secret"], + "out": { + "status": 200, + "type": "application/json" + } + }, + { + "path": "/info", + "method": "GET", + "process": "xiang.user.Info", + "in": [], + "out": { + "status": 200, + "type": "application/json" + } + }, + { + "path": "/token/refresh", + "method": "GET", + "process": "xiang.user.TokenRefresh", + "in": [], + "out": { + "status": 200, + "type": "application/json" + } + } + ] +} diff --git a/xiang/apis/xiang.http.json b/xiang/apis/xiang.http.json index dbabb92c..499f995a 100644 --- a/xiang/apis/xiang.http.json +++ b/xiang/apis/xiang.http.json @@ -9,7 +9,18 @@ "path": "/ping", "method": "GET", "guard": "-", - "process": "xiang.global.ping", + "process": "xiang.global.Ping", + "in": [], + "out": { + "status": 200, + "type": "application/json" + } + }, + { + "path": "/info", + "method": "GET", + "guard": "-", + "process": "xiang.global.Info", "in": [], "out": { "status": 200,