+ JWT, Captcha, Password 校验

This commit is contained in:
Max 2021-11-21 21:15:33 +08:00
parent 3d9e85e216
commit 3f68510076
13 changed files with 554 additions and 140 deletions

View file

@ -89,17 +89,17 @@ func NewConfig(envfile ...string) Config {
if len(envfile) > 0 {
file, err := filepath.Abs(envfile[0])
if err != nil {
log.Printf("加载环境配置文件%s出错 %s\n", envfile[0], err.Error())
} else {
if err == nil {
// log.Printf("加载环境配置文件%s出错 %s\n", envfile[0], err.Error())
// } else {
filename = file
}
}
err := godotenv.Overload(filename)
if err != nil {
log.Printf("加载环境配置文件%s出错 %s\n", filename, err.Error())
}
godotenv.Overload(filename)
// if err != nil {
// log.Printf("加载环境配置文件%s出错 %s\n", filename, err.Error())
// }
cfg := Config{}
if err := env.Parse(&cfg); err != nil {

47
helper/array.process.go Normal file
View file

@ -0,0 +1,47 @@
package helper
import "github.com/yaoapp/gou"
// ProcessArrayPluck xiang.helper.ArrayPluck 将多个数据记录集合,合并为一个数据记录集合
func ProcessArrayPluck(process *gou.Process) interface{} {
process.ValidateArgNums(2)
columns := process.ArgsStrings(0)
pluck := process.ArgsMap(1)
return ArrayPluck(columns, pluck)
}
// ProcessArraySplit xiang.helper.ArraySplit 将多条数记录集合,分解为一个 columns:[]string 和 values: [][]interface{}
func ProcessArraySplit(process *gou.Process) interface{} {
process.ValidateArgNums(1)
records := process.ArgsRecords(0)
columns, values := ArraySplit(records)
return map[string]interface{}{
"columns": columns,
"values": values,
}
}
// ProcessArrayColumn xiang.helper.ArrayColumn 返回多条数据记录,指定字段数值。
func ProcessArrayColumn(process *gou.Process) interface{} {
process.ValidateArgNums(2)
records := process.ArgsRecords(0)
name := process.ArgsString(1)
values := ArrayColumn(records, name)
return values
}
// ProcessArrayKeep xiang.helper.ArrayKeep 仅保留指定键名的数据
func ProcessArrayKeep(process *gou.Process) interface{} {
process.ValidateArgNums(2)
records := process.ArgsRecords(0)
columns := process.ArgsStrings(1)
return ArrayKeep(records, columns)
}
// ProcessArrayTree xiang.helper.ArrayTree 转换为属性结构
func ProcessArrayTree(process *gou.Process) interface{} {
process.ValidateArgNums(2)
records := process.ArgsRecords(0)
setting := process.ArgsMap(1)
return ArrayTree(records, setting)
}

View file

@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou"
"github.com/yaoapp/kun/maps"
)
@ -106,3 +107,50 @@ func TestArrayTree(t *testing.T) {
res := ArrayTree(records, map[string]interface{}{"parent": "parent_id"})
assert.Equal(t, 9, len(res))
}
func TestProcessArrayPluck(t *testing.T) {
args := []interface{}{
[]interface{}{"城市", "行业", "计费"},
map[string]interface{}{
"行业": map[string]interface{}{"key": "city", "value": "数量", "items": []map[string]interface{}{{"city": "北京", "数量": 32}, {"city": "上海", "数量": 20}}},
"计费": map[string]interface{}{"key": "city", "value": "计费种类", "items": []map[string]interface{}{{"city": "北京", "计费种类": 6}, {"city": "西安", "计费种类": 3}}},
},
}
process := gou.NewProcess("xiang.helper.ArrayPluck", args...)
response := ProcessArrayPluck(process)
assert.NotNil(t, response)
items, ok := response.([]map[string]interface{})
assert.True(t, ok)
assert.Equal(t, 3, len(items))
for _, item := range items {
maps.Of(item).Has("城市")
maps.Of(item).Has("行业")
maps.Of(item).Has("计费")
}
}
func TestProcessArraySplit(t *testing.T) {
args := []interface{}{
[]map[string]interface{}{
{"name": "阿里云计算有限公司", "short_name": "阿里云"},
{"name": "世纪互联蓝云", "short_name": "上海蓝云"},
},
}
process := gou.NewProcess("xiang.helper.ArraySplit", args...)
response := process.Run()
assert.NotNil(t, response)
res, ok := response.(map[string]interface{})
assert.True(t, ok)
columns, ok := res["columns"].([]string)
assert.True(t, ok)
values, ok := res["values"].([][]interface{})
assert.True(t, ok)
assert.Equal(t, 2, len(columns))
assert.Equal(t, 2, len(values))
for _, value := range values {
assert.Equal(t, 2, len(value))
}
}

170
helper/captcha.go Normal file
View file

@ -0,0 +1,170 @@
package helper
import (
"errors"
"image/color"
"github.com/mojocn/base64Captcha"
"github.com/yaoapp/gou"
"github.com/yaoapp/kun/any"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/kun/maps"
"github.com/yaoapp/xiang/config"
"github.com/yaoapp/xiang/xlog"
)
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",
}
}
// CaptchaMake 制作验证码
func CaptchaMake(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 := captchaBackground(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()
}
// 打印日志
if config.IsDebug() {
xlog.Println("图形/音频 ID:", id, "验证码:", captchaStore.Get(id, false))
}
return id, content
}
// CaptchaValidate 校验验证码
func CaptchaValidate(id string, value string) bool {
return captchaStore.Verify(id, value, true)
}
func captchaBackground(s string) *color.RGBA {
if s == "" {
s = "#555555"
}
bg, err := captchaParseHexColorFast(s)
if err != nil {
exception.New("背景色格式错误 %s", 400, s).Throw()
}
return &bg
}
func captchaParseHexColorFast(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
}
// ProcessCaptchaValidate xiang.helper.CaptchaValidate 校验图形/音频验证码
func ProcessCaptchaValidate(process *gou.Process) interface{} {
process.ValidateArgNums(2)
if !CaptchaValidate(process.ArgsString(0), process.ArgsString(1)) {
exception.New("验证码不正确", 400).Throw()
return false
}
return true
}
// ProcessCaptcha xiang.helper.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 := CaptchaMake(option)
return maps.Map{
"id": id,
"content": content,
}
}

65
helper/captcha_test.go Normal file
View file

@ -0,0 +1,65 @@
package helper
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou"
"github.com/yaoapp/kun/maps"
)
func TestCaptcha(t *testing.T) {
id, content := CaptchaMake(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, CaptchaValidate(id, captchaStore.Get(id, false)))
id, content = CaptchaMake(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, CaptchaValidate(id, captchaStore.Get(id, false)))
id, content = CaptchaMake(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, CaptchaValidate(id, captchaStore.Get(id, false)))
}
func TestProcessCaptcha(t *testing.T) {
args := url.Values{}
args.Add("type", "math")
args.Add("lang", "zh")
process := gou.NewProcess("xiang.helper.Captcha", args)
res := process.Run().(maps.Map)
assert.IsType(t, "string", res.Get("id"))
assert.IsType(t, "string", res.Get("content"))
value := captchaStore.Get(res.Get("id").(string), false)
process = gou.NewProcess("xiang.helper.CaptchaValidate", res.Get("id"), value)
assert.True(t, process.Run().(bool))
assert.Panics(t, func() {
gou.NewProcess("xiang.helper.CaptchaValidate", res.Get("id"), "xxx").Run()
})
}

97
helper/jwt.go Normal file
View file

@ -0,0 +1,97 @@
package helper
import (
"fmt"
"time"
"github.com/golang-jwt/jwt"
"github.com/yaoapp/gou"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/xiang/config"
)
// JwtClaims 用户Token
type JwtClaims struct {
ID int
Data map[string]interface{}
jwt.StandardClaims
}
// JwtValidate JWT 校验
func JwtValidate(tokenString string) map[string]interface{} {
token, err := jwt.ParseWithClaims(tokenString, &JwtClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(config.Conf.JWT.Secret), nil
})
if err != nil {
exception.New("令牌无效", 403).Ctx(err.Error()).Throw()
return nil
}
if claims, ok := token.Claims.(*JwtClaims); ok && token.Valid {
return claims.Data
}
exception.New("令牌无效", 403).Ctx(token.Claims).Throw()
return nil
}
// JwtMake 生成 JWT
// subject options[0], audience options[1], issuer options[1]
func JwtMake(id int, data map[string]interface{}, timeout int64, options ...string) string {
now := time.Now().Unix()
expiresAt := now + timeout
uid := fmt.Sprintf("%d", id)
subject := "User Token"
audience := "Xiang Metadata Admin Panel"
issuer := fmt.Sprintf("xiang:%d", id)
length := len(options)
if length > 0 {
subject = options[0]
}
if length > 1 {
audience = options[1]
}
if length > 2 {
issuer = options[2]
}
claims := &JwtClaims{
ID: id,
Data: data,
StandardClaims: jwt.StandardClaims{
Id: uid, // 唯一ID
Subject: subject, // 主题
Audience: audience, // 接收人
ExpiresAt: expiresAt, // 过期时间
NotBefore: now, // 生效时间
IssuedAt: now, // 签发时间
Issuer: issuer, // 签发人
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(config.Conf.JWT.Secret))
if err != nil {
exception.New("生成令牌失败", 500).Ctx(err).Throw()
}
return tokenString
}
// ProcessJwtMake xiang.helper.JwtMake 生成JWT
func ProcessJwtMake(process *gou.Process) interface{} {
process.ValidateArgNums(3)
id := process.ArgsInt(0)
data := process.ArgsMap(1)
timeout := int64(process.ArgsInt(2))
args := []string{}
for i := 3; i < len(process.Args); i++ {
args = append(args, fmt.Sprintf("%v", process.Args[i]))
}
return JwtMake(id, data, timeout, args...)
}
// ProcessJwtValidate xiang.helper.JwtValidate 校验JWT
func ProcessJwtValidate(process *gou.Process) interface{} {
process.ValidateArgNums(1)
tokenString := process.ArgsString(0)
return JwtValidate(tokenString)
}

32
helper/jwt_test.go Normal file
View file

@ -0,0 +1,32 @@
package helper
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou"
)
func TestJwt(t *testing.T) {
data := map[string]interface{}{"hello": "world", "id": 1}
tokenString := JwtMake(1, data, 1, "Unit Test", "Test", "UnitTest")
res := JwtValidate(tokenString)
assert.Equal(t, float64(1), res["id"])
assert.Equal(t, "world", res["hello"])
time.Sleep(2 * time.Second)
assert.Panics(t, func() { JwtValidate(tokenString) })
}
func TestProcessJwt(t *testing.T) {
data := map[string]interface{}{"hello": "world", "id": 1}
args := []interface{}{1, data, 1, "Unit Test", "Test", "UnitTest"}
process := gou.NewProcess("xiang.helper.JwtMake", args...)
token := process.Run()
tokenString := token.(string)
res := gou.NewProcess("xiang.helper.JwtValidate", tokenString).Run().(map[string]interface{})
assert.Equal(t, float64(1), res["id"])
assert.Equal(t, "world", res["hello"])
time.Sleep(2 * time.Second)
assert.Panics(t, func() { gou.NewProcess("xiang.helper.JwtValidate", tokenString).Run() })
}

View file

@ -1,5 +1,7 @@
package helper
import "github.com/yaoapp/gou"
// MapValues 返回映射的数值
func MapValues(record map[string]interface{}) []interface{} {
values := []interface{}{}
@ -17,3 +19,17 @@ func MapKeys(record map[string]interface{}) []string {
}
return keys
}
// ProcessMapValues xiang.helper.MapValues 返回映射的数值
func ProcessMapValues(process *gou.Process) interface{} {
process.ValidateArgNums(1)
record := process.ArgsMap(0)
return MapValues(record)
}
// ProcessMapKeys xiang.helper.MapKeys 返回映射的键
func ProcessMapKeys(process *gou.Process) interface{} {
process.ValidateArgNums(1)
record := process.ArgsMap(0)
return MapKeys(record)
}

21
helper/password.go Normal file
View file

@ -0,0 +1,21 @@
package helper
import (
"github.com/yaoapp/gou"
"golang.org/x/crypto/bcrypt"
)
// PasswordValidate 校验密码
func PasswordValidate(password string, passwordHash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password))
if err != nil {
return false
}
return true
}
// ProcessPasswordValidate xiang.helper.PasswordValidate 校验密码
func ProcessPasswordValidate(process *gou.Process) interface{} {
process.ValidateArgNums(2)
return PasswordValidate(process.ArgsString(0), process.ArgsString(1))
}

27
helper/password_test.go Normal file
View file

@ -0,0 +1,27 @@
package helper
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou"
)
func TestPassword(t *testing.T) {
assert.True(t, PasswordValidate("U123456p+", "$2a$04$TS/rWBs66jADjQl8fa.w..ivkNAjH8d4sI1OPGvEB9Leed6EpzIF2"))
assert.False(t, PasswordValidate("U123456p+", "123456"))
}
func TestProcessPassword(t *testing.T) {
pwd := "U123456p+"
hash := "$2a$04$TS/rWBs66jADjQl8fa.w..ivkNAjH8d4sI1OPGvEB9Leed6EpzIF2"
args := []interface{}{pwd, hash}
process := gou.NewProcess("xiang.helper.PasswordValidate", args...)
res := process.Run()
assert.True(t, res.(bool))
args = []interface{}{pwd, "123456"}
process = gou.NewProcess("xiang.helper.PasswordValidate", args...)
res = process.Run()
assert.False(t, res.(bool))
}

View file

@ -14,88 +14,16 @@ func init() {
gou.RegisterProcessHandler("xiang.helper.ArrayTree", ProcessArrayTree)
gou.RegisterProcessHandler("xiang.helper.MapKeys", ProcessMapKeys)
gou.RegisterProcessHandler("xiang.helper.MapValues", ProcessMapValues)
gou.RegisterProcessHandler("xiang.helper.Captcha", ProcessCaptcha)
gou.RegisterProcessHandler("xiang.helper.CaptchaValidate", ProcessCaptchaValidate)
gou.RegisterProcessHandler("xiang.helper.PasswordValidate", ProcessPasswordValidate)
gou.RegisterProcessHandler("xiang.helper.JwtMake", ProcessJwtMake)
gou.RegisterProcessHandler("xiang.helper.JwtValidate", ProcessJwtValidate)
gou.RegisterProcessHandler("xiang.helper.For", ProcessFor)
gou.RegisterProcessHandler("xiang.helper.Each", ProcessEach)
gou.RegisterProcessHandler("xiang.helper.Print", ProcessPrint)
}
// ProcessArrayPluck xiang.helper.ArrayPluck 将多个数据记录集合,合并为一个数据记录集合
func ProcessArrayPluck(process *gou.Process) interface{} {
process.ValidateArgNums(2)
columns := process.ArgsStrings(0)
pluck := process.ArgsMap(1)
return ArrayPluck(columns, pluck)
}
// ProcessArraySplit xiang.helper.ArraySplit 将多条数记录集合,分解为一个 columns:[]string 和 values: [][]interface{}
func ProcessArraySplit(process *gou.Process) interface{} {
process.ValidateArgNums(1)
records := process.ArgsRecords(0)
columns, values := ArraySplit(records)
return map[string]interface{}{
"columns": columns,
"values": values,
}
}
// ProcessArrayColumn xiang.helper.ArrayColumn 返回多条数据记录,指定字段数值。
func ProcessArrayColumn(process *gou.Process) interface{} {
process.ValidateArgNums(2)
records := process.ArgsRecords(0)
name := process.ArgsString(1)
values := ArrayColumn(records, name)
return values
}
// ProcessArrayKeep xiang.helper.ArrayKeep 仅保留指定键名的数据
func ProcessArrayKeep(process *gou.Process) interface{} {
process.ValidateArgNums(2)
records := process.ArgsRecords(0)
columns := process.ArgsStrings(1)
return ArrayKeep(records, columns)
}
// ProcessArrayTree xiang.helper.ArrayTree 转换为属性结构
func ProcessArrayTree(process *gou.Process) interface{} {
process.ValidateArgNums(2)
records := process.ArgsRecords(0)
setting := process.ArgsMap(1)
return ArrayTree(records, setting)
}
// ProcessMapValues xiang.helper.MapValues 返回映射的数值
func ProcessMapValues(process *gou.Process) interface{} {
process.ValidateArgNums(1)
record := process.ArgsMap(0)
return MapValues(record)
}
// ProcessMapKeys xiang.helper.MapKeys 返回映射的键
func ProcessMapKeys(process *gou.Process) interface{} {
process.ValidateArgNums(1)
record := process.ArgsMap(0)
return MapKeys(record)
}
// ProcessEach xiang.helper.Each 循环过程控制
func ProcessEach(process *gou.Process) interface{} {
process.ValidateArgNums(2)
v := process.Args[0]
p := ProcessOf(process.ArgsMap(1))
Range(v, p)
return nil
}
// ProcessFor xiang.helper.For 循环过程控制
func ProcessFor(process *gou.Process) interface{} {
process.ValidateArgNums(3)
from := process.ArgsInt(0)
to := process.ArgsInt(1)
p := ProcessOf(process.ArgsMap(2))
For(from, to, p)
return nil
}
// ProcessPrint xiang.helper.Print 打印语句
func ProcessPrint(process *gou.Process) interface{} {
process.ValidateArgNums(1)

View file

@ -1,56 +0,0 @@
package helper
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou"
"github.com/yaoapp/kun/maps"
)
func TestProcessArrayPluck(t *testing.T) {
args := []interface{}{
[]interface{}{"城市", "行业", "计费"},
map[string]interface{}{
"行业": map[string]interface{}{"key": "city", "value": "数量", "items": []map[string]interface{}{{"city": "北京", "数量": 32}, {"city": "上海", "数量": 20}}},
"计费": map[string]interface{}{"key": "city", "value": "计费种类", "items": []map[string]interface{}{{"city": "北京", "计费种类": 6}, {"city": "西安", "计费种类": 3}}},
},
}
process := gou.NewProcess("xiang.helper.ArrayPluck", args...)
response := ProcessArrayPluck(process)
assert.NotNil(t, response)
items, ok := response.([]map[string]interface{})
assert.True(t, ok)
assert.Equal(t, 3, len(items))
for _, item := range items {
maps.Of(item).Has("城市")
maps.Of(item).Has("行业")
maps.Of(item).Has("计费")
}
}
func TestProcessArraySplit(t *testing.T) {
args := []interface{}{
[]map[string]interface{}{
{"name": "阿里云计算有限公司", "short_name": "阿里云"},
{"name": "世纪互联蓝云", "short_name": "上海蓝云"},
},
}
process := gou.NewProcess("xiang.helper.ArraySplit", args...)
response := process.Run()
assert.NotNil(t, response)
res, ok := response.(map[string]interface{})
assert.True(t, ok)
columns, ok := res["columns"].([]string)
assert.True(t, ok)
values, ok := res["values"].([][]interface{})
assert.True(t, ok)
assert.Equal(t, 2, len(columns))
assert.Equal(t, 2, len(values))
for _, value := range values {
assert.Equal(t, 2, len(value))
}
}

View file

@ -139,3 +139,22 @@ func ProcessOf(v map[string]interface{}) Process {
Args: []interface{}{},
}
}
// ProcessEach xiang.helper.Each 循环过程控制
func ProcessEach(process *gou.Process) interface{} {
process.ValidateArgNums(2)
v := process.Args[0]
p := ProcessOf(process.ArgsMap(1))
Range(v, p)
return nil
}
// ProcessFor xiang.helper.For 循环过程控制
func ProcessFor(process *gou.Process) interface{} {
process.ValidateArgNums(3)
from := process.ArgsInt(0)
to := process.ArgsInt(1)
p := ProcessOf(process.ArgsMap(2))
For(from, to, p)
return nil
}