Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/interface/main/captcha/cmd:all-srcs",
"//app/interface/main/captcha/conf:all-srcs",
"//app/interface/main/captcha/dao:all-srcs",
"//app/interface/main/captcha/http:all-srcs",
"//app/interface/main/captcha/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,28 @@
### captcha 图片验证码服务
#### V1.0.6
> 1.修复http层接口返回多json.
> 2.修复map并发锁.
#### V1.0.5
> 1.使用新的verify
> 2.使用Toml2()
#### V1.0.4
> 1.迁移项目至main目录
> 2.修改bind使用方法
#### V1.0.3
> 1.将captcha client 集成于captcha-interface接口返回url
#### V1.0.2
> 1.将/x/v1/captcha/get接口图片encode逻辑转移到service层提前处理
> 2.HTTP初始化时增加请求限速功能
> 3./x/internal/v1/captcha/token、/x/internal/v1/captcha/verify接口增加identify认证
#### V1.0.1
> 1.修改URI /x/internal/v1
#### V1.0.0
> 1.迁移大仓库
> 2.使用新的HTTP框架

View File

@@ -0,0 +1,15 @@
# Owner
liangkai
renwei
renyashun
# Author
liangkai
renyashun
zhangshengchao
# Reviewer
liangkai
renyashun
zhangshengchao
zhapuyu

View File

@@ -0,0 +1,18 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- liangkai
- renwei
- renyashun
- zhangshengchao
labels:
- interface
- interface/main/captcha
- main
options:
no_parent_owners: true
reviewers:
- liangkai
- renyashun
- zhangshengchao
- zhapuyu

View File

@@ -0,0 +1,17 @@
#### captcha
### 项目简介
> 验证码服务
##### 依赖包
> 1.公共包go-common
##### 编译环境
> 请使用golang v1.8.x以上版本编译执行。
##### 编译执行
> 在主目录执行go build。
> 编译后可执行 ./cmd/cmd -conf captcha.toml 使用项目本地配置文件启动服务。
##### 特别说明
> http接口文档可参考

View File

@@ -0,0 +1,44 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = ["captcha-test.toml"],
importpath = "go-common/app/interface/main/captcha/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/captcha/conf:go_default_library",
"//app/interface/main/captcha/http:go_default_library",
"//app/interface/main/captcha/service:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,140 @@
# This is a TOML document. Boom.
version = "1.0.0"
user = "nobody"
pid = "/tmp/captcha.pid"
dir = "./"
perf = "127.0.0.1:6240"
family = "captcha"
[xlog]
dir = "/data/log/captcha/"
[ecode]
domain = "172.16.33.248:6401"
all = "1h"
diff = "10m"
[ecode.clientconfig]
dial = "2000ms"
timeout = "2s"
keepAlive = "10s"
timer = 128
key = "test"
secret = "e6c4c252dc7e3d8a90805eecd7c73396"
[ecode.clientconfig.breaker]
window ="3s"
sleep ="100ms"
bucket = 10
ratio = 0.5
request = 100
[bm]
[bm.outer]
addr = "0.0.0.0:6241"
maxListen = 1000
timeout = "100ms"
[identify]
whiteAccessKey = ""
whiteMid = 0
[identify.app]
key = "f022126a8a365e20"
secret = "b7b86838145d634b487e67b811b8fab2"
[identify.memcache]
name = "go-business/identify"
proto = "tcp"
addr = "172.16.33.54:11211"
active = 5
idle = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "80s"
[identify.host]
auth = "http://passport.bilibili.com"
secret = "http://open.bilibili.com"
[identify.httpClient]
key = "f022126a8a365e20"
secret = "b7b86838145d634b487e67b811b8fab2"
dial = "30ms"
timeout = "100ms"
keepAlive = "60s"
[identify.httpClient.breaker]
window = "10s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify.httpClient.url]
"http://passport.bilibili.co/intranet/auth/tokenInfo" = {timeout = "100ms"}
"http://passport.bilibili.co/intranet/auth/cookieInfo" = {timeout = "100ms"}
"http://open.bilibili.co/api/getsecret" = {timeout = "500ms"}
[rate]
[rate.apps]
"reply" = {limit = 10000.0, burst = 10000}
"live" = {limit = 30000.0, burst = 10000}
[rate.urls]
"/x/internal/v1/captcha/token" = {limit = 30000.0, burst = 10000}
"/x/internal/v1/captcha/verify" = {limit = 30000.0, burst = 10000}
"/x/v1/captcha/get" = {limit = 30000.0, burst = 10000}
[memcache]
proto = "tcp"
addr = "172.16.33.54:11211"
idle = 10
active = 10
dialTimeout = "2s"
readTimeout = "2s"
writeTimeout = "2s"
idleTimeout = "7h"
[captcha]
outerHost = "http://api.bilibili.com"
capacity = 1000
interval = "5m"
disturbLevel = 16
ext = "jpeg"
fonts = ["./fonts/comic.ttf"]
[[captcha.bkgColors]]
r = 255
g = 0
b = 0
a = 255
[[captcha.bkgColors]]
r = 0
g = 0
b = 255
a = 255
[[captcha.bkgColors]]
r = 0
g = 153
b = 0
a = 255
[[captcha.frontColors]]
r = 255
g = 255
b = 255
a = 255
[[business]]
businessID = "default"
lenStart = 4
lenEnd = 4
width = 100
length =50
ttl = "120s"
[[business]]
businessID = "account"
lenStart = 4
lenEnd = 5
width = 100
length =50
ttl = "120s"
[[business]]
businessID = "101"
lenStart = 4
lenEnd = 5
width = 100
length =50
ttl = "5m"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,51 @@
package main
import (
"flag"
"math/rand"
"os"
"os/signal"
"syscall"
"time"
"go-common/app/interface/main/captcha/conf"
"go-common/app/interface/main/captcha/http"
"go-common/app/interface/main/captcha/service"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
"go-common/library/net/trace"
)
func main() {
rand.Seed(time.Now().Unix())
flag.Parse()
if err := conf.Init(); err != nil {
log.Error("conf.Init() error(%v)", err)
panic(err)
}
log.Init(conf.Conf.XLog)
defer log.Close()
log.Info("captcha-service start")
trace.Init(conf.Conf.Tracer)
defer trace.Close()
svr := service.New(conf.Conf)
ecode.Init(conf.Conf.Ecode)
http.Init(conf.Conf, svr)
// signal
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("captcha-service get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
// svr.Close()
log.Info("captcha-service exit")
return
case syscall.SIGHUP:
// TODO reload
default:
return
}
}
}

View File

@@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["conf.go"],
importpath = "go-common/app/interface/main/captcha/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/conf:go_default_library",
"//library/ecode/tip:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/rate:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//library/net/trace:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,101 @@
package conf
import (
"errors"
"flag"
"image/color"
"go-common/library/cache/memcache"
"go-common/library/conf"
ecode "go-common/library/ecode/tip"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/rate"
"go-common/library/net/http/blademaster/middleware/verify"
"go-common/library/net/trace"
"go-common/library/time"
"github.com/BurntSushi/toml"
)
var (
confPath string
// Conf .
Conf = &Config{}
)
// Config captcha service config struct.
type Config struct {
XLog *log.Config
Tracer *trace.Config
Ecode *ecode.Config
BM *HTTPServers
Verify *verify.Config
Rate *rate.Config
Memcache *Memcache
Captcha *Captcha
Business []*Business
}
// Memcache represent mc conf
type Memcache struct {
*memcache.Config
Expire time.Duration
}
// HTTPServers Http Servers
type HTTPServers struct {
Outer *bm.ServerConfig
}
// Business third business confs.
type Business struct {
BusinessID string
LenStart int
LenEnd int
Width int
Length int
TTL time.Duration
}
// Captcha captcha service conf.
type Captcha struct {
OuterHost string
Capacity int
DisturbLevel int // 4 normal, 8 medium, 16 high
Ext string // jpeg
Fonts []string
BkgColors []color.RGBA
FrontColors []color.RGBA
Interval time.Duration
}
func init() {
flag.StringVar(&confPath, "conf", "", "config path")
}
// Init captcha service init.
func Init() (err error) {
if confPath == "" {
return configCenter()
}
_, err = toml.DecodeFile(confPath, &Conf)
return
}
// configCenter connect to config center, get configs.
func configCenter() (err error) {
var (
client *conf.Client
value string
ok bool
)
if client, err = conf.New(); err != nil {
return
}
if value, ok = client.Toml2(); !ok {
return errors.New("load config center error")
}
_, err = toml.Decode(value, &Conf)
return
}

View File

@@ -0,0 +1,52 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"captcha_mc_test.go",
"dao_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/main/captcha/conf:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"captcha_mc.go",
"dao.go",
],
importpath = "go-common/app/interface/main/captcha/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/captcha/conf:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/log:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,65 @@
package dao
import (
"context"
"go-common/library/cache/memcache"
"go-common/library/log"
)
var (
_defaultCode = "@@@@"
)
// AddTokenCache add token redis cache.
func (d *Dao) AddTokenCache(c context.Context, key string, ttl int32) (err error) {
conn := d.memcache.Get(c)
defer conn.Close()
item := &memcache.Item{Key: key, Value: []byte(_defaultCode), Expiration: ttl, Flags: memcache.FlagRAW}
if err = conn.Set(item); err != nil {
log.Error("conn.Set(%s,%v) error(%v)", key, _defaultCode, err)
}
return
}
// UpdateTokenCache update token cache.
func (d *Dao) UpdateTokenCache(c context.Context, token, code string, ttl int32) (err error) {
conn := d.memcache.Get(c)
defer conn.Close()
item := &memcache.Item{Key: token, Value: []byte(code), Expiration: ttl, Flags: memcache.FlagRAW}
if err = conn.Set(item); err != nil {
log.Error("conn.Set(%s,%v) error(%v)", token, code, err)
}
return
}
// CaptchaCache get captcha cache.
func (d *Dao) CaptchaCache(c context.Context, token string) (code string, isInit bool, err error) {
conn := d.memcache.Get(c)
defer conn.Close()
item, err := conn.Get(token)
if err != nil {
if err == memcache.ErrNotFound {
err = nil
} else {
log.Error("conn.Get(GET %s) error(%v)", token, err)
}
return
}
if err = conn.Scan(item, &code); err != nil {
log.Error("conn.Scan(%s) error(%v)", item.Value, err)
return
}
isInit = code == _defaultCode
return
}
// DelCaptchaCache delete captcha cache.
func (d *Dao) DelCaptchaCache(c context.Context, token string) (err error) {
conn := d.memcache.Get(c)
defer conn.Close()
if err = conn.Delete(token); err != nil {
log.Error("conn.Delete(%s) error(%v)", token, err)
}
return
}

View File

@@ -0,0 +1,43 @@
package dao
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
var (
token = "5049a45ffc7c49489c14a7677c4548e2"
ttl = 150
)
func TestAddTokenCache(t *testing.T) {
var (
c = context.TODO()
)
Convey("err should return nil, and ttl not -1", t, func() {
err := d.AddTokenCache(c, token, int32(ttl))
So(err, ShouldBeNil)
})
}
func TestDelCaptchaCache(t *testing.T) {
var (
c = context.TODO()
)
Convey("err should return nil", t, func() {
err := d.DelCaptchaCache(c, token)
So(err, ShouldBeNil)
})
}
func TestCaptchaCache(t *testing.T) {
var (
c = context.TODO()
)
Convey("err should return nil", t, func() {
_, _, err := d.CaptchaCache(c, token)
So(err, ShouldBeNil)
})
}

View File

@@ -0,0 +1,48 @@
package dao
import (
"context"
"time"
"go-common/app/interface/main/captcha/conf"
"go-common/library/cache/memcache"
)
// Dao captcha service Dao.
type Dao struct {
conf *conf.Config
memcache *memcache.Pool
mcExpire int32
}
// New new a captcha dao.
func New(c *conf.Config) (d *Dao) {
d = &Dao{
conf: c,
memcache: memcache.NewPool(c.Memcache.Config),
mcExpire: int32(time.Duration(c.Memcache.Expire) / time.Second),
}
return d
}
// Ping captcha service health check, connection is ok.
func (d *Dao) Ping(c context.Context) (err error) {
// return d.pingRedis(c)
return d.pingMemcache(c)
}
// pingMemcache check Memcache health.
func (d *Dao) pingMemcache(c context.Context) (err error) {
conn := d.memcache.Get(c)
item := memcache.Item{Key: "ping", Value: []byte{1}, Expiration: d.mcExpire}
err = conn.Set(&item)
conn.Close()
return
}
// Close close captcha all connection.
func (d *Dao) Close() {
if d.memcache != nil {
d.memcache.Close()
}
}

View File

@@ -0,0 +1,21 @@
package dao
import (
"flag"
"path/filepath"
"testing"
"go-common/app/interface/main/captcha/conf"
)
var (
d *Dao
)
func TestMain(m *testing.M) {
flag.Parse()
dir, _ := filepath.Abs("../cmd/captcha-test.toml")
flag.Set("conf", dir)
conf.Init()
d = New(conf.Conf)
}

View File

@@ -0,0 +1,41 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"captcha.go",
"http.go",
"render_image.go",
],
importpath = "go-common/app/interface/main/captcha/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/captcha/conf:go_default_library",
"//app/interface/main/captcha/service:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/rate:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,70 @@
package http
import (
"net/http"
bm "go-common/library/net/http/blademaster"
)
// get user get a image.
func get(c *bm.Context) {
var (
err error
v = new(struct {
Token string `form:"token" validate:"required"`
Bid string `form:"bid" validate:"required"`
})
img []byte
)
if err = c.Bind(v); err != nil {
return
}
if img, err = svr.CaptchaImg(c, v.Token, v.Bid); err != nil {
c.JSON(nil, err)
return
}
code := http.StatusOK
c.Render(code, Image{
Body: img,
})
}
// token third business get token.
func token(c *bm.Context) {
var (
err error
v = new(struct {
Bid string `form:"bid" validate:"required"`
})
token, url string
)
if err = c.Bind(v); err != nil {
return
}
if url, token, err = svr.Token(c, v.Bid); err != nil {
c.JSON(nil, err)
return
}
data := make(map[string]interface{}, 1)
data["data"] = map[string]string{
"token": token,
"url": url,
}
c.JSONMap(data, nil)
}
// verify third business verify.
func verify(c *bm.Context) {
var (
err error
v = new(struct {
Token string `form:"token" validate:"required"`
Code string `form:"code" validate:"required"`
})
)
if err = c.Bind(v); err != nil {
return
}
err = svr.VerifyCaptcha(c, v.Token, v.Code)
c.JSON(nil, err)
}

View File

@@ -0,0 +1,53 @@
package http
import (
"net/http"
"go-common/app/interface/main/captcha/conf"
"go-common/app/interface/main/captcha/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/rate"
verifyx "go-common/library/net/http/blademaster/middleware/verify"
)
var (
svr *service.Service
verifySvc *verifyx.Verify
)
// Init captcha http init.
func Init(c *conf.Config, s *service.Service) (err error) {
svr = s
verifySvc = verifyx.New(c.Verify)
rateLimit := rate.New(c.Rate)
engineOuter := bm.DefaultServer(c.BM.Outer)
engineOuter.Use(rateLimit)
outerRouter(engineOuter)
interRouter(engineOuter)
if err := engineOuter.Start(); err != nil {
log.Error("bm.DefaultServer error(%v)", err)
panic(err)
}
return
}
func outerRouter(e *bm.Engine) {
e.Ping(ping)
group := e.Group("/x/v1/captcha")
group.GET("/get", get)
}
func interRouter(e *bm.Engine) {
group := e.Group("/x/internal/v1/captcha")
group.GET("/token", verifySvc.Verify, token)
group.POST("/verify", verifySvc.Verify, verify)
}
func ping(c *bm.Context) {
if svr.Ping(c) != nil {
log.Error("captcha service ping error")
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}

View File

@@ -0,0 +1,45 @@
package http
import (
"net/http"
"github.com/pkg/errors"
)
var (
imageContentType = []string{"image/jpeg"}
_ Render = Image{}
)
// Render http reponse render.
type Render interface {
// Render render it to http response writer.
Render(http.ResponseWriter) error
// WriteContentType write content-type to http response writer.
WriteContentType(w http.ResponseWriter)
}
// Image Image.
type Image struct {
Body []byte
}
// WriteContentType write json ContentType.
func (j Image) WriteContentType(w http.ResponseWriter) {
writeContentType(w, imageContentType)
}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
// Render (JSON) writes data with json ContentType.
func (j Image) Render(w http.ResponseWriter) (err error) {
if _, err = w.Write(j.Body); err != nil {
err = errors.WithStack(err)
}
return
}

View File

@@ -0,0 +1,62 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"captcha_test.go",
"service_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/main/captcha/conf:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"bilinear.go",
"business.go",
"captcha.go",
"draw.go",
"image.go",
"rotate.go",
"service.go",
],
importpath = "go-common/app/interface/main/captcha/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/captcha/conf:go_default_library",
"//app/interface/main/captcha/dao:go_default_library",
"//library/cache:go_default_library",
"//library/ecode:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/golang/freetype:go_default_library",
"//vendor/github.com/golang/freetype/truetype:go_default_library",
"//vendor/github.com/satori/go.uuid:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,126 @@
package service
import (
"image"
"image/color"
"math"
)
var bili = Bilinear{}
// Bilinear Bilinear.
type Bilinear struct{}
// BilinearSrc BilinearSrc.
type BilinearSrc struct {
// Top-left and bottom-right interpolation sources
low, high image.Point
// Fraction of each pixel to take. The 0 suffix indicates
// top/left, and the 1 suffix indicates bottom/right.
frac00, frac01, frac10, frac11 float64
}
// RGBA RGBA.
func (Bilinear) RGBA(src *image.RGBA, x, y float64) color.RGBA {
p := findLinearSrc(src.Bounds(), x, y)
// Array offsets for the surrounding pixels.
off00 := offRGBA(src, p.low.X, p.low.Y)
off01 := offRGBA(src, p.high.X, p.low.Y)
off10 := offRGBA(src, p.low.X, p.high.Y)
off11 := offRGBA(src, p.high.X, p.high.Y)
var fr, fg, fb, fa float64
fr += float64(src.Pix[off00+0]) * p.frac00
fg += float64(src.Pix[off00+1]) * p.frac00
fb += float64(src.Pix[off00+2]) * p.frac00
fa += float64(src.Pix[off00+3]) * p.frac00
fr += float64(src.Pix[off01+0]) * p.frac01
fg += float64(src.Pix[off01+1]) * p.frac01
fb += float64(src.Pix[off01+2]) * p.frac01
fa += float64(src.Pix[off01+3]) * p.frac01
fr += float64(src.Pix[off10+0]) * p.frac10
fg += float64(src.Pix[off10+1]) * p.frac10
fb += float64(src.Pix[off10+2]) * p.frac10
fa += float64(src.Pix[off10+3]) * p.frac10
fr += float64(src.Pix[off11+0]) * p.frac11
fg += float64(src.Pix[off11+1]) * p.frac11
fb += float64(src.Pix[off11+2]) * p.frac11
fa += float64(src.Pix[off11+3]) * p.frac11
var c color.RGBA
c.R = uint8(fr + 0.5)
c.G = uint8(fg + 0.5)
c.B = uint8(fb + 0.5)
c.A = uint8(fa + 0.5)
return c
}
func findLinearSrc(b image.Rectangle, sx, sy float64) BilinearSrc {
maxX := float64(b.Max.X)
maxY := float64(b.Max.Y)
minX := float64(b.Min.X)
minY := float64(b.Min.Y)
lowX := math.Floor(sx - 0.5)
lowY := math.Floor(sy - 0.5)
if lowX < minX {
lowX = minX
}
if lowY < minY {
lowY = minY
}
highX := math.Ceil(sx - 0.5)
highY := math.Ceil(sy - 0.5)
if highX >= maxX {
highX = maxX - 1
}
if highY >= maxY {
highY = maxY - 1
}
// In the variables below, the 0 suffix indicates top/left, and the
// 1 suffix indicates bottom/right.
// Center of each surrounding pixel.
x00 := lowX + 0.5
y00 := lowY + 0.5
x01 := highX + 0.5
y01 := lowY + 0.5
x10 := lowX + 0.5
y10 := highY + 0.5
x11 := highX + 0.5
y11 := highY + 0.5
p := BilinearSrc{
low: image.Pt(int(lowX), int(lowY)),
high: image.Pt(int(highX), int(highY)),
}
// Literally, edge cases. If we are close enough to the edge of
// the image, curtail the interpolation sources.
if lowX == highX && lowY == highY {
p.frac00 = 1.0
} else if sy-minY <= 0.5 && sx-minX <= 0.5 {
p.frac00 = 1.0
} else if maxY-sy <= 0.5 && maxX-sx <= 0.5 {
p.frac11 = 1.0
} else if sy-minY <= 0.5 || lowY == highY {
p.frac00 = x01 - sx
p.frac01 = sx - x00
} else if sx-minX <= 0.5 || lowX == highX {
p.frac00 = y10 - sy
p.frac10 = sy - y00
} else if maxY-sy <= 0.5 {
p.frac10 = x11 - sx
p.frac11 = sx - x10
} else if maxX-sx <= 0.5 {
p.frac01 = y11 - sy
p.frac11 = sy - y01
} else {
p.frac00 = (x01 - sx) * (y10 - sy)
p.frac01 = (sx - x00) * (y11 - sy)
p.frac10 = (x11 - sx) * (sy - y00)
p.frac11 = (sx - x10) * (sy - y01)
}
return p
}

View File

@@ -0,0 +1,29 @@
package service
import (
"time"
"go-common/app/interface/main/captcha/conf"
xtime "go-common/library/time"
)
var (
_defaultBusiness = &conf.Business{
BusinessID: "default",
LenStart: 4,
LenEnd: 4,
Width: 100,
Length: 50,
TTL: xtime.Duration(300 * time.Second),
}
)
// LookUp look up business services.
func (s *Service) LookUp(bid string) (business *conf.Business) {
for _, b := range s.conf.Business {
if b.BusinessID == bid {
return b
}
}
return _defaultBusiness
}

View File

@@ -0,0 +1,112 @@
package service
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"image/jpeg"
"image/png"
"math/rand"
"strings"
"sync"
"time"
"go-common/library/ecode"
uuid "github.com/satori/go.uuid"
)
const (
_captchaURL = "%s/x/v1/captcha/get?bid=%s&token=%s"
)
// Token use bid, get a token.
func (s *Service) Token(c context.Context, bid string) (url string, token string, err error) {
token = hex.EncodeToString(uuid.NewV4().Bytes())
business := s.LookUp(bid)
if err = s.dao.AddTokenCache(c, token, int32(time.Duration(business.TTL)/time.Second)); err != nil {
return
}
url = fmt.Sprintf(_captchaURL, s.conf.Captcha.OuterHost, bid, token)
return
}
// CaptchaImg get a captcha by token,bid.
func (s *Service) CaptchaImg(c context.Context, token, bid string) (img []byte, err error) {
code, img, ttl := s.randomCaptcha(bid)
realCode, _, err := s.dao.CaptchaCache(c, token)
if err != nil {
return
}
if realCode == "" {
err = ecode.CaptchaTokenExpired
return
}
err = s.dao.UpdateTokenCache(c, token, code, ttl)
return
}
// VerifyCaptcha verify captcha by token and code.
func (s *Service) VerifyCaptcha(c context.Context, token, code string) (err error) {
var (
realCode string
isInit bool
)
if realCode, isInit, err = s.dao.CaptchaCache(c, token); err != nil {
return
}
if realCode == "" {
err = ecode.CaptchaCodeNotFound
return
}
if isInit {
err = ecode.CaptchaNotCreate
return
}
if ok := strings.ToLower(realCode) == strings.ToLower(code); ok {
s.cacheCh.Save(func() {
s.dao.DelCaptchaCache(context.Background(), token)
})
} else {
err = ecode.CaptchaErr
}
return
}
func (s *Service) initGenerater(waiter *sync.WaitGroup, bid string, lenStart, lenEnd, width, length int) {
s.generater(bid, lenStart, lenEnd, width, length)
waiter.Done()
}
func (s *Service) generater(bid string, lenStart, lenEnd, width, length int) {
images := make(map[string][]byte, s.conf.Captcha.Capacity)
codes := make([]string, 0, s.conf.Captcha.Capacity)
for i := 0; i < s.conf.Captcha.Capacity; i++ {
img, code := s.captcha.createImage(lenStart, lenEnd, width, length, TypeALL)
var b bytes.Buffer
switch s.conf.Captcha.Ext {
case "png":
png.Encode(&b, img)
case "jpeg":
jpeg.Encode(&b, img, &jpeg.Options{Quality: 100})
default:
jpeg.Encode(&b, img, &jpeg.Options{Quality: 100})
}
images[code] = b.Bytes()
codes = append(codes, code)
}
s.lock.Lock()
s.mImage[bid] = images
s.mCode[bid] = codes
s.lock.Unlock()
}
func (s *Service) randomCaptcha(bid string) (code string, img []byte, ttl int32) {
business := s.LookUp(bid)
ttl = int32(time.Duration(business.TTL) / time.Second)
rnd := rand.Intn(s.conf.Captcha.Capacity)
code = s.mCode[business.BusinessID][rnd]
img = s.mImage[business.BusinessID][code]
return
}

View File

@@ -0,0 +1,32 @@
package service
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
var (
bid = "account"
token = "5049a45ffc7c49489c14a7677c4548e2"
)
func TestToken(t *testing.T) {
var (
c = context.Background()
)
Convey("err should return nil", t, func() {
_, t, err := svr.Token(c, bid)
So(err, ShouldBeNil)
So(t, ShouldNotBeNil)
})
Convey("err should return nil", t, func() {
err := svr.VerifyCaptcha(c, token, "test")
So(err, ShouldNotBeNil)
})
Convey("err should return nil", t, func() {
business := svr.LookUp(bid)
So(business, ShouldNotBeEmpty)
})
}

View File

@@ -0,0 +1,247 @@
package service
import (
"image"
"image/color"
"image/draw"
"io/ioutil"
"math"
"math/rand"
"time"
"go-common/app/interface/main/captcha/conf"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
)
// CONST VALUE.
const (
NORMAL = int(4)
MEDIUM = int(8)
HIGH = int(16)
MinLenStart = int(4)
MinWidth = int(48)
MinLength = int(20)
Length48 = int(48)
TypeNone = int(0)
TypeLOWER = int(1)
TypeUPPER = int(2)
TypeALL = int(3)
)
var fontKinds = [][]int{[]int{10, 48}, []int{26, 97}, []int{26, 65}}
func sign(x int) int {
if x > 0 {
return 1
}
return -1
}
// NewCaptcha new a captcha.
func newCaptcha(c *conf.Captcha) *Captcha {
captcha := &Captcha{
disturbLevel: NORMAL,
}
captcha.frontColors = []color.Color{color.Black}
captcha.bkgColors = []color.Color{color.White}
captcha.setFont(c.Fonts...)
colors := []color.Color{}
for _, v := range c.BkgColors {
colors = append(colors, v)
}
captcha.setBkgColor(colors...)
colors = []color.Color{}
for _, v := range c.FrontColors {
colors = append(colors, v)
}
captcha.setFontColor(colors...)
captcha.setDisturbance(c.DisturbLevel)
return captcha
}
// addFont add font.
func (c *Captcha) addFont(path string) error {
fontdata, erro := ioutil.ReadFile(path)
if erro != nil {
return erro
}
font, erro := freetype.ParseFont(fontdata)
if erro != nil {
return erro
}
if c.fonts == nil {
c.fonts = []*truetype.Font{}
}
c.fonts = append(c.fonts, font)
return nil
}
// setFont set font.
func (c *Captcha) setFont(paths ...string) (err error) {
for _, v := range paths {
if err = c.addFont(v); err != nil {
return err
}
}
return nil
}
// setBkgColor set backgroud color.
func (c *Captcha) setBkgColor(colors ...color.Color) {
if len(colors) > 0 {
c.bkgColors = c.bkgColors[:0]
c.bkgColors = append(c.bkgColors, colors...)
}
}
func (c *Captcha) randFont() *truetype.Font {
return c.fonts[rand.Intn(len(c.fonts))]
}
// setBkgsetFontColorColor set font color.
func (c *Captcha) setFontColor(colors ...color.Color) {
if len(colors) > 0 {
c.frontColors = c.frontColors[:0]
c.frontColors = append(c.frontColors, colors...)
}
}
// setDisturbance set disturbance.
func (c *Captcha) setDisturbance(d int) {
if d > 0 {
c.disturbLevel = d
}
}
func (c *Captcha) createImage(lenStart, lenEnd, width, length, t int) (image *Image, str string) {
num := MinLenStart
if lenStart < MinLenStart {
lenStart = MinLenStart
}
if lenEnd > lenStart {
// rand.Seed(time.Now().UnixNano())
num = rand.Intn(lenEnd-lenStart+1) + lenStart
}
str = c.randStr(num, t)
return c.createCustom(str, width, length), str
}
func (c *Captcha) createCustom(str string, width, length int) *Image {
// boundary check
if len(str) == 0 {
str = "bilibili"
}
if width < MinWidth {
width = MinWidth
}
if length < MinLength {
length = MinLength
}
dst := newImage(width, length)
c.drawBkg(dst)
c.drawNoises(dst)
c.drawString(dst, str, width, length)
return dst
}
// randStr ascII random
// 48~57 -> 0~9 number
// 65~90 -> A~Z uppercase
// 98~122 -> a~z lowcase
func (c *Captcha) randStr(size, kind int) string {
ikind, result := kind, make([]byte, size)
isAll := kind > TypeUPPER || kind < TypeNone
// rand.Seed(time.Now().UnixNano())
for i := 0; i < size; i++ {
if isAll {
ikind = rand.Intn(TypeALL)
}
scope, base := fontKinds[ikind][0], fontKinds[ikind][1]
result[i] = uint8(base + rand.Intn(scope))
}
return string(result)
}
func (c *Captcha) drawBkg(img *Image) {
ra := rand.New(rand.NewSource(time.Now().UnixNano()))
//填充主背景色
bgcolorindex := ra.Intn(len(c.bkgColors))
bkg := image.NewUniform(c.bkgColors[bgcolorindex])
img.fillBkg(bkg)
}
func (c *Captcha) drawNoises(img *Image) {
ra := rand.New(rand.NewSource(time.Now().UnixNano()))
//// 待绘制图片的尺寸
point := img.Bounds().Size()
disturbLevel := c.disturbLevel
// 绘制干扰斑点
for i := 0; i < disturbLevel; i++ {
x := ra.Intn(point.X)
y := ra.Intn(point.Y)
radius := ra.Intn(point.Y/20) + 1
colorindex := ra.Intn(len(c.frontColors))
img.drawCircle(x, y, radius, i%4 != 0, c.frontColors[colorindex])
}
// 绘制干扰线
for i := 0; i < disturbLevel; i++ {
x := ra.Intn(point.X)
y := ra.Intn(point.Y)
o := int(math.Pow(-1, float64(i)))
w := ra.Intn(point.Y) * o
h := ra.Intn(point.Y/10) * o
colorindex := ra.Intn(len(c.frontColors))
img.drawLine(x, y, x+w, y+h, c.frontColors[colorindex])
colorindex++
}
}
// 绘制文字
func (c *Captcha) drawString(img *Image, str string, width, length int) {
if c.fonts == nil {
panic("没有设置任何字体")
}
tmp := newImage(width, length)
// 文字大小为图片高度的 0.6
fsize := int(float64(length) * 0.6)
// 用于生成随机角度
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// 文字之间的距离
// 左右各留文字的1/4大小为内部边距
padding := fsize / 4
gap := (width - padding*2) / (len(str))
// 逐个绘制文字到图片上
for i, char := range str {
// 创建单个文字图片
// 以文字为尺寸创建正方形的图形
str := newImage(fsize, fsize)
// str.FillBkg(image.NewUniform(color.Black))
// 随机取一个前景色
colorindex := r.Intn(len(c.frontColors))
//随机取一个字体
font := c.randFont()
str.drawString(font, c.frontColors[colorindex], string(char), float64(fsize))
// 转换角度后的文字图形
rs := str.rotate(float64(r.Intn(40) - 20))
// 计算文字位置
s := rs.Bounds().Size()
left := i*gap + padding
top := (length - s.Y) / 2
// 绘制到图片上
draw.Draw(tmp, image.Rect(left, top, left+s.X, top+s.Y), rs, image.ZP, draw.Over)
}
if length >= Length48 {
// 高度大于48添加波纹 小于48波纹影响用户识别
tmp.distortTo(float64(fsize)/10, 200.0)
}
draw.Draw(img, tmp.Bounds(), tmp, image.ZP, draw.Over)
}

View File

@@ -0,0 +1,132 @@
package service
import (
"image"
"image/color"
"image/draw"
"math"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
)
// Image Image.
type Image struct {
*image.RGBA
}
// newImage create a new image
func newImage(width, length int) *Image {
img := &Image{image.NewRGBA(image.Rect(0, 0, width, length))}
return img
}
func (img *Image) fillBkg(c image.Image) {
draw.Draw(img, img.Bounds(), c, image.ZP, draw.Over)
}
// drawCircle draw circle.
func (img *Image) drawCircle(cx, cy, radius int, isFillColor bool, color color.Color) {
point := img.Bounds().Size()
// 如果圆在图片可见区域外,直接退出
if cx+radius < 0 || cx-radius >= point.X || cy+radius < 0 || cy-radius >= point.Y {
return
}
x, y, d := 0, radius, 3-2*radius
for x <= y {
if isFillColor {
for yi := x; yi <= y; yi++ {
img.drawCircle8(cx, cy, x, yi, color)
}
} else {
img.drawCircle8(cx, cy, x, y, color)
}
if d < 0 {
d = d + 4*x + 6
} else {
d = d + 4*(x-y) + 10
y--
}
x++
}
}
// drawLine .
// Bresenham算法(https://zh.wikipedia.org/zh-cn/布雷森漢姆直線演算法).
// startX,startY 起点 endX,endY终点.
func (img *Image) drawLine(startX, startY, endX, endY int, color color.Color) {
dx, dy, flag := int(math.Abs(float64(startY-startX))), int(math.Abs(float64(endY-startY))), false
if dy > dx {
flag = true
startX, startY = startY, startX
endX, endY = endY, endX
dx, dy = dy, dx
}
ix, iy := sign(endX-startX), sign(endY-startY)
n2dy := dy * 2
n2dydx := (dy - dx) * 2
d := n2dy - dx
for startX != endX {
if d < 0 {
d += n2dy
} else {
startY += iy
d += n2dydx
}
if flag {
img.Set(startY, startX, color)
} else {
img.Set(startX, startY, color)
}
startX += ix
}
}
func (img *Image) drawCircle8(xc, yc, x, y int, color color.Color) {
img.Set(xc+x, yc+y, color)
img.Set(xc-x, yc+y, color)
img.Set(xc+x, yc-y, color)
img.Set(xc-x, yc-y, color)
img.Set(xc+y, yc+x, color)
img.Set(xc-y, yc+x, color)
img.Set(xc+y, yc-x, color)
img.Set(xc-y, yc-x, color)
}
// drawString image draw string.
func (img *Image) drawString(font *truetype.Font, color color.Color, str string, fontsize float64) {
ctx := freetype.NewContext()
// default 72dpi
ctx.SetDst(img)
ctx.SetClip(img.Bounds())
ctx.SetSrc(image.NewUniform(color))
ctx.SetFontSize(fontsize)
ctx.SetFont(font)
// 写入文字的位置
pt := freetype.Pt(0, int(-fontsize/6)+ctx.PointToFixed(fontsize).Ceil())
ctx.DrawString(str, pt)
}
// 水波纹, amplude=振幅, period=周期
// copy from https://github.com/dchest/captcha/blob/master/image.go
func (img *Image) distortTo(amplude float64, period float64) {
w := img.Bounds().Max.X
h := img.Bounds().Max.Y
oldm := img.RGBA
dx := 1.4 * math.Pi / period
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
xo := amplude * math.Sin(float64(y)*dx)
yo := amplude * math.Cos(float64(x)*dx)
rgba := oldm.RGBAAt(x+int(xo), y+int(yo))
if rgba.A > 0 {
oldm.SetRGBA(x, y, rgba)
}
}
}
}
// Rotate 旋转
func (img *Image) rotate(angle float64) image.Image {
return new(rotate).rotate(angle, img.RGBA).transformRGBA()
}

View File

@@ -0,0 +1,90 @@
package service
import (
"image"
"math"
)
type rotate struct {
dx float64
dy float64
sin float64
cos float64
neww float64
newh float64
src *image.RGBA
}
func (r *rotate) rotate(angle float64, src *image.RGBA) *rotate {
r.src = src
srsize := src.Bounds().Size()
width, height := srsize.X, srsize.Y
// 源图四个角的坐标(以图像中心为坐标系原点)
// 左下角,右下角,左上角,右上角
srcwp, srchp := float64(width)*0.5, float64(height)*0.5
srcx1, srcy1 := -srcwp, srchp
srcx2, srcy2 := srcwp, srchp
srcx3, srcy3 := -srcwp, -srchp
srcx4, srcy4 := srcwp, -srchp
r.sin, r.cos = math.Sincos(radian(angle))
// 旋转后的四角坐标
desx1, desy1 := r.cos*srcx1+r.sin*srcy1, -r.sin*srcx1+r.cos*srcy1
desx2, desy2 := r.cos*srcx2+r.sin*srcy2, -r.sin*srcx2+r.cos*srcy2
desx3, desy3 := r.cos*srcx3+r.sin*srcy3, -r.sin*srcx3+r.cos*srcy3
desx4, desy4 := r.cos*srcx4+r.sin*srcy4, -r.sin*srcx4+r.cos*srcy4
// 新的高度很宽度
r.neww = math.Max(math.Abs(desx4-desx1), math.Abs(desx3-desx2)) + 0.5
r.newh = math.Max(math.Abs(desy4-desy1), math.Abs(desy3-desy2)) + 0.5
r.dx = -0.5*r.neww*r.cos - 0.5*r.newh*r.sin + srcwp
r.dy = 0.5*r.neww*r.sin - 0.5*r.newh*r.cos + srchp
return r
}
func radian(angle float64) float64 {
return angle * math.Pi / 180.0
}
func (r *rotate) transformRGBA() image.Image {
srcb := r.src.Bounds()
b := image.Rect(0, 0, int(r.neww), int(r.newh))
dst := image.NewRGBA(b)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
sx, sy := r.pt(x, y)
if inBounds(srcb, sx, sy) {
// 消除锯齿填色
c := bili.RGBA(r.src, sx, sy)
off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
dst.Pix[off+0] = c.R
dst.Pix[off+1] = c.G
dst.Pix[off+2] = c.B
dst.Pix[off+3] = c.A
}
}
}
return dst
}
func (r *rotate) pt(x, y int) (float64, float64) {
return float64(-y)*r.sin + float64(x)*r.cos + r.dy,
float64(y)*r.cos + float64(x)*r.sin + r.dx
}
func inBounds(b image.Rectangle, x, y float64) bool {
if x < float64(b.Min.X) || x >= float64(b.Max.X) {
return false
}
if y < float64(b.Min.Y) || y >= float64(b.Max.Y) {
return false
}
return true
}
func offRGBA(src *image.RGBA, x, y int) int {
return (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4
}

View File

@@ -0,0 +1,79 @@
package service
import (
"context"
"image/color"
"sync"
"time"
"go-common/app/interface/main/captcha/conf"
"go-common/app/interface/main/captcha/dao"
"go-common/library/cache"
"go-common/library/ecode"
"github.com/golang/freetype/truetype"
)
// Captcha captcha.
type Captcha struct {
frontColors []color.Color
bkgColors []color.Color
disturbLevel int
fonts []*truetype.Font
}
// Service captcha service.
type Service struct {
conf *conf.Config
dao *dao.Dao
captcha *Captcha
cacheCh *cache.Cache
// captcha mem.
init bool
lock sync.RWMutex
mCode map[string][]string
mImage map[string]map[string][]byte
}
// New new a service.
func New(c *conf.Config) (s *Service) {
s = &Service{
conf: c,
dao: dao.New(c),
captcha: newCaptcha(c.Captcha),
cacheCh: cache.New(1, 1024),
mCode: make(map[string][]string, len(c.Business)),
mImage: make(map[string]map[string][]byte, len(c.Business)),
}
go s.generaterProc()
return s
}
// Close close all dao.
func (s *Service) Close() {
s.dao.Close()
}
// Ping ping dao.
func (s *Service) Ping(c context.Context) error {
if !s.init {
return ecode.CaptchaNotCreate
}
return s.dao.Ping(c)
}
func (s *Service) generaterProc() {
waiter := &sync.WaitGroup{}
for _, b := range s.conf.Business {
waiter.Add(1)
go s.initGenerater(waiter, b.BusinessID, b.LenStart, b.LenEnd, b.Width, b.Length)
}
waiter.Wait()
s.init = true
for {
for _, b := range s.conf.Business {
go s.generater(b.BusinessID, b.LenStart, b.LenEnd, b.Width, b.Length)
}
time.Sleep(time.Duration(s.conf.Captcha.Interval))
}
}

View File

@@ -0,0 +1,19 @@
package service
import (
"flag"
"path/filepath"
"testing"
"go-common/app/interface/main/captcha/conf"
)
var svr *Service
func TestMain(m *testing.M) {
flag.Parse()
dir, _ := filepath.Abs("../cmd/captcha-test.toml")
flag.Set("conf", dir)
conf.Init()
svr = New(conf.Conf)
}