├── .gitignore ├── LICENSE ├── README.md ├── doc ├── 1.gif └── design.md ├── example └── puzzle_captcha │ └── main.go ├── go.mod ├── go.sum ├── images └── puzzle_captcha │ ├── backgroud │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ └── 6.png │ └── block │ ├── 1.png.1 │ ├── 2.png.l │ ├── 3.png.l │ ├── 4.png.l │ ├── 5.png │ └── 6.png ├── puzzle_captcha ├── captcha.go ├── check.go ├── image.go ├── image_cache.go ├── rand.go └── types.go └── tools └── imagedebug └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | test_data -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go captcha 2 | 3 | 受[AJ-Captcha](https://gitee.com/anji-plus/captcha)启发,用golang实现的滑动验证码。 4 | 5 | 6 | ## 运行 7 | 8 | ```bash 9 | $ go mod tidy 10 | $ go run example/puzzle_captcha/main.go 11 | 12 | ``` 13 | 14 | ## 前端 15 | 16 | [前端代码仓库](https://github.com/widaT/gocatcha-ui) 17 | 18 | 运行前端 19 | 20 | ```bash 21 | $ git clone git@github.com:widaT/gocatcha-ui.git 22 | $ cd gocatcha-ui 23 | $ npm i 24 | $ npm run dev 25 | ``` 26 | 27 | ## 运行截图 28 | 29 | ![](./doc/1.gif) 30 | 31 | 32 | ## 感谢 33 | 34 | 本程序算法受[AJ-Captcha](https://gitee.com/anji-plus/captcha)启发,本程序的前端UI则来着[AJ-Captcha](https://gitee.com/anji-plus/captcha)的vue代码,感谢`AJ-Captcha`的开源。 -------------------------------------------------------------------------------- /doc/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/doc/1.gif -------------------------------------------------------------------------------- /doc/design.md: -------------------------------------------------------------------------------- 1 | # Go-Captcha 2 | 3 | 滑动验证码,这几年非常流行,但是很少有公司自己代码实现,大都是几大公司提供的api。[AJ-Captcha](https://gitee.com/anji-plus/captcha)是java为数不多的开源实现。 4 | 5 | 本人参考`AJ-Captcha`,用golang重新实现滑动验证码`go-captcha`,并将他开源。 6 | - [github](https://github.com/widaT/go-captcha) 7 | - [gitee](https://gitee.com/wida/go-captcha) 8 | 9 | 10 | ## 滑动验证码原理 11 | 12 | - 精心选择一定宽高固定的底图 13 | - 设计滑块图,滑块需要png格式,一般高度和底图一样,除了滑块图片其他的像素为全透明 14 | - 随机选择底图和滑块图 15 | - 随机出抠图X轴位置 16 | - 从底图X位置扣出以滑块图为模板不透明的像素,生成新的滑块。 17 | 18 | ## 为什么用go造轮子 19 | 20 | - AJ-Captcha 使用java实现,项目依赖spring boot,相对项目比较庞大复杂。 21 | - 使用go重写,可以顺利融合go生态 22 | - 优化一些算法,优化依赖,核心算法不依赖第三方。 23 | 24 | 25 | ## 核心实现 26 | 27 | ```go 28 | // cutOut 抠图 29 | func cutOut(bgImage, bkImage, newBkImage *ImageBuf, x int) { 30 | var values [9]color.RGBA64 31 | bkWidth := bkImage.getWidth() 32 | bkHeight := bkImage.getHeight() 33 | for i := 0; i < bkWidth; i++ { 34 | for j := 0; j < bkHeight; j++ { 35 | pixel := bkImage.getRGBA(i, j) 36 | // 滑块图片非透明像素点,从背景图偏移x 像素拷贝到新图层 37 | if pixel.A > 0 { 38 | newBkImage.setRGBA(i, j, bgImage.getRGBA(x+i, j)) 39 | readNeighborPixel(bgImage, x+i, j, &values) 40 | bgImage.setRGBA(x+i, j, gaussianBlur(&values)) 41 | } 42 | 43 | if i == (bkWidth-1) || j == (bkHeight-1) { 44 | continue 45 | } 46 | rightPixel := bkImage.getRGBA(i+1, j) 47 | bottomPixel := bkImage.getRGBA(i, j+1) 48 | // 用白色给底图和新图层描边 49 | if (pixel.A > 0 && rightPixel.A == 0) || 50 | (pixel.A == 0 && rightPixel.A > 0) || 51 | (pixel.A > 0 && bottomPixel.A == 0) || 52 | (pixel.A == 0 && bottomPixel.A > 0) { 53 | white := color.White 54 | newBkImage.setRGBA(i, j, white) 55 | bgImage.setRGBA(x+i, j, white) 56 | } 57 | } 58 | } 59 | } 60 | 61 | //readNeighborPixel 读取邻近9个点像素,后面最类似高斯模糊计算 62 | //(并非严格的高斯模糊,高斯模糊算法效率太低,本例不需要严格的高斯模糊算法) 63 | // |2|3|4| 64 | // |5|1|6| 65 | // |7|8|9| 66 | // 中心点为1 67 | func readNeighborPixel(img *ImageBuf, x, y int, pixels *[9]color.RGBA64) { 68 | xStart := x - 1 69 | yStart := y - 1 70 | current := 0 71 | for i := xStart; i < 3+xStart; i++ { 72 | for j := yStart; j < 3+yStart; j++ { 73 | tx := i 74 | if tx < 0 { 75 | tx = -tx 76 | } else if tx >= img.getWidth() { 77 | tx = x 78 | } 79 | ty := j 80 | if ty < 0 { 81 | ty = -ty 82 | } else if ty >= img.getHeight() { 83 | ty = y 84 | } 85 | pixels[current] = img.getRGBA(tx, ty) 86 | current++ 87 | } 88 | } 89 | } 90 | 91 | // gaussianBlur 类高斯模糊算法 92 | func gaussianBlur(values *[9]color.RGBA64) color.RGBA64 { 93 | //这边需要 uint32 防止多个uint16相加后溢出 94 | var r uint32 95 | var g uint32 96 | var b uint32 97 | var a uint32 98 | for i := 0; i < len(values); i++ { 99 | if i == 4 { //去掉中间原像素点 100 | continue 101 | } 102 | x := values[i] 103 | r += uint32(x.R) 104 | g += uint32(x.G) 105 | b += uint32(x.B) 106 | a += uint32(x.A) 107 | } 108 | return color.RGBA64{ 109 | uint16(r / 8), 110 | uint16(g / 8), 111 | uint16(b / 8), 112 | uint16(a / 8)} 113 | } 114 | ``` 115 | 116 | ## 看效果 117 | 118 | ![](./1.gif) 119 | 120 | 121 | ## Thanks 122 | 123 | - 欢迎大家使用,并提供反馈。 124 | - 同时感谢[AJ-Captcha](https://gitee.com/anji-plus/captcha)的开源。 -------------------------------------------------------------------------------- /example/puzzle_captcha/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | uuid "github.com/satori/go.uuid" 12 | captcha "github.com/widaT/go-captcha/puzzle_captcha" 13 | ) 14 | 15 | type RespJson struct { 16 | RepCode string `json:"repCode"` 17 | RepData interface{} `json:"repData"` 18 | RepMsg string `json:"repMsg"` 19 | } 20 | 21 | type Data struct { 22 | OrgImg string `json:"originalImageBase64"` 23 | BlkImg string `json:"jigsawImageBase64"` 24 | Token string `json:"token"` 25 | SecretKey string `json:"secretKey"` 26 | } 27 | 28 | type Item struct { 29 | Point *captcha.Point 30 | Expired time.Time 31 | } 32 | 33 | type CheckParams struct { 34 | Point *captcha.Point `json:"point"` 35 | Token string `json:"token"` 36 | } 37 | 38 | type VerificationParams struct { 39 | Verification string `json:"verification"` 40 | } 41 | 42 | var ( 43 | lock sync.Mutex 44 | cache = make(map[string]*Item) //实际项目组应该放到redis 45 | ) 46 | 47 | func cos(handle func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { 48 | a := func(rw http.ResponseWriter, req *http.Request) { 49 | rw.Header().Set("Access-Control-Allow-Origin", "*") // 允许访问所有域,可以换成具体url,注意仅具体url才能带cookie信息 50 | rw.Header().Add("Access-Control-Allow-Credentials", "true") //设置为true,允许ajax异步请求带cookie信息 51 | rw.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") //允许请求方法 52 | rw.Header().Set("Access-Control-Allow-Headers", "Content-Type,access-control-allow-origin, access-control-allow-headers,x-requested-with") 53 | rw.Header().Set("content-type", "application/json;charset=UTF-8") //返回数据格式是json 54 | if req.Method == "OPTIONS" { 55 | rw.WriteHeader(http.StatusNoContent) 56 | return 57 | } 58 | handle(rw, req) 59 | } 60 | return a 61 | } 62 | 63 | func main() { 64 | 65 | captcha.LoadBackgroudImages("./images/puzzle_captcha/backgroud") 66 | captcha.LoadBlockImages("./images/puzzle_captcha/block") 67 | 68 | http.HandleFunc("/captcha/get", cos(func(rw http.ResponseWriter, req *http.Request) { 69 | ret, err := captcha.Run() 70 | if err != nil { 71 | return 72 | } 73 | data := &Data{ 74 | BlkImg: ret.BlockImg, 75 | OrgImg: ret.BackgroudImg, 76 | Token: uuid.NewV4().String(), 77 | } 78 | 79 | lock.Lock() 80 | cache[data.Token] = &Item{ 81 | Point: ret.Point, 82 | Expired: time.Now().Add(3 * time.Second), 83 | } 84 | lock.Unlock() 85 | 86 | resp := &RespJson{ 87 | RepCode: "0000", 88 | RepData: data, 89 | } 90 | buf, err := json.Marshal(resp) 91 | if err != nil { 92 | return 93 | } 94 | rw.Write(buf) 95 | })) 96 | 97 | http.HandleFunc("/captcha/check", cos(func(rw http.ResponseWriter, req *http.Request) { 98 | reqdata := CheckParams{} 99 | resp := &RespJson{ 100 | RepCode: "0000", 101 | RepData: nil, 102 | } 103 | err := json.NewDecoder(req.Body).Decode(&reqdata) 104 | if err != nil { 105 | fmt.Println(err) 106 | resp = &RespJson{ 107 | RepCode: "0007", 108 | RepMsg: "参数错误", 109 | } 110 | goto end 111 | } 112 | 113 | lock.Lock() 114 | defer lock.Unlock() 115 | if i, found := cache[reqdata.Token]; found { 116 | if i.Expired.Before(time.Now()) { 117 | resp.RepCode = "0005" 118 | resp.RepMsg = "过期了" 119 | } else { 120 | delete(cache, reqdata.Token) //删除缓存 121 | if err := captcha.Check(reqdata.Point, i.Point); err != nil { 122 | resp.RepCode = "0004" 123 | resp.RepMsg = "位置不正确" 124 | } else { 125 | //二次认证数据 入缓存 126 | verificationKey := fmt.Sprintf("second:%s", 127 | md5Str(fmt.Sprintf("%s--- %d---%d", reqdata.Token, reqdata.Point.X, reqdata.Point.Y))) 128 | //第二次判断只要判断key在不在缓存里头 129 | cache[verificationKey] = &Item{ 130 | Expired: time.Now().Add(3 * time.Second), 131 | } 132 | resp.RepData = VerificationParams{Verification: verificationKey} 133 | } 134 | } 135 | } else { 136 | resp.RepCode = "0001" 137 | resp.RepMsg = "token不存在" 138 | } 139 | 140 | end: 141 | buf, _ := json.Marshal(&resp) 142 | rw.Write(buf) 143 | })) 144 | 145 | //二次验证 146 | http.HandleFunc("/captcha/verification", cos(func(rw http.ResponseWriter, req *http.Request) { 147 | v := VerificationParams{} 148 | json.NewDecoder(req.Body).Decode(&v) 149 | resp := &RespJson{ 150 | RepCode: "0000", 151 | RepData: nil, 152 | } 153 | lock.Lock() 154 | defer lock.Unlock() 155 | if i, found := cache[v.Verification]; found { 156 | if i.Expired.Before(time.Now()) { 157 | resp.RepCode = "0005" 158 | resp.RepMsg = "过期了" 159 | } 160 | } else { 161 | resp.RepCode = "0001" 162 | resp.RepMsg = "token不存在" 163 | } 164 | buf, _ := json.Marshal(&resp) 165 | rw.Write(buf) 166 | })) 167 | 168 | http.Handle("/", http.FileServer(http.Dir("./dist"))) 169 | panic(http.ListenAndServe(":8081", nil)) 170 | } 171 | func md5Str(str string) string { 172 | data := []byte(str) 173 | has := md5.Sum(data) 174 | md5str := fmt.Sprintf("%x", has) 175 | return md5str 176 | } 177 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/widaT/go-captcha 2 | 3 | go 1.17 4 | 5 | require github.com/satori/go.uuid v1.2.0 6 | 7 | require gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 2 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 3 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 4 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 5 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 6 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 7 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 8 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 9 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 10 | -------------------------------------------------------------------------------- /images/puzzle_captcha/backgroud/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/backgroud/1.png -------------------------------------------------------------------------------- /images/puzzle_captcha/backgroud/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/backgroud/2.png -------------------------------------------------------------------------------- /images/puzzle_captcha/backgroud/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/backgroud/3.png -------------------------------------------------------------------------------- /images/puzzle_captcha/backgroud/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/backgroud/4.png -------------------------------------------------------------------------------- /images/puzzle_captcha/backgroud/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/backgroud/5.png -------------------------------------------------------------------------------- /images/puzzle_captcha/backgroud/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/backgroud/6.png -------------------------------------------------------------------------------- /images/puzzle_captcha/block/1.png.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/block/1.png.1 -------------------------------------------------------------------------------- /images/puzzle_captcha/block/2.png.l: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/block/2.png.l -------------------------------------------------------------------------------- /images/puzzle_captcha/block/3.png.l: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/block/3.png.l -------------------------------------------------------------------------------- /images/puzzle_captcha/block/4.png.l: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/block/4.png.l -------------------------------------------------------------------------------- /images/puzzle_captcha/block/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/block/5.png -------------------------------------------------------------------------------- /images/puzzle_captcha/block/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widaT/go-captcha/d336f5cf7dd361c24b34cfa3ad0bf36ca5231f12/images/puzzle_captcha/block/6.png -------------------------------------------------------------------------------- /puzzle_captcha/captcha.go: -------------------------------------------------------------------------------- 1 | package captcha 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/png" 10 | ) 11 | 12 | // 干扰图片 13 | const interferenceOptions = 1 14 | 15 | func Run() (*CutoutRet, error) { 16 | bgImage, err := randBackgroudImage() 17 | if err != nil { 18 | return nil, err 19 | } 20 | bkImage, itImage, err := randBlockImage() 21 | if err != nil { 22 | return nil, err 23 | } 24 | return run(bgImage, bkImage, itImage) 25 | } 26 | 27 | func run(bgImage, bkImage, itImage *ImageBuf) (*CutoutRet, error) { 28 | ret := new(CutoutRet) 29 | bgWidth := bgImage.getWidth() 30 | bgHeight := bgImage.getHeight() 31 | bkWidth := bkImage.getWidth() 32 | bkHeight := bkImage.getHeight() 33 | ret.Point = randPoint(bgWidth, bgHeight, bkWidth, bkHeight) 34 | newBkImage := &ImageBuf{ 35 | w: bkWidth, 36 | h: bkHeight, 37 | i: image.NewNRGBA(image.Rect(0, 0, bkWidth, bkHeight)), 38 | } 39 | x := ret.Point.X 40 | 41 | // 抠图 42 | cutOut(bgImage, bkImage, newBkImage, x) 43 | 44 | if interferenceOptions > 0 { 45 | position := 0 46 | if bgWidth-x-5 > bkWidth*2 { 47 | //在原扣图右边插入干扰图 48 | position = r.Intn((bgWidth-bkWidth)-(x+bkWidth+5)) + (x + bkWidth + 5) 49 | } else { 50 | //在原扣图左边插入干扰图 51 | position = r.Intn((x-bkWidth-5)-100) + 100 52 | } 53 | // 干扰图 54 | interfere(bgImage, itImage, position) 55 | } 56 | 57 | var err error 58 | ret.BackgroudImg, err = img2Base64(bgImage) 59 | if err != nil { 60 | return nil, err 61 | } 62 | ret.BlockImg, err = img2Base64(newBkImage) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return ret, nil 67 | } 68 | 69 | // randPoint 随机生成抠图位置 70 | func randPoint(bgWidth, bgHeight, bkWidth, bkHeight int) *Point { 71 | wDiff := bgWidth - bkWidth 72 | hDiff := bgHeight - bkWidth 73 | var x, y int 74 | if wDiff <= 0 { 75 | x = 5 76 | } else { 77 | x = r.Intn(wDiff-100) + 100 78 | } 79 | if hDiff <= 0 { 80 | y = 5 81 | } else { 82 | y = r.Intn(hDiff) + 5 83 | } 84 | return &Point{x, y} 85 | } 86 | 87 | // cutOut 抠图 88 | func cutOut(bgImage, bkImage, newBkImage *ImageBuf, x int) { 89 | var values [9]color.RGBA64 90 | bkWidth := bkImage.getWidth() 91 | bkHeight := bkImage.getHeight() 92 | for i := 0; i < bkWidth; i++ { 93 | for j := 0; j < bkHeight; j++ { 94 | pixel := bkImage.getRGBA(i, j) 95 | // 滑块图片非透明像素点,从背景图偏移x 像素拷贝到新图层 96 | if pixel.A > 0 { 97 | newBkImage.setRGBA(i, j, bgImage.getRGBA(x+i, j)) 98 | readNeighborPixel(bgImage, x+i, j, &values) 99 | bgImage.setRGBA(x+i, j, gaussianBlur(&values)) 100 | } 101 | 102 | if i == (bkWidth-1) || j == (bkHeight-1) { 103 | continue 104 | } 105 | rightPixel := bkImage.getRGBA(i+1, j) 106 | bottomPixel := bkImage.getRGBA(i, j+1) 107 | // 用白色给底图和新图层描边 108 | if (pixel.A > 0 && rightPixel.A == 0) || 109 | (pixel.A == 0 && rightPixel.A > 0) || 110 | (pixel.A > 0 && bottomPixel.A == 0) || 111 | (pixel.A == 0 && bottomPixel.A > 0) { 112 | white := color.White 113 | newBkImage.setRGBA(i, j, white) 114 | bgImage.setRGBA(x+i, j, white) 115 | } 116 | } 117 | } 118 | } 119 | 120 | //readNeighborPixel 读取邻近9个点像素,后面最类似高斯模糊计算 121 | //(并非严格的高斯模糊,高斯模糊算法效率太低,本例不需要严格的高斯模糊算法) 122 | // |2|3|4| 123 | // |5|1|6| 124 | // |7|8|9| 125 | // 中心点为1 126 | func readNeighborPixel(img *ImageBuf, x, y int, pixels *[9]color.RGBA64) { 127 | xStart := x - 1 128 | yStart := y - 1 129 | current := 0 130 | for i := xStart; i < 3+xStart; i++ { 131 | for j := yStart; j < 3+yStart; j++ { 132 | tx := i 133 | if tx < 0 { 134 | tx = -tx 135 | } else if tx >= img.getWidth() { 136 | tx = x 137 | } 138 | ty := j 139 | if ty < 0 { 140 | ty = -ty 141 | } else if ty >= img.getHeight() { 142 | ty = y 143 | } 144 | pixels[current] = img.getRGBA(tx, ty) 145 | current++ 146 | } 147 | } 148 | } 149 | 150 | // gaussianBlur 类高斯模糊算法 151 | func gaussianBlur(values *[9]color.RGBA64) color.RGBA64 { 152 | //这边需要 uint32 防止多个uint16相加后溢出 153 | var r uint32 154 | var g uint32 155 | var b uint32 156 | var a uint32 157 | for i := 0; i < len(values); i++ { 158 | if i == 4 { //去掉中间原像素点 159 | continue 160 | } 161 | x := values[i] 162 | r += uint32(x.R) 163 | g += uint32(x.G) 164 | b += uint32(x.B) 165 | a += uint32(x.A) 166 | } 167 | return color.RGBA64{ 168 | uint16(r / 8), 169 | uint16(g / 8), 170 | uint16(b / 8), 171 | uint16(a / 8)} 172 | } 173 | 174 | // img2Base64 图片base64 175 | func img2Base64(image *ImageBuf) (string, error) { 176 | var buf bytes.Buffer 177 | if err := png.Encode(&buf, image.i); err != nil { 178 | return "", fmt.Errorf("unable to encode png: %w", err) 179 | } 180 | data := buf.Bytes() 181 | return base64.StdEncoding.EncodeToString(data), nil 182 | } 183 | 184 | // 干扰图片 185 | func interfere(bgImage, iImage *ImageBuf, x int) { 186 | var values [9]color.RGBA64 187 | iWidth := iImage.getWidth() 188 | iHeight := iImage.getHeight() 189 | 190 | for i := 0; i < iWidth; i++ { 191 | for j := 0; j < iHeight; j++ { 192 | pixel := iImage.getRGBA(i, j) 193 | if pixel.A > 0 { 194 | readNeighborPixel(bgImage, x+i, j, &values) 195 | bgImage.setRGBA(x+i, j, gaussianBlur(&values)) 196 | } 197 | if i == (iWidth-1) || j == (iHeight-1) { 198 | continue 199 | } 200 | rightPixel := iImage.getRGBA(i+1, j) 201 | bottomPixel := iImage.getRGBA(i, j+1) 202 | if (pixel.A > 0 && rightPixel.A == 0) || 203 | (pixel.A == 0 && rightPixel.A > 0) || 204 | (pixel.A > 0 && bottomPixel.A == 0) || 205 | (pixel.A == 0 && bottomPixel.A > 0) { 206 | white := color.White 207 | bgImage.setRGBA(x+i, j, white) 208 | } 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /puzzle_captcha/check.go: -------------------------------------------------------------------------------- 1 | package captcha 2 | 3 | import "errors" 4 | 5 | const slipOffset = 5.0 6 | 7 | var ( 8 | ErrPostionErr = errors.New("postion error") 9 | ) 10 | 11 | // Check 验证位置是否正确 12 | func Check(paramInPoint *Point, cachedPoint *Point) error { 13 | if cachedPoint.X-slipOffset > paramInPoint.X || 14 | paramInPoint.X > cachedPoint.X+slipOffset { 15 | return ErrPostionErr 16 | } 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /puzzle_captcha/image.go: -------------------------------------------------------------------------------- 1 | package captcha 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | type ImageBuf struct { 9 | i image.Image 10 | w int 11 | h int 12 | } 13 | 14 | func (i *ImageBuf) getHeight() int { 15 | return i.h 16 | } 17 | 18 | func (i *ImageBuf) getWidth() int { 19 | return i.w 20 | } 21 | 22 | func (i *ImageBuf) getRGBA(x, y int) color.RGBA64 { 23 | r, g, b, a := i.i.At(x, y).RGBA() 24 | return color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)} 25 | } 26 | 27 | func (i *ImageBuf) setRGBA(x, y int, c color.Color) { 28 | switch i.i.(type) { 29 | case *image.RGBA: 30 | i.i.(*image.RGBA).Set(x, y, c) 31 | case *image.NRGBA: 32 | i.i.(*image.NRGBA).Set(x, y, c) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /puzzle_captcha/image_cache.go: -------------------------------------------------------------------------------- 1 | package captcha 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "io/ioutil" 7 | "path" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | bgImgCache [][]byte //缓存背景图片 13 | bkImgCache [][]byte //缓存滑块模板图片 14 | ) 15 | 16 | func LoadBackgroudImages(path string) (err error) { 17 | bgImgCache, err = loadImages(path) 18 | return 19 | } 20 | 21 | func LoadBlockImages(path string) (err error) { 22 | bkImgCache, err = loadImages(path) 23 | return 24 | } 25 | 26 | func loadImages(basePath string) ([][]byte, error) { 27 | files, err := ioutil.ReadDir(basePath) 28 | if err != nil { 29 | return nil, err 30 | } 31 | var fileArr [][]byte 32 | for _, f := range files { 33 | if f.IsDir() { 34 | continue 35 | } 36 | if strings.HasSuffix(f.Name(), ".png") { 37 | buf, err := ioutil.ReadFile(path.Join(basePath, f.Name())) 38 | if err != nil { 39 | return nil, err 40 | } 41 | fileArr = append(fileArr, buf) 42 | } 43 | } 44 | return fileArr, nil 45 | } 46 | 47 | // randBackgroudImage 随机抽取 背景图 48 | func randBackgroudImage() (*ImageBuf, error) { 49 | n := r.Intn(len(bgImgCache)) 50 | buf := bgImgCache[n] 51 | im, _, err := image.Decode(bytes.NewReader(buf)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return &ImageBuf{ 56 | w: im.Bounds().Dx(), 57 | h: im.Bounds().Dy(), 58 | i: im, 59 | }, nil 60 | } 61 | 62 | // randBlockImage 随机抽取 滑块图,和干扰图 63 | func randBlockImage() (a *ImageBuf, b *ImageBuf, err error) { 64 | l := len(bkImgCache) 65 | n := r.Intn(len(bkImgCache)) 66 | buf := bkImgCache[n] 67 | im, _, err := image.Decode(bytes.NewReader(buf)) 68 | if err != nil { 69 | return nil, nil, err 70 | } 71 | var next = n + 1 72 | if next == l { 73 | next = 0 74 | } 75 | buf2 := bkImgCache[next] 76 | im2, _, err := image.Decode(bytes.NewReader(buf2)) 77 | if err != nil { 78 | return nil, nil, err 79 | } 80 | a = &ImageBuf{ 81 | w: im.Bounds().Dx(), 82 | h: im.Bounds().Dy(), 83 | i: im, 84 | } 85 | b = &ImageBuf{ 86 | w: im2.Bounds().Dx(), 87 | h: im2.Bounds().Dy(), 88 | i: im2, 89 | } 90 | return 91 | } 92 | -------------------------------------------------------------------------------- /puzzle_captcha/rand.go: -------------------------------------------------------------------------------- 1 | package captcha 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var r = rand.New(rand.NewSource(time.Now().UnixNano())) 9 | -------------------------------------------------------------------------------- /puzzle_captcha/types.go: -------------------------------------------------------------------------------- 1 | package captcha 2 | 3 | // Point 随机生成的抠图位置 4 | type Point struct { 5 | X int 6 | Y int 7 | } 8 | 9 | // CutoutRet 抠图出来的结果 10 | type CutoutRet struct { 11 | Point *Point 12 | BackgroudImg string 13 | BlockImg string 14 | } 15 | -------------------------------------------------------------------------------- /tools/imagedebug/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //调整算法后,快速验证的工具 4 | import ( 5 | "encoding/base64" 6 | "io/ioutil" 7 | 8 | captcha "github.com/widaT/go-captcha/puzzle_captcha" 9 | ) 10 | 11 | func main() { 12 | captcha.LoadBackgroudImages("./images/puzzle_captcha/backgroud") 13 | captcha.LoadBlockImages("./images/puzzle_captcha/block") 14 | 15 | ret, err := captcha.Run() 16 | if err != nil { 17 | return 18 | } 19 | 20 | saveImage("test_data/bg.png", ret.BackgroudImg) 21 | saveImage("test_data/bk.png", ret.BlockImg) 22 | } 23 | 24 | func saveImage(path string, base64Img string) error { 25 | i, err := base64.StdEncoding.DecodeString(base64Img) 26 | if err != nil { 27 | return err 28 | } 29 | return ioutil.WriteFile(path, i, 0666) 30 | } 31 | --------------------------------------------------------------------------------