diff --git a/link_homework/api/v1/homework/login.go b/link_homework/api/v1/homework/login.go new file mode 100644 index 0000000..ca2fd8f --- /dev/null +++ b/link_homework/api/v1/homework/login.go @@ -0,0 +1 @@ +package homework diff --git a/link_homework/go.mod b/link_homework/go.mod index 8321280..f524a4a 100644 --- a/link_homework/go.mod +++ b/link_homework/go.mod @@ -4,8 +4,9 @@ go 1.21.13 require ( github.com/gogf/gf/contrib/drivers/mysql/v2 v2.8.1 - github.com/gogf/gf/contrib/nosql/redis/v2 v2.8.2 - github.com/gogf/gf/v2 v2.8.2 + github.com/gogf/gf/contrib/nosql/redis/v2 v2.8.1 + github.com/gogf/gf/v2 v2.8.1 + github.com/golang-jwt/jwt/v4 v4.5.1 ) require ( diff --git a/link_homework/go.sum b/link_homework/go.sum index 5ee4499..45393c4 100644 --- a/link_homework/go.sum +++ b/link_homework/go.sum @@ -1,3 +1,4 @@ +github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= diff --git a/link_homework/internal/cmd/cmd.go b/link_homework/internal/cmd/cmd.go index d62a5e7..ea1f338 100644 --- a/link_homework/internal/cmd/cmd.go +++ b/link_homework/internal/cmd/cmd.go @@ -11,6 +11,8 @@ import ( "link_homework/internal/controller/live" "link_homework/internal/controller/record" "link_homework/internal/logic/middleware" + "link_homework/internal/controller/auth" + "link_homework/internal/logic/middleware" ) var ( @@ -21,23 +23,12 @@ var ( Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { s := g.Server() //后台 - - ////启动gtoken - //// 创建一个GfToken对象,用于处理用户登录、登出、权限验证等操作 - //gfToken := >oken.GfToken{ - // // 设置登录路径,即用户登录接口登入成功后会获得一个Token - // LoginPath: "/login", - // //// 设置登录前执行的函数,在用户登录之前会调用这个函数进行一些预处理,比如验证用户名和密码等。 - // //LoginBeforeFunc: loginFunc, //手动编写 没有同时配置登入路径,登入方法,登出路径启动时会报错 - // // 设置登出路径,即用户登出接口登入成功后会删除Token - // LogoutPath: "/user/logout", - // //// 设置需要拦截的路径,按照前缀拦截,所有以/user或/system开头的路径都需要进行Token认证。 - // //AuthPaths: g.SliceStr{"/user", "/system"}, - // //// 设置不需要拦截的路径,所有以/user/info或/system/user/开头的路径都不需要进行Token认证。 - // //AuthExcludePaths: g.SliceStr{"/user/info", "/system/user/*"}, - // //// 开启全局拦截,默认关闭,如果设置为true,则所有请求都会经过Token认证中间件,如果设置为false,则只有指定路径的请求会经过Token认证中间件。 - // //GlobalMiddleware: true, - //} + s.Group("/api", func(group *ghttp.RouterGroup) { + // 登录接口 + group.POST("/login", auth.NewLoginController().Login) + // 退出接口 + group.POST("/logout", auth.NewLoginController().Logout) + }) s.Group("/api/homework_manage", func(group *ghttp.RouterGroup) { group.Middleware(middleware.MiddlewareCORS) //group.Middleware(middleware.MiddlewareIsLogin) diff --git a/link_homework/internal/consts/consts.go b/link_homework/internal/consts/consts.go index d709a2b..6a1a295 100644 --- a/link_homework/internal/consts/consts.go +++ b/link_homework/internal/consts/consts.go @@ -1 +1,7 @@ package consts + +// Redis和其他配置常量 +const ( + URL_KEY = "jingwang:cms:env" // Redis中的键 + URL_HASH_KEY = "HLJW_BASE_URL" // Redis中的hashKey +) diff --git a/link_homework/internal/controller/auth/login.go b/link_homework/internal/controller/auth/login.go new file mode 100644 index 0000000..cb22fbb --- /dev/null +++ b/link_homework/internal/controller/auth/login.go @@ -0,0 +1,56 @@ +package auth + +import ( + "net/http" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "link_homework/internal/model/dto" + "link_homework/internal/service" +) + +type LoginController struct { +} + +func NewLoginController() *LoginController { + return &LoginController{} +} + +// 登录接口 + +func (c *LoginController) Login(r *ghttp.Request) { + var ( + username = r.Get("username").String() + password = r.Get("password").String() + ) + + token, err := service.LoginLogic().Login(r.Context(), username, password) + if err != nil { + r.Response.WriteJsonExit(dto.Error(err.Error())) + } + + r.Response.WriteJsonExit(dto.SuccessWithData(g.Map{ + "token": token, + })) +} + +// 退出接口 +func (c *LoginController) Logout(r *ghttp.Request) { + // 获取请求中的 context + ctx := r.Context() + + // 从请求头中获取 token + token := r.Header.Get("token") + if token == "" { + r.Response.WriteJsonExit(dto.ErrorWithCode(http.StatusUnauthorized, "Token 不能为空")) + } + + // 校验 Token 是否有效 + valid, err := service.LoginLogic().ValidateToken(ctx, token) + if err != nil || !valid { + r.Response.WriteJsonExit(dto.ErrorWithCode(http.StatusUnauthorized, "Token 无效")) + } + + // 返回登出成功 + r.Response.WriteJsonExit(dto.Success()) +} diff --git a/link_homework/internal/logic/logic.go b/link_homework/internal/logic/logic.go index 180fa90..8df460e 100644 --- a/link_homework/internal/logic/logic.go +++ b/link_homework/internal/logic/logic.go @@ -1,3 +1,7 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + package logic import ( @@ -6,4 +10,6 @@ import ( _ "link_homework/internal/logic/homework" _ "link_homework/internal/logic/live" _ "link_homework/internal/logic/record" + _ "link_homework/internal/logic/login" + _ "link_homework/internal/logic/middleware" ) diff --git a/link_homework/internal/logic/login/login.go b/link_homework/internal/logic/login/login.go new file mode 100644 index 0000000..4752a2a --- /dev/null +++ b/link_homework/internal/logic/login/login.go @@ -0,0 +1,76 @@ +package login + +import ( + "context" + "errors" + "time" + + "github.com/golang-jwt/jwt/v4" + "link_homework/internal/service" +) + +type sLoginLogic struct{} + +var ( + SecretKey = []byte("HomilyLink") // 用于签名和验证 JWT 的密钥 +) + +// 自定义声明结构 +type CustomClaims struct { + Username string `json:"username"` + jwt.RegisteredClaims +} + +func NewLoginLogic() *sLoginLogic { + return &sLoginLogic{} +} + +// 确保在初始化时注册该实现 +func init() { + service.RegisterLoginLogic(&sLoginLogic{}) +} + +// Login 方法实现用户登录并生成 Token +func (l *sLoginLogic) Login(ctx context.Context, username, password string) (string, error) { + if username != "admin" || password != "12345" { + return "", errors.New("用户名或密码错误") + } + + // 创建 JWT Token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "username": username, + "exp": time.Now().Add(time.Hour * 1).Unix(), // 1 小时后过期 + }) + + // 签名并获取完整的编码后的字符串 token + tokenString, err := token.SignedString(SecretKey) + if err != nil { + return "", err + } + + return tokenString, nil +} + +// ValidateToken 验证 Token 是否有效 +func (l *sLoginLogic) ValidateToken(ctx context.Context, tokenString string) (bool, error) { + // 解析 token + token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { + return SecretKey, nil + }) + + if err != nil { + return false, errors.New("Token 无效: " + err.Error()) + } + + // 如果 token 有效,返回 true + if token.Valid { + // 提取 CustomClaims 并打印 username + //if claims, ok := token.Claims.(*CustomClaims); ok { + // log.Printf("解析到的用户名: %s", claims.Username) // 打印解析到的用户名 + //} + return true, nil + } + + // 如果 token 无效 + return false, errors.New("Token 验证失败") +} diff --git a/link_homework/internal/logic/middleware/JWTMiddleware.go b/link_homework/internal/logic/middleware/JWTMiddleware.go new file mode 100644 index 0000000..8ffddad --- /dev/null +++ b/link_homework/internal/logic/middleware/JWTMiddleware.go @@ -0,0 +1,43 @@ +package middleware + +import ( + "link_homework/internal/logic/login" + "net/http" + "strings" + + "github.com/gogf/gf/v2/net/ghttp" + "github.com/golang-jwt/jwt/v4" +) + +// JWT 验证中间件 +func JWTMiddleware(r *ghttp.Request) { + // 从请求头中获取 token + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + r.Response.WriteStatusExit(http.StatusUnauthorized, "Authorization header missing") + } + + // 检查 token 前缀 + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + r.Response.WriteStatusExit(http.StatusUnauthorized, "Invalid Authorization header format") + } + + tokenString := parts[1] + + // 解析 token + token, err := jwt.ParseWithClaims(tokenString, &login.CustomClaims{}, func(token *jwt.Token) (interface{}, error) { + return login.SecretKey, nil + }) + + if err != nil || !token.Valid { + r.Response.WriteStatusExit(http.StatusUnauthorized, "Invalid token") + } + + // 将用户信息存储在上下文中 + if claims, ok := token.Claims.(*login.CustomClaims); ok { + r.SetCtxVar("username", claims.Username) + } + + r.Middleware.Next() +} diff --git a/link_homework/internal/service/login.go b/link_homework/internal/service/login.go new file mode 100644 index 0000000..e49b489 --- /dev/null +++ b/link_homework/internal/service/login.go @@ -0,0 +1,34 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" +) + +type ( + ILoginLogic interface { + // Login 方法实现用户登录并生成 Token + Login(ctx context.Context, username string, password string) (string, error) + // ValidateToken 验证 Token 是否有效 + ValidateToken(ctx context.Context, tokenString string) (bool, error) + } +) + +var ( + localLoginLogic ILoginLogic +) + +func LoginLogic() ILoginLogic { + if localLoginLogic == nil { + panic("implement not found for interface ILoginLogic, forgot register?") + } + return localLoginLogic +} + +func RegisterLoginLogic(i ILoginLogic) { + localLoginLogic = i +} diff --git a/link_homework/main.go b/link_homework/main.go index 135dbcb..da64cb4 100644 --- a/link_homework/main.go +++ b/link_homework/main.go @@ -1,13 +1,14 @@ package main import ( - _ "github.com/gogf/gf/contrib/drivers/mysql/v2" + "fmt" "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gctx" _ "link_homework/internal/packed" - _ "github.com/gogf/gf/contrib/nosql/redis/v2" + "github.com/gogf/gf/v2/os/gctx" + _ "link_homework/internal/packed" + _ "github.com/gogf/gf/contrib/drivers/mysql/v2" _ "github.com/gogf/gf/contrib/nosql/redis/v2" "link_homework/internal/cmd" @@ -20,7 +21,6 @@ func main() { //启动日志 g.Log().Info(ctx, "服务启动中") - //检查数据库链接 // 检查默认数据库链接 db := g.DB() @@ -38,6 +38,24 @@ func main() { } g.Log().Info(ctx, "CMS 数据库链接成功") + // 检查 Redis 配置并连接 + // 如果配置中存在 Redis 配置,初始化 Redis 客户端 + // 打印 Redis 配置,确保它加载正确 + redisConfig, err := g.Cfg().Get(ctx, "redis.default") + if err != nil { + fmt.Println("获取 Redis 配置失败:", err) + return + } + fmt.Printf("Redis 配置:%v\n", redisConfig) + + // 测试 Redis 连接 + _, err = g.Redis().Do(ctx, "PING") + if err != nil { + g.Log().Fatal(ctx, fmt.Sprintf("Redis 链接失败: %v", err)) + return + } + g.Log().Info(ctx, "Redis 链接成功") + //启动主命令逻辑 cmd.Main.Run(ctx) diff --git a/link_homework/utility/utility.go b/link_homework/utility/utility.go new file mode 100644 index 0000000..513bfc1 --- /dev/null +++ b/link_homework/utility/utility.go @@ -0,0 +1,115 @@ +package utility + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gctx" + "io/ioutil" + "link_homework/internal/consts" + "link_homework/internal/model/dto" + "net/http" + "strings" +) + +// 获取 URL +func getUrl(key, hashKey string) *dto.Result { + ctx := gctx.New() + + // 1. 从 Redis 获取 URL + redisUrl, err := g.Redis().Do(ctx, "HGET", key, hashKey) + if err == nil && redisUrl.String() != "" { + g.Log().Infof(ctx, "从 Redis 获取到 URL: %s", redisUrl.String()) + return dto.SuccessWithData(redisUrl.String()) + } + + // 2. 如果 Redis 中没有,查询数据库 + urlResult := selectBaseUrl(hashKey) + if urlResult.Code != 200 { + return urlResult // 数据库查询失败,返回错误 + } + url := urlResult.Data.(string) + + // 3. 将 URL 存入 Redis + if _, err = g.Redis().Do(ctx, "HSET", key, hashKey, url); err != nil { + g.Log().Warningf(ctx, "将数据存入 Redis 失败: %v", err) + } + + g.Log().Infof(ctx, "将 URL 存入 Redis: %s", url) + return dto.SuccessWithData(url) +} + +// 查询数据库中的 URL +func selectBaseUrl(hashKey string) *dto.Result { + ctx := gctx.New() + + // 查询数据库 + value, err := g.DB("cms").Model("env").Where("`key` = ?", hashKey).Value("value") + if err != nil { + g.Log().Errorf(ctx, "数据库查询失败, 错误: %v, key: %s", err, hashKey) + return dto.Error("数据库查询失败") + } + + if value.IsNil() || value.String() == "" { + g.Log().Errorf(ctx, "未找到对应数据, key: %s", hashKey) + return dto.Error("未找到对应数据") + } + + return dto.SuccessWithData(value.String()) +} + +// 获取 jwcode +func GetJwcodeJSON(token string) (string, error) { + // 1. 获取基础 URL + urlResult := getUrl(consts.URL_KEY, consts.URL_HASH_KEY) + if urlResult.Code != 200 { + return "", errors.New("获取基础 URL 失败: " + urlResult.Message) + } + baseUrl := urlResult.Data.(string) + + // 2. 拼接完整的 URL + url := baseUrl + "/api/v2/member/info" + requestBody := strings.NewReader(`{"token":"` + token + `"}`) + + // 3. 创建 HTTP 请求 + req, err := http.NewRequest("POST", url, requestBody) + if err != nil { + return "", fmt.Errorf("HTTP 请求创建失败: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + // 4. 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("HTTP 请求失败: %v", err) + } + defer resp.Body.Close() + + // 5. 检查 HTTP 状态码 + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP 状态码错误: %v", resp.Status) + } + + // 6. 读取并解析响应体 + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取响应失败: %v", err) + } + + // 7. 解析 JSON 数据 + var jsonResponse map[string]interface{} + if err = json.Unmarshal(body, &jsonResponse); err != nil { + return "", fmt.Errorf("解析 JSON 失败: %v", err) + } + + // 8. 提取并直接返回 jwcode + if data, ok := jsonResponse["data"].(map[string]interface{}); ok { + if jwcode, exists := data["jwcode"].(string); exists { + return jwcode, nil // 直接返回 jwcode + } + } + + return "", errors.New("响应体中没有 jwcode") +}