diff --git a/GinSkeleton/.vscode/lanuch.json b/GinSkeleton/.vscode/lanuch.json new file mode 100644 index 0000000..d495fcf --- /dev/null +++ b/GinSkeleton/.vscode/lanuch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "~/WorkSpace/GinSkeleto", + "env": {}, + "args": [] + } + ] +} \ No newline at end of file diff --git a/GinSkeleton/api_doc.md b/GinSkeleton/api_doc.md new file mode 100644 index 0000000..b661a6a --- /dev/null +++ b/GinSkeleton/api_doc.md @@ -0,0 +1,14 @@ +## 获取公钥 +/admin/users/publickey +参数字段|参数属性|类型|选项| +user_name|form-data|string|必填 +> 返回示例: +```json +{ + "code": 200, + "data": { + "PublicKey": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFrZUx2RWNpS3o5UytUQ3E1VnE5MQpTU2RaNm55d3VzbHNPRzAzZnY1VXMxVzJTR0ZDVnpTY3N6aWlLYlIrQk9FR3JsSVRQN29Yb2w3enhQUm55eTczCkZjTkRDOHQwQlhxcGR0U3pkL3V1N1JndXpDYW5BYXVaRXh4RERLUmZEV0MrR0p4TUlBaUV0VHJwT1d6dWp1azgKbDdDMWprTlRhQUpBMmx6ODA2ZWNHZ1NIcFg4MHhCZUpwV3lERnF2N3J3eS9EWjhaekQvRTNXa2ZLREUvRzFFTApPNWRBWUg0QXoxcVQ3SHFEY0hpVVlrNGFDWUswb1pJSC9hSXlKRjhnMDVIbER6NUN2eXNXZVZCTWljT0VRaXQxCnJMaDRaWVhqSVAyVEZmYU5hTlpuVVdhR1BCc05VRThjRU92MlA1d3ZHazFGL29yQ0NyNlFQRytBVE00SU1EME4KR3dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + }, + "msg": "Success" +} +``` \ No newline at end of file diff --git a/GinSkeleton/app/http/controller/web/users_controller.go b/GinSkeleton/app/http/controller/web/users_controller.go index aa8428b..3230050 100644 --- a/GinSkeleton/app/http/controller/web/users_controller.go +++ b/GinSkeleton/app/http/controller/web/users_controller.go @@ -1,6 +1,7 @@ package web import ( + // "fmt" "goskeleton/app/global/consts" "goskeleton/app/global/variable" "goskeleton/app/model" @@ -9,6 +10,7 @@ import ( "goskeleton/app/utils/response" rsa "goskeleton/app/utils/rsa" "time" + "net/http" "github.com/gin-gonic/gin" ) @@ -199,64 +201,14 @@ func (u *Users) Logout(c *gin.Context) { func (u *Users)PublicKey(c *gin.Context){ userName:=c.GetString(consts.ValidatorPrefix+"user_name") key := model.CreateUserFactory("").PublicKey(userName) + // fmt.Println("public key: ",string(key)) if key!=nil{ - response.Success(c,consts.CurdStatusOkMsg,gin.H{"PublicKey":key}) + // response.Success(c,consts.CurdStatusOkMsg,gin.H{"PublicKey":key}) + c.Data(http.StatusOK, "application/x-pem-file", key) }else{ response.Fail(c,consts.CurdPublicKeyFailCode,consts.CurdPublicKeyFailMsg,"") } } -// // 解密密码 -// func DecryptPassword(userName,pass string)[]byte{ -// if pass==""{ -// return nil -// } -// key:=model.CreateUserFactory("").PrivateKey(userName) -// if key!=nil{ -// privateKey,err:=parsePrivateKeyFromPEM(key) -// if err!=nil{ -// return nil -// } - -// // Base64 decode the encrypted password -// encryptedPassword, err := base64.StdEncoding.DecodeString(pass) -// if err != nil { -// return nil -// } -// // Decrypt the password using the private key -// decryptedBytes, err := rsa.DecryptPKCS1v15(rand.Reader, privateKey, encryptedPassword) -// if err != nil { -// return nil -// } -// return decryptedBytes -// } -// return nil -// } - -// // 解析PEM格式的私钥 -// func parsePrivateKeyFromPEM(pemKey []byte)(*rsa.PrivateKey,error){ - -// block, _ := pem.Decode(pemKey) -// if block == nil { -// return nil, fmt.Errorf("failed to parse PEM block") -// } -// // 解析私钥 -// privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) -// if err != nil { -// // 如果不是PKCS#1格式,尝试使用PKCS#8格式解析 -// privateKeyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) -// if err != nil { -// return nil, fmt.Errorf("failed to parse PKCS8 private key: %v", err) -// } -// privateKey, ok := privateKeyInterface.(*rsa.PrivateKey) -// if !ok { -// return nil, fmt.Errorf("parsed key is not an RSA private key") -// } -// return privateKey,nil -// } -// return privateKey, nil -// } - - // 密码解密 func (u *Users)DecryptPassword(userName, pass string) []byte { @@ -274,7 +226,7 @@ func (u *Users)DecryptPassword(userName, pass string) []byte { // 解析私钥 privateKey, err := rsa.ParsePrivateKeyFromPEM(key) if err != nil { - variable.ZapLog.Error("私钥解析失败") + variable.ZapLog.Error("私钥解析失败,"+err.Error()) return nil } diff --git a/GinSkeleton/app/model/users.go b/GinSkeleton/app/model/users.go index 966de93..b14ef00 100644 --- a/GinSkeleton/app/model/users.go +++ b/GinSkeleton/app/model/users.go @@ -2,7 +2,7 @@ package model import ( - "encoding/pem" + // "encoding/pem" // "goskeleton/app/global/consts" "goskeleton/app/global/variable" "goskeleton/app/service/users/token_cache_redis" @@ -73,8 +73,8 @@ func (u *UsersModel) Login(userName string, pass string) *UsersModel { //记录用户登陆(login)生成的token,每次登陆记录一次token func (u *UsersModel) OauthLoginToken(userId int64, token string, expiresAt int64) bool { sql := ` - INSERT INTO tb_oauth_access_tokens(fr_user_id,action_name,token,expires_at,client_ip) - SELECT ?,'login',? ,?,? FROM DUAL WHERE NOT EXISTS(SELECT 1 FROM tb_oauth_access_tokens a WHERE a.fr_user_id=? AND a.action_name='login' AND a.token=? ) + INSERT INTO tb_oauth_access_tokens(fr_user_id,action_name,token,expires_at) + SELECT ?,'login',? ,? FROM DUAL WHERE NOT EXISTS(SELECT 1 FROM tb_oauth_access_tokens a WHERE a.fr_user_id=? AND a.action_name='login' AND a.token=? ) ` //注意:token的精确度为秒,如果在一秒之内,一个账号多次调用接口生成的token其实是相同的,这样写入数据库,第二次的影响行数为0,知己实际上操作仍然是有效的。 //所以这里只判断无错误即可,判断影响行数的话,>=0 都是ok的 @@ -277,13 +277,20 @@ func (u *UsersModel) Destroy(id int,userName,pass string) bool { // 账号密码验证成功 if len(u.Pass) > 0 && (u.Pass == md5_encrypt.Base64Md5(pass)) { // 删除用户密钥 - u.deleteKeyFromDB(userName) + err:=u.deleteKeyFromDB(userName) + if err!=nil{ + variable.ZapLog.Error(err.Error()) + } // 删除用户 if u.Delete(u, id).Error == nil { // 删除token if u.OauthDestroyToken(id) { return true + }else{ + variable.ZapLog.Error("删除用户时token删除失败") } + }else{ + variable.ZapLog.Error("删除用户失败") } } } @@ -302,7 +309,9 @@ func (u *UsersModel)Logout(id int,userName string)bool{ variable.ZapLog.Error("登出时删除token失败") } // 删除用户密钥 - u.deleteKeyFromDB(userName) + if err:=u.deleteKeyFromDB(userName);err!=nil{ + variable.ZapLog.Error("登出时删除密钥失败") + } return true } // 返回用户密码加密所需的公钥 @@ -321,10 +330,12 @@ func (u *UsersModel)PublicKey(userName string)[]byte{ // 不存在密钥,重新生成密钥 pubPEM, priPEM, err := rsa.GenerateRSAKeyPair() if err != nil { + variable.ZapLog.Error("不存在密钥且密钥生成失败") return nil } if err := u.storeKeyPair(userName, pubPEM, priPEM); err != nil { - return nil + variable.ZapLog.Error("密钥存储失败") + return nil } return pubPEM } @@ -342,12 +353,14 @@ func (u *UsersModel)PrivateKey(userName string)[]byte{ // return nil privateKey, err := u.getKeyFromDB(userName, "private_key") if err == nil { - priPEM := pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", - Bytes: []byte(privateKey), - }) - return priPEM + // priPEM := pem.EncodeToMemory(&pem.Block{ + // Type: "PRIVATE KEY", + // Bytes: []byte(privateKey), + // }) + // return priPEM + return []byte(privateKey) } + variable.ZapLog.Error("返回私钥失败") return nil } // func (u *UsersModel)GenerateKeyPair(userName string)[]byte{ diff --git a/GinSkeleton/app/utils/rsa/rsa.go b/GinSkeleton/app/utils/rsa/rsa.go index 7461d85..e1c3ef4 100644 --- a/GinSkeleton/app/utils/rsa/rsa.go +++ b/GinSkeleton/app/utils/rsa/rsa.go @@ -58,15 +58,11 @@ func parsePKCS1PrivateKey(block *pem.Block) (*rsa.PrivateKey, error) { } func parsePKCS8PrivateKey(block *pem.Block) (*rsa.PrivateKey, error) { - privateKeyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) + privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse PKCS8 private key: %v", err) } - privateKey, ok := privateKeyInterface.(*rsa.PrivateKey) - if !ok { - return nil, fmt.Errorf("parsed key is not an RSA private key") - } - return privateKey, nil + return privateKey.(*rsa.PrivateKey), nil } func ParsePrivateKeyFromPEM(pemKey []byte) (*rsa.PrivateKey, error) { diff --git a/GinSkeleton/database/db_demo_mysql.sql b/GinSkeleton/database/db_demo_mysql.sql index ec0e507..6a0f2df 100644 --- a/GinSkeleton/database/db_demo_mysql.sql +++ b/GinSkeleton/database/db_demo_mysql.sql @@ -61,8 +61,8 @@ UNIQUE KEY `unique_index` (`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) CREATE TABLE `tb_rsa_keypair` ( `user_name` VARCHAR(30) DEFAULT '' COMMENT '账号', - `public_key` VARCHAR(400) DEFAULT '' COMMENT '公钥', - `private_key` VARCHAR(400) DEFAULT '' COMMENT '私钥', + `public_key` VARCHAR(512) DEFAULT '' COMMENT '公钥', + `private_key` VARCHAR(2048) DEFAULT '' COMMENT '私钥', /* `pass` VARCHAR(128) DEFAULT '' COMMENT '密码', */ PRIMARY KEY (`user_name`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; diff --git a/GinSkeleton/routers/web.go b/GinSkeleton/routers/web.go index abeffdd..d2767de 100644 --- a/GinSkeleton/routers/web.go +++ b/GinSkeleton/routers/web.go @@ -3,6 +3,7 @@ package routers import ( "github.com/gin-contrib/pprof" "github.com/gin-gonic/gin" + // "go.uber.org/zap" "goskeleton/app/global/consts" "goskeleton/app/global/variable" @@ -14,117 +15,6 @@ import ( // "net/http" ) -// 该路由主要设置 后台管理系统等后端应用路由 - -// func InitWebRouter() *gin.Engine { -// var router *gin.Engine -// // 非调试模式(生产模式) 日志写到日志文件 -// if variable.ConfigYml.GetBool("AppDebug") == false { - -// //1.gin自行记录接口访问日志,不需要nginx,如果开启以下3行,那么请屏蔽第 34 行代码 -// //gin.DisableConsoleColor() -// //f, _ := os.Create(variable.BasePath + variable.ConfigYml.GetString("Logs.GinLogName")) -// //gin.DefaultWriter = io.MultiWriter(f) - -// //【生产模式】 -// // 根据 gin 官方的说明:[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. -// // 如果部署到生产环境,请使用以下模式: -// // 1.生产模式(release) 和开发模式的变化主要是禁用 gin 记录接口访问日志, -// // 2.go服务就必须使用nginx作为前置代理服务,这样也方便实现负载均衡 -// // 3.如果程序发生 panic 等异常使用自定义的 panic 恢复中间件拦截、记录到日志 -// router = gin_release.ReleaseRouter() - -// } else { -// // 调试模式,开启 pprof 包,便于开发阶段分析程序性能 -// router = gin.Default() -// pprof.Register(router) -// } - -// // 设置可信任的代理服务器列表,gin (2021-11-24发布的v1.7.7版本之后出的新功能) -// if variable.ConfigYml.GetInt("HttpServer.TrustProxies.IsOpen") == 1 { -// if err := router.SetTrustedProxies(variable.ConfigYml.GetStringSlice("HttpServer.TrustProxies.ProxyServerList")); err != nil { -// variable.ZapLog.Error(consts.GinSetTrustProxyError, zap.Error(err)) -// } -// } else { -// _ = router.SetTrustedProxies(nil) -// } - -// //根据配置进行设置跨域 -// if variable.ConfigYml.GetBool("HttpServer.AllowCrossDomain") { -// router.Use(cors.Next()) -// } - -// router.GET("/", func(context *gin.Context) { -// context.String(http.StatusOK, "HelloWorld,这是后端模块") -// }) - -// //处理静态资源(不建议gin框架处理静态资源,参见 public/readme.md 说明 ) -// router.Static("/public", "./public") // 定义静态资源路由与实际目录映射关系 -// router.StaticFS("/dir", http.Dir("./public")) // 将public目录内的文件列举展示 -// router.StaticFile("/abcd", "./public/readme.md") // 可以根据文件名绑定需要返回的文件名 - -// // 创建一个验证码路由 -// verifyCode := router.Group("captcha") -// { -// // 验证码业务,该业务无需专门校验参数,所以可以直接调用控制器 -// verifyCode.GET("/", (&captcha.Captcha{}).GenerateId) // 获取验证码ID -// verifyCode.GET("/:captcha_id", (&captcha.Captcha{}).GetImg) // 获取图像地址 -// verifyCode.GET("/:captcha_id/:captcha_value", (&captcha.Captcha{}).CheckCode) // 校验验证码 -// } -// // 创建一个后端接口路由组 -// backend := router.Group("/admin/") -// { -// // 创建一个websocket,如果ws需要账号密码登录才能使用,就写在需要鉴权的分组,这里暂定是开放式的,不需要严格鉴权,我们简单验证一下token值 -// backend.GET("ws", validatorFactory.Create(consts.ValidatorPrefix+"WebsocketConnect")) - -// // 【不需要token】中间件验证的路由 用户注册、登录 -// noAuth := backend.Group("users/") -// { -// // 关于路由的第二个参数用法说明 -// // 1.编写一个表单参数验证器结构体,参见代码: app/http/validator/web/users/register.go -// // 2.将以上表单参数验证器注册,遵守 键 =》值 格式注册即可 ,app/http/validator/common/register_validator/web_register_validator.go 20行就是注册时候的键 consts.ValidatorPrefix+"UsersRegister" -// // 3.按照注册时的键,直接从容器调用即可 :validatorFactory.Create(consts.ValidatorPrefix+"UsersRegister") -// noAuth.POST("register", validatorFactory.Create(consts.ValidatorPrefix+"UsersRegister")) -// // 不需要验证码即可登陆 -// noAuth.POST("login", validatorFactory.Create(consts.ValidatorPrefix+"UsersLogin")) - -// // 如果加载了验证码中间件,那么就需要提交验证码才可以登陆(本质上就是给登陆接口增加了2个参数:验证码id提交时的键:captcha_id 和 验证码值提交时的键 captcha_value,具体参见配置文件) -// //noAuth.Use(authorization.CheckCaptchaAuth()).POST("login", validatorFactory.Create(consts.ValidatorPrefix+"UsersLogin")) - -// } - -// // 刷新token -// refreshToken := backend.Group("users/") -// { -// // 刷新token,当过期的token在允许失效的延长时间范围内,用旧token换取新token -// refreshToken.Use(authorization.RefreshTokenConditionCheck()).POST("refreshtoken", validatorFactory.Create(consts.ValidatorPrefix+"RefreshToken")) -// } - -// // 【需要token】中间件验证的路由 -// backend.Use(authorization.CheckTokenAuth()) -// { -// // 用户组路由 -// users := backend.Group("users/") -// { -// // 查询 ,这里的验证器直接从容器获取,是因为程序启动时,将验证器注册在了容器,具体代码位置:App\Http\Validator\Web\Users\xxx -// users.GET("index", validatorFactory.Create(consts.ValidatorPrefix+"UsersShow")) -// // 新增 -// users.POST("create", validatorFactory.Create(consts.ValidatorPrefix+"UsersStore")) -// // 更新 -// users.POST("edit", validatorFactory.Create(consts.ValidatorPrefix+"UsersUpdate")) -// // 删除 -// users.POST("delete", validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy")) -// } -// //文件上传公共路由 -// uploadFiles := backend.Group("upload/") -// { -// uploadFiles.POST("files", validatorFactory.Create(consts.ValidatorPrefix+"UploadFiles")) -// } -// } -// } -// return router -// } - func InitWebRouter_Co() *gin.Engine { var router *gin.Engine = gin.Default() // 日志显示在控制台 @@ -160,9 +50,9 @@ func InitWebRouter_Co() *gin.Engine { // 如果加载了验证码中间件,那么就需要提交验证码才可以登陆(本质上就是给登陆接口增加了2个参数:验证码id提交时的键:captcha_id 和 验证码值提交时的键 captcha_value,具体参见配置文件) //noAuth.Use(authorization.CheckCaptchaAuth()).POST("login", validatorFactory.Create(consts.ValidatorPrefix+"UsersLogin")) - + // 获取公钥,用于密码等敏感信息的加密 - noAuth.POST("publickey",validatorFactory.Create(consts.ValidatorPrefix+"PublicKey")) + noAuth.POST("publickey", validatorFactory.Create(consts.ValidatorPrefix+"PublicKey")) } // 刷新token @@ -185,7 +75,7 @@ func InitWebRouter_Co() *gin.Engine { // users.POST("create", validatorFactory.Create(consts.ValidatorPrefix+"UsersStore")) // not need // 更新用户名或密码 users.POST("username", validatorFactory.Create(consts.ValidatorPrefix+"UsersNameUpdate")) - users.POST("password",validatorFactory.Create(consts.ValidatorPrefix+"UsersPasswordUpdate")) + users.POST("password", validatorFactory.Create(consts.ValidatorPrefix+"UsersPasswordUpdate")) // 注销用户 users.POST("delete", validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy")) // 退出登录 @@ -194,4 +84,4 @@ func InitWebRouter_Co() *gin.Engine { } } return router -} \ No newline at end of file +} diff --git a/GinSkeleton/test/login_test.html b/GinSkeleton/test/login_test.html new file mode 100644 index 0000000..1758d2d --- /dev/null +++ b/GinSkeleton/test/login_test.html @@ -0,0 +1,73 @@ + + + + + + Login + + + + +
+ + + +
+ + + + + \ No newline at end of file