diff --git a/internal/app/system/controller/sys_login.go b/internal/app/system/controller/sys_login.go index 91ccc32..31448cd 100644 --- a/internal/app/system/controller/sys_login.go +++ b/internal/app/system/controller/sys_login.go @@ -9,6 +9,7 @@ package controller import ( "context" + "strings" "github.com/gogf/gf/v2/crypto/gmd5" "github.com/gogf/gf/v2/errors/gerror" @@ -20,6 +21,7 @@ import ( commonService "github.com/tiger1103/gfast/v3/internal/app/common/service" "github.com/tiger1103/gfast/v3/internal/app/system/model" "github.com/tiger1103/gfast/v3/internal/app/system/service" + "github.com/tiger1103/gfast/v3/library/geoallow" "github.com/tiger1103/gfast/v3/library/libUtils" ) @@ -38,6 +40,21 @@ func (c *loginController) Login(ctx context.Context, req *system.UserLoginReq) ( permissions []string menuList []*model.UserMenus ) + // 登录地区 IP 闸门: 非中国大陆(非内部)地址一律禁止登录。先于验证码/密码校验执行, 失败不泄露账号信息。 + ip := libUtils.GetRealClientIP(ctx) + userAgent := libUtils.GetUserAgent(ctx) + if err = loginCheckIPAllow(ctx, ip); err != nil { + // 保存登录失败的日志信息 + service.SysLoginLog().Invoke(gctx.New(), &model.LoginLogParams{ + Status: 0, + Username: req.Username, + Ip: ip, + UserAgent: userAgent, + Msg: err.Error(), + Module: "系统后台", + }) + return + } //判断验证码是否正确 debug := gmode.IsDevelop() if !debug { @@ -46,8 +63,6 @@ func (c *loginController) Login(ctx context.Context, req *system.UserLoginReq) ( return } } - ip := libUtils.GetClientIp(ctx) - userAgent := libUtils.GetUserAgent(ctx) user, err = service.SysUser().GetAdminUserByUsernamePassword(ctx, req) if err != nil { // 保存登录失败的日志信息 @@ -117,3 +132,27 @@ func (c *loginController) LoginOut(ctx context.Context, req *system.UserLoginOut err = service.GfToken().RemoveToken(ctx, service.GfToken().GetRequestToken(g.RequestFromCtx(ctx))) return } + +// loginCheckIPAllow 登录地区 IP 闸门判定。 +// +// 配置位于 config.yaml 的 loginIpFilter 段: +// - enabled: 总开关, 默认开启; 关闭则一律放行(紧急恢复手段) +// - allowIps: 显式放行 IP 列表(不受地区限制), 如办公网/跳板出口公网 IP +// - denyMsg: 拒绝时返回给前端的文案 +// +// 放行条件: 开关关闭 / 命中 allowIps / 内部·保留地址(环回、内网、链路本地等) / +// 中国大陆分配网段。境外及港澳台地址返回拒绝错误。 +func loginCheckIPAllow(ctx context.Context, ip string) error { + if !g.Cfg().MustGet(ctx, "loginIpFilter.enabled", true).Bool() { + return nil + } + for _, a := range g.Cfg().MustGet(ctx, "loginIpFilter.allowIps").Strings() { + if strings.TrimSpace(a) == ip { + return nil + } + } + if geoallow.Allow(ip) { + return nil + } + return gerror.New(g.Cfg().MustGet(ctx, "loginIpFilter.denyMsg", "当前网络环境不允许登录").String()) +} diff --git a/library/libUtils/utils.go b/library/libUtils/utils.go index a74a675..5009f62 100644 --- a/library/libUtils/utils.go +++ b/library/libUtils/utils.go @@ -56,6 +56,22 @@ func GetClientIp(ctx context.Context) string { return g.RequestFromCtx(ctx).GetClientIp() } +// GetRealClientIP 获取不可伪造的真实客户端IP。 +// 优先取 X-Real-IP(部署 nginx 以 $remote_addr 覆盖写入, 客户端无法伪造); +// 否则回退 RemoteAddr(TCP 对端)。 +// 不采信 X-Forwarded-For 的首个值: 其采用追加语义, 首段可由客户端注入, 用作 +// 登录地区闸门判据时会被伪造绕过。多跳代理部署下须保证最终前置 nginx 不把 +// X-Real-IP 覆盖成代理自身地址(单跳时即真实客户端)。 +func GetRealClientIP(ctx context.Context) string { + r := g.RequestFromCtx(ctx) + if ip := strings.TrimSpace(r.Header.Get("X-Real-IP")); ip != "" { + if net.ParseIP(ip) != nil { + return ip + } + } + return r.GetRemoteIp() +} + // GetUserAgent 获取user-agent func GetUserAgent(ctx context.Context) string { return ghttp.RequestFromCtx(ctx).Header.Get("User-Agent") diff --git a/manifest/config/config.yaml b/manifest/config/config.yaml index c6e3c8c..0920815 100644 --- a/manifest/config/config.yaml +++ b/manifest/config/config.yaml @@ -64,6 +64,12 @@ gfToken: excludePaths: - "/api/v1/system/login" +# 登录 IP 地区闸门: 开启后仅允许「中国大陆网段 / 内部·保留地址 / allowIps」的 IP 登录, +# 国外 IP 一律拒绝。enabled 置 false 可整体关闭(紧急恢复), 不影响已登录会话。 +loginIpFilter: + enabled: true + allowIps: [] # 显式放行 IP(不受地区限制), 如办公网/跳板出口公网 IP + denyMsg: "当前网络环境不允许登录" # Redis 配置示例 redis: