zcool321@sina.com 6 vuotta sitten
commit
fc56edc821
18 muutettua tiedostoa jossa 1076 lisäystä ja 0 poistoa
  1. 14 0
      .gitignore
  2. 201 0
      LICENSE
  3. 67 0
      README.md
  4. 43 0
      boot/boot.go
  5. 76 0
      boot/router.go
  6. 11 0
      config/config.toml
  7. 5 0
      go.mod
  8. 2 0
      go.sum
  9. 330 0
      gtoken/gtoken.go
  10. 10 0
      main.go
  11. 3 0
      public/resource/css/index.css
  12. BIN
      public/resource/image/favicon.ico
  13. 0 0
      public/resource/js/.gitkeep
  14. 16 0
      template/index.html
  15. 152 0
      test/api_test.go
  16. 22 0
      test/cache_test.go
  17. 32 0
      test/token_test.go
  18. 92 0
      utils/resp/resp.go

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+.idea
+logs

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [2019] [FLY的狐狸]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 67 - 0
README.md

@@ -0,0 +1,67 @@
+# gtoken
+
+#### 介绍
+基于gf框架的token插件,通过服务端验证方式实现token认证;
+
+1. 支持单机gcache模式
+2. 支持简单token认证
+3. 框架使用简单,只需要设置登录验证方法以及登录、登出、拦截路径即可;
+
+#### 安装教程
+
+gopath模式: `go get https://gitee.com/goflyfox/gtoken`
+
+或者 使用go.mod添加 :`require gitee.com/goflyfox/gtoken last`
+
+#### 使用说明
+
+只需要配置登录路径、登出路径、拦截路径以及登录校验实现即可
+
+```go
+	// 启动gtoken
+	gtoken := &gtoken.GfToken{
+		LoginPath:       "/login",
+		LoginBeforeFunc: loginFunc,
+		LogoutPath:      "/user/logout",
+		AuthPaths:       g.SliceStr{"/user/*", "/system/*"},
+	}
+	gtoken.Start()
+```
+
+登录方法实现
+
+```go
+func Login(r *ghttp.Request) (string, interface{}) {
+	username := r.GetPostString("username")
+	passwd := r.GetPostString("passwd")
+
+	// TODO 进行登录校验
+
+	return username, ""
+}
+```
+
+#### 逻辑测试
+
+可运行api_test.go进行测试并查看结果;验证逻辑说明:
+
+1. 访问用户信息,提示未携带token
+2. 登录后,携带token访问正常
+3. 登出成功
+4. 携带之前token访问,提示未登录
+
+```json
+--- PASS: TestSystemUser (0.00s)
+    api_test.go:43: 1. not login and visit user
+    api_test.go:50: {"code":-1,"data":"","msg":"query token fail"}
+    api_test.go:63: 2. execute login and visit user
+    api_test.go:66: {"code":0,"msg":"success","data":"system user"}
+    api_test.go:72: 3. execute logout
+    api_test.go:75: {"code":0,"msg":"success","data":"logout success"}
+    api_test.go:81: 4. visit user
+    api_test.go:86: {"code":-1,"msg":"login timeout or not login","data":""}
+```
+
+#### 感谢
+
+1. gf框架 [https://github.com/gogf/gf](https://github.com/gogf/gf) 

+ 43 - 0
boot/boot.go

@@ -0,0 +1,43 @@
+package boot
+
+import (
+	"github.com/gogf/gf/g"
+	"github.com/gogf/gf/g/net/ghttp"
+	"github.com/gogf/gf/g/os/glog"
+)
+
+// 管理初始化顺序.
+func init() {
+	initConfig()
+	initRouter()
+}
+
+// 用于配置初始化.
+func initConfig() {
+	glog.Info("########service start...")
+
+	v := g.View()
+	c := g.Config()
+	s := g.Server()
+
+	path := ""
+	// 配置对象及视图对象配置
+	c.AddPath(path + "config")
+
+	v.SetDelimiters("${", "}")
+	v.AddPath(path + "template")
+
+	// glog配置
+	logPath := c.GetString("log-path")
+	glog.SetPath(logPath)
+	glog.SetStdoutPrint(true)
+
+	s.SetServerRoot("./public")
+	s.SetNameToUriType(ghttp.NAME_TO_URI_TYPE_ALLLOWER)
+	s.SetLogPath(logPath)
+	s.SetErrorLogEnabled(true)
+	s.SetAccessLogEnabled(true)
+	s.SetPort(c.GetInt("http-port"))
+
+	glog.Info("########service finish.")
+}

+ 76 - 0
boot/router.go

@@ -0,0 +1,76 @@
+package boot
+
+import (
+	"github.com/gogf/gf/g"
+	"github.com/gogf/gf/g/net/ghttp"
+	"github.com/gogf/gf/g/os/glog"
+	"gtoken/gtoken"
+	"gtoken/utils/resp"
+)
+
+/*
+绑定业务路由
+*/
+func bindRouter() {
+
+	s := g.Server()
+	// 调试路由
+	s.BindHandler("/hello", func(r *ghttp.Request) {
+		r.Response.WriteJson(resp.Succ("hello"))
+	})
+	s.BindHandler("/system/user", func(r *ghttp.Request) {
+		r.Response.WriteJson(resp.Succ("system user"))
+	})
+
+	loginFunc := Login
+	// 启动gtoken
+	gtoken := &gtoken.GfToken{
+		LoginPath:       "/login",
+		LoginBeforeFunc: loginFunc,
+		LogoutPath:      "/user/logout",
+		AuthPaths:       g.SliceStr{"/user/*", "/system/*"},
+	}
+	gtoken.Start()
+
+}
+
+/*
+统一路由注册
+*/
+func initRouter() {
+
+	s := g.Server()
+
+	// 绑定路由
+	bindRouter()
+
+	// 首页
+	s.BindHandler("/", func(r *ghttp.Request) {
+		content, err := g.View().Parse("index.html", map[string]interface{}{
+			"id":    1,
+			"name":  "GTOKEN",
+			"title": g.Config().GetString("setting.title"),
+		})
+		if err != nil {
+			glog.Error(err)
+		}
+		r.Response.Write(content)
+
+	})
+
+	// 某些浏览器直接请求favicon.ico文件,特别是产生404时
+	s.SetRewrite("/favicon.ico", "/resource/image/favicon.ico")
+
+}
+
+func Login(r *ghttp.Request) (string, interface{}) {
+	username := r.GetPostString("username")
+	passwd := r.GetPostString("passwd")
+
+	if username == "" || passwd == "" {
+		r.Response.WriteJson(resp.Fail("账号或密码错误."))
+		r.ExitAll()
+	}
+
+	return username, ""
+}

+ 11 - 0
config/config.toml

@@ -0,0 +1,11 @@
+log-path = "./logs"
+http-port = 80
+
+# title设置
+[setting]
+    title = "token demo"
+
+# WebServer管理
+[admin]
+    user = "admin"
+    pass = "123"

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module gtoken
+
+require github.com/gogf/gf v1.6.17
+
+go 1.12

+ 2 - 0
go.sum

@@ -0,0 +1,2 @@
+github.com/gogf/gf v1.6.17 h1:XWtf3RzV1zqQZX6qoDstoSV2ylE3ckLK66z/xX+ft3M=
+github.com/gogf/gf v1.6.17/go.mod h1:41x91y5T+itylJH0NsW/CfTD87qeBoPe/BX3DZAwP0U=

+ 330 - 0
gtoken/gtoken.go

@@ -0,0 +1,330 @@
+package gtoken
+
+import (
+	"github.com/gogf/gf/g"
+	"github.com/gogf/gf/g/crypto/gaes"
+	"github.com/gogf/gf/g/crypto/gmd5"
+	"github.com/gogf/gf/g/encoding/gbase64"
+	"github.com/gogf/gf/g/net/ghttp"
+	"github.com/gogf/gf/g/os/gcache"
+	"github.com/gogf/gf/g/os/glog"
+	"github.com/gogf/gf/g/text/gstr"
+	"github.com/gogf/gf/g/util/gconv"
+	"github.com/gogf/gf/g/util/grand"
+	"gtoken/utils/resp"
+	"strings"
+)
+
+type GfToken struct {
+	// 缓存key
+	CacheKey string
+	// 超时时间 默认2小时
+	Timeout int
+	// Token分隔符
+	TokenDelimiter string
+	// Token加密key
+	EncryptKey []byte
+
+	LoginPath string
+	// 登录验证方法
+	// return userKey 用户标识 如果userKey为空,结束执行
+	LoginBeforeFunc func(r *ghttp.Request) (string, interface{})
+	// 登录返回方法
+	LoginAfterFunc func(r *ghttp.Request, respData resp.Resp)
+
+	// 登出地址
+	LogoutPath string
+	// 登出验证方法
+	// return true 继续执行,否则结束执行
+	LogoutBeforeFunc func(r *ghttp.Request) bool
+	// 登出返回方法
+	LogoutAfterFunc func(r *ghttp.Request, respData resp.Resp)
+
+	// 拦截地址
+	AuthPaths g.SliceStr
+	// 认证验证方法
+	// return true 继续执行,否则结束执行
+	AuthBeforeFunc func(r *ghttp.Request) bool
+	// 认证返回方法
+	AuthAfterFunc func(r *ghttp.Request, respData resp.Resp)
+}
+
+func (m *GfToken) Init() bool {
+
+	if m.CacheKey == "" {
+		m.CacheKey = "GToken:"
+	}
+
+	if m.Timeout == 0 {
+		m.Timeout = 2 * 60 * 60 * 1000
+	}
+
+	if m.TokenDelimiter == "" {
+		m.TokenDelimiter = "_"
+	}
+
+	if len(m.EncryptKey) == 0 {
+		m.EncryptKey = []byte("12345678912345678912345678912345")
+	}
+
+	if m.LoginAfterFunc == nil {
+		m.LoginAfterFunc = func(r *ghttp.Request, respData resp.Resp) {
+			if !respData.Success() {
+				r.Response.WriteJson(respData)
+			} else {
+				r.Response.WriteJson(resp.Succ(g.Map{
+					"token": respData.GetString("token"),
+				}))
+			}
+		}
+	}
+
+	if m.LogoutBeforeFunc == nil {
+		m.LogoutBeforeFunc = func(r *ghttp.Request) bool {
+			return true
+		}
+	}
+
+	if m.LogoutAfterFunc == nil {
+		m.LogoutAfterFunc = func(r *ghttp.Request, respData resp.Resp) {
+			if respData.Success() {
+				r.Response.WriteJson(resp.Succ("logout success"))
+			} else {
+				r.Response.WriteJson(respData)
+			}
+		}
+	}
+
+	if m.AuthBeforeFunc == nil {
+		m.AuthBeforeFunc = func(r *ghttp.Request) bool {
+			return true
+		}
+	}
+	if m.AuthAfterFunc == nil {
+		m.AuthAfterFunc = func(r *ghttp.Request, respData resp.Resp) {
+			if !respData.Success() {
+				r.Response.WriteJson(respData)
+				r.ExitAll()
+			}
+		}
+	}
+
+	return true
+}
+
+func (m *GfToken) Start() bool {
+	glog.Info("[GToken][params:" + gconv.String(m) + "]start... ")
+	if !m.Init() {
+		return false
+	}
+
+	s := g.Server()
+
+	// 认证拦截器
+	if m.AuthPaths == nil {
+		glog.Error("[GToken]HookPathList not set")
+		return false
+	}
+	for _, authPath := range m.AuthPaths {
+		s.BindHookHandler(authPath, ghttp.HOOK_BEFORE_SERVE, m.authHook)
+	}
+
+	// 登录
+	if m.LoginPath == "" || m.LoginBeforeFunc == nil {
+		glog.Error("[GToken]LoginPath or LoginBeforeFunc not set")
+		return false
+	}
+	s.BindHandler(m.LoginPath, m.login)
+
+	// 登出
+	if m.LogoutPath == "" {
+		glog.Error("[GToken]LogoutPath or LogoutFunc not set")
+		return false
+	}
+	s.BindHandler(m.LogoutPath, m.logout)
+
+	return true
+}
+
+func (m *GfToken) Stop() bool {
+	glog.Info("[GToken]stop. ")
+	return true
+}
+
+// 通过token获取对象
+func (m *GfToken) GetTokenData(r *ghttp.Request) resp.Resp {
+	respData := m.getRequestToken(r)
+	if respData.Success() {
+		// 验证token
+		respData = m.validToken(respData.DataString())
+	}
+
+	return respData
+}
+
+// 登录
+func (m *GfToken) login(r *ghttp.Request) {
+	userKey, data := m.LoginBeforeFunc(r)
+	if userKey != "" {
+		// 生成token
+		respToken := m.genToken(userKey, data)
+		m.LoginAfterFunc(r, respToken)
+	}
+
+}
+
+// 登出
+func (m *GfToken) logout(r *ghttp.Request) {
+	if m.LogoutBeforeFunc(r) {
+		// 获取请求token
+		respData := m.getRequestToken(r)
+		if respData.Success() {
+			// 删除token
+			m.removeToken(respData.DataString())
+		}
+
+		m.LogoutAfterFunc(r, respData)
+	}
+}
+
+// 认证拦截
+func (m *GfToken) authHook(r *ghttp.Request) {
+	if m.AuthBeforeFunc(r) {
+		// 获取请求token
+		tokenResp := m.getRequestToken(r)
+		if tokenResp.Success() {
+			// 验证token
+			tokenResp = m.validToken(tokenResp.DataString())
+		}
+
+		m.AuthAfterFunc(r, tokenResp)
+	}
+}
+
+// 返回请求Token
+func (m *GfToken) getRequestToken(r *ghttp.Request) resp.Resp {
+	authHeader := r.Header.Get("Authorization")
+	if authHeader != "" {
+		parts := strings.SplitN(authHeader, " ", 2)
+		if !(len(parts) == 2 && parts[0] == "Bearer") {
+			glog.Info("[GToken]authHeader:" + authHeader + "get token key fail")
+			return resp.Fail("get token key fail")
+		} else if parts[1] == "" {
+			return resp.Fail("get token fail")
+		}
+
+		return resp.Succ(parts[1])
+	}
+
+	authHeader = r.GetPostString("token")
+	if authHeader == "" {
+		return resp.Fail("query token fail")
+	}
+	return resp.Succ(authHeader)
+
+}
+
+// 生成Token
+func (m *GfToken) genToken(userKey string, data interface{}) resp.Resp {
+	token := m.EncryptToken(userKey)
+	if !token.Success() {
+		return token
+	}
+
+	cacheKey := m.CacheKey + userKey
+	cacheValue := g.Map{
+		"userKey": userKey,
+		"uuid":    token.GetString("uuid"),
+		"data":    data,
+	}
+	gcache.Set(cacheKey, cacheValue, m.Timeout)
+
+	return token
+}
+
+// 验证Token
+func (m *GfToken) validToken(token string) resp.Resp {
+	if token == "" {
+		return resp.Fail("valid token empty")
+	}
+
+	decryptToken := m.DecryptToken(token)
+	if !decryptToken.Success() {
+		return decryptToken
+	}
+
+	userKey := decryptToken.GetString("userKey")
+	uuid := decryptToken.GetString("uuid")
+	cacheKey := m.CacheKey + userKey
+
+	userCache := gcache.Get(cacheKey)
+
+	if userCache == nil {
+		return resp.Fail("login timeout or not login")
+	}
+
+	cacheValue := gconv.Map(userCache)
+	if uuid != cacheValue["uuid"] {
+		glog.Error("[GToken]user auth error, decryptToken:" + decryptToken.Json() + " cacheValue:" + gconv.String(cacheValue))
+		return resp.Fail("user auth error")
+	}
+
+	return resp.Succ(userCache)
+}
+
+// 删除Token
+func (m *GfToken) removeToken(token string) resp.Resp {
+	decryptToken := m.DecryptToken(token)
+	if !decryptToken.Success() {
+		return decryptToken
+	}
+
+	cacheKey := m.CacheKey + decryptToken.GetString("userKey")
+	gcache.Remove(cacheKey)
+
+	return resp.Succ("")
+}
+
+func (m *GfToken) EncryptToken(userKey string) resp.Resp {
+	if userKey == "" {
+		return resp.Fail("encrypt userKey empty")
+	}
+
+	uuid := gmd5.Encrypt(grand.Str(10))
+	tokenStr := userKey + m.TokenDelimiter + uuid
+
+	token, err := gaes.Encrypt([]byte(tokenStr), m.EncryptKey)
+	if err != nil {
+		return resp.Error("encrypt error")
+	}
+
+	return resp.Succ(g.Map{
+		"userKey": userKey,
+		"uuid":    uuid,
+		"token":   gbase64.Encode(string(token)),
+	})
+}
+
+func (m *GfToken) DecryptToken(token string) resp.Resp {
+	if token == "" {
+		return resp.Fail("decrypt token empty")
+	}
+
+	token64, err := gbase64.Decode(token)
+	if err != nil {
+		return resp.Error("decode error")
+	}
+	decryptToken, err2 := gaes.Decrypt([]byte(token64), m.EncryptKey)
+	if err2 != nil {
+		return resp.Error("decrypt error")
+	}
+	tokenArray := gstr.Split(string(decryptToken), m.TokenDelimiter)
+	if len(tokenArray) < 2 {
+		return resp.Error("token len error")
+	}
+
+	return resp.Succ(g.Map{
+		"userKey": tokenArray[0],
+		"uuid":    tokenArray[1],
+	})
+}

+ 10 - 0
main.go

@@ -0,0 +1,10 @@
+package main
+
+import (
+	"github.com/gogf/gf/g"
+	_ "gtoken/boot"
+)
+
+func main() {
+	g.Server().Run()
+}

+ 3 - 0
public/resource/css/index.css

@@ -0,0 +1,3 @@
+body {
+    text-align: center;
+}

BIN
public/resource/image/favicon.ico


+ 0 - 0
public/resource/js/.gitkeep


+ 16 - 0
template/index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>${.title}</title>
+    <link rel="icon" href="/resource/image/favicon.ico" type="image/x-icon">
+    <link rel="stylesheet" href="/resource/css/index.css">
+</head>
+<body>
+<div class="container">
+    <div>
+        欢迎您访问${.name}示例
+    </div>
+</div>
+</body>
+</html>

+ 152 - 0
test/api_test.go

@@ -0,0 +1,152 @@
+package test
+
+import (
+	"encoding/json"
+	"github.com/gogf/gf/g"
+	"github.com/gogf/gf/g/net/ghttp"
+	"gtoken/utils/resp"
+	"testing"
+)
+
+const (
+	TestURL string = "http://127.0.0.1:80"
+)
+
+var (
+	Token    = g.MapStrStr{}
+	Username = "flyfox"
+)
+
+func TestHello(t *testing.T) {
+	t.Log("visit hello and no auth")
+	if r, e := ghttp.Post(TestURL+"/hello", "username="+Username); e != nil {
+		t.Error(e)
+	} else {
+		defer r.Close()
+
+		content := string(r.ReadAll())
+		t.Log(content)
+
+		var respData resp.Resp
+		err := json.Unmarshal([]byte(content), &respData)
+		if err != nil {
+			t.Error(err)
+		}
+		if !respData.Success() {
+			t.Error(respData.Json())
+		}
+	}
+}
+
+func TestSystemUser(t *testing.T) {
+	// 未登录
+	t.Log("1. not login and visit user")
+	if r, e := ghttp.Post(TestURL+"/system/user", "username="+Username); e != nil {
+		t.Error(e)
+	} else {
+		defer r.Close()
+
+		content := string(r.ReadAll())
+		t.Log(content)
+
+		var respData resp.Resp
+		err := json.Unmarshal([]byte(content), &respData)
+		if err != nil {
+			t.Error(err)
+		}
+		if respData.Success() {
+			t.Error(respData.Json())
+		}
+	}
+
+	// 登录,访问用户信息
+	t.Log("2. execute login and visit user")
+	data := Post(t, "/system/user", "username="+Username)
+	if data.Success() {
+		t.Log(data.Json())
+	} else {
+		t.Error(data.Json())
+	}
+
+	// 登出
+	t.Log("3. execute logout")
+	data = Post(t, "/user/logout", "username="+Username)
+	if data.Success() {
+		t.Log(data.Json())
+	} else {
+		t.Error(data.Json())
+	}
+
+	// 登出访问用户信息
+	t.Log("4. visit user")
+	data = Post(t, "/system/user", "username="+Username)
+	if data.Success() {
+		t.Error(data.Json())
+	} else {
+		t.Log(data.Json())
+	}
+}
+
+func TestLogin(t *testing.T) {
+	Username = "testLogin"
+	t.Log(" login first ")
+	token1 := getToken(t)
+	t.Log("token:" + token1)
+	t.Log(" login second and same token ")
+	t.Log("token:" + getToken(t))
+	if token1 != getToken(t) {
+		t.Error("token not same ")
+	}
+	Username = "flyfox"
+}
+
+func TestLogout(t *testing.T) {
+	Username = "testLogout"
+	t.Log(" logout test ")
+	data := Post(t, "/user/logout", "username="+Username)
+	if data.Success() {
+		t.Log(data.Json())
+	} else {
+		t.Error(data.Json())
+	}
+	Username = "flyfox"
+}
+
+func Post(t *testing.T, urlPath string, data ...interface{}) resp.Resp {
+	client := ghttp.NewClient()
+	client.SetHeader("Authorization", "Bearer "+getToken(t))
+	content := client.DoRequestContent("POST", TestURL+urlPath, data...)
+	var respData resp.Resp
+	err := json.Unmarshal([]byte(content), &respData)
+	if err != nil {
+		t.Error(err)
+	}
+	return respData
+}
+
+func getToken(t *testing.T) string {
+	if Token[Username] != "" {
+		return Token[Username]
+	}
+
+	if r, e := ghttp.Post(TestURL+"/login", "username="+Username+"&passwd=123456"); e != nil {
+		t.Error(e)
+	} else {
+		defer r.Close()
+
+		content := string(r.ReadAll())
+
+		var respData resp.Resp
+		err := json.Unmarshal([]byte(content), &respData)
+		if err != nil {
+			t.Error(err)
+		}
+
+		if !respData.Success() {
+			t.Error("resp fail:" + respData.Json())
+		}
+
+		Token[Username] = respData.GetString("token")
+	}
+	return Token[Username]
+}

+ 22 - 0
test/cache_test.go

@@ -0,0 +1,22 @@
+package test
+
+import (
+	"github.com/gogf/gf/g/os/gcache"
+	"testing"
+)
+
+func TestCache(t *testing.T) {
+	t.Log("cache test ")
+	userKey := "123123"
+	gcache.Set(userKey, "1", 10000)
+
+	if gcache.Get(userKey).(string) == userKey {
+		t.Error("cache get error")
+	}
+
+	gcache.Remove(userKey)
+	if gcache.Get(userKey) != nil {
+		t.Error("cache remove error")
+	}
+
+}

+ 32 - 0
test/token_test.go

@@ -0,0 +1,32 @@
+package test
+
+import (
+	"gtoken/gtoken"
+	"testing"
+)
+
+func TestToken(t *testing.T) {
+	t.Log(" token test ")
+	gtoken := gtoken.GfToken{}
+	gtoken.Init()
+
+	userKey := "123123"
+	token := gtoken.EncryptToken(userKey)
+	if !token.Success() {
+		t.Error(token.Json())
+	}
+	t.Log(token.DataString())
+
+	token2 := gtoken.DecryptToken(token.GetString("token"))
+	if !token2.Success() {
+		t.Error(token2.Json())
+	}
+	t.Log(token2.DataString())
+	if userKey != token2.GetString("userKey") {
+		t.Error("token decrypt userKey error")
+	}
+	if token.GetString("uuid") != token2.GetString("uuid") {
+		t.Error("token decrypt uuid error")
+	}
+
+}

+ 92 - 0
utils/resp/resp.go

@@ -0,0 +1,92 @@
+package resp
+
+import (
+	"encoding/json"
+	"github.com/gogf/gf/g/util/gconv"
+)
+
+const (
+	SUCCESS      = 0
+	FAIL         = -1
+	ERROR        = -99
+	UNAUTHORIZED = -401
+	//配置
+	TYPE_CONFIG = 1
+	//  字典
+	TYPE_DICT = 2
+)
+
+type Resp struct {
+	Code int         `json:"code"`
+	Msg  string      `json:"msg"`
+	Data interface{} `json:"data"`
+}
+
+// 获取Data值转字符串
+func (resp Resp) Success() bool {
+	return resp.Code == SUCCESS
+}
+
+// 获取Data转字符串
+func (resp Resp) DataString() string {
+	return gconv.String(resp.Data)
+}
+
+// 获取Data转Int
+func (resp Resp) DataInt() int {
+	return gconv.Int(resp.Data)
+}
+
+// 获取Data值转字符串
+func (resp Resp) GetString(key string) string {
+	return gconv.String(resp.Get(key))
+}
+
+// 获取Data值转Int
+func (resp Resp) GetInt(key string) int {
+	return gconv.Int(resp.Get(key))
+}
+
+// 获取Data值
+func (resp Resp) Get(key string) interface{} {
+	m := gconv.Map(resp.Data)
+	if m == nil {
+		return ""
+	}
+	return m[key]
+}
+
+func (resp Resp) Json() string {
+	str, _ := json.Marshal(resp)
+	return string(str)
+}
+
+// 成功
+func Succ(data interface{}) Resp {
+	return Resp{SUCCESS, "success", data}
+}
+
+// 失败
+func Fail(msg string) Resp {
+	return Resp{FAIL, msg, ""}
+}
+
+// 失败设置Data
+func FailData(msg string, data interface{}) Resp {
+	return Resp{FAIL, msg, data}
+}
+
+// 错误
+func Error(msg string) Resp {
+	return Resp{ERROR, msg, ""}
+}
+
+// 错误设置Data
+func ErrorData(msg string, data interface{}) Resp {
+	return Resp{ERROR, msg, data}
+}
+
+// 认证失败
+func Unauthorized(msg string, data interface{}) Resp {
+	return Resp{UNAUTHORIZED, msg, data}
+}