Merge pull request #342 from wsczx/dev

1.修复防爆策略用户登录成功后没有重置计数的Bug
This commit is contained in:
bjdgyc 2024-10-28 17:57:35 +08:00 committed by GitHub
commit 436b8e3129
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 484 additions and 466 deletions

391
server/admin/lockmanager.go Normal file
View File

@ -0,0 +1,391 @@
package admin
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/bjdgyc/anylink/base"
)
type LockInfo struct {
Description string `json:"description"` // 锁定原因
Username string `json:"username"` // 用户名
IP string `json:"ip"` // IP 地址
State *LockState `json:"state"` // 锁定状态信息
}
type LockState struct {
Locked bool `json:"locked"` // 是否锁定
FailureCount int `json:"attempts"` // 失败次数
LockTime time.Time `json:"lock_time"` // 锁定截止时间
LastAttempt time.Time `json:"lastAttempt"` // 最后一次尝试的时间
}
type IPWhitelists struct {
IP net.IP
CIDR *net.IPNet
}
type LockManager struct {
mu sync.Mutex
LoginStatus sync.Map // 登录状态
ipLocks map[string]*LockState // 全局IP锁定状态
userLocks map[string]*LockState // 全局用户锁定状态
ipUserLocks map[string]map[string]*LockState // 单用户IP锁定状态
ipWhitelists []IPWhitelists // 全局IP白名单包含IP地址和CIDR范围
cleanupTicker *time.Ticker
}
var lockmanager *LockManager
var once sync.Once
func GetLockManager() *LockManager {
once.Do(func() {
lockmanager = &LockManager{
LoginStatus: sync.Map{},
ipLocks: make(map[string]*LockState),
userLocks: make(map[string]*LockState),
ipUserLocks: make(map[string]map[string]*LockState),
ipWhitelists: make([]IPWhitelists, 0),
}
})
return lockmanager
}
const defaultGlobalLockStateExpirationTime = 3600
func InitLockManager() {
lm := GetLockManager()
if base.Cfg.AntiBruteForce {
if base.Cfg.GlobalLockStateExpirationTime <= 0 {
base.Cfg.GlobalLockStateExpirationTime = defaultGlobalLockStateExpirationTime
}
lm.StartCleanupTicker()
lm.InitIPWhitelist()
}
}
func GetLocksInfo(w http.ResponseWriter, r *http.Request) {
lm := GetLockManager()
locksInfo := lm.GetLocksInfo()
RespSucess(w, locksInfo)
}
func UnlockUser(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
lockinfo := LockInfo{}
if err := json.Unmarshal(body, &lockinfo); err != nil {
RespError(w, RespInternalErr, err)
return
}
if lockinfo.State == nil {
RespError(w, RespInternalErr, fmt.Errorf("未找到锁定用户!"))
return
}
lm := GetLockManager()
lm.mu.Lock()
defer lm.mu.Unlock()
lm.Unlock(lockinfo.State)
base.Info("解锁成功:", lockinfo.Description, lockinfo.Username, lockinfo.IP)
RespSucess(w, "解锁成功!")
}
func (lm *LockManager) GetLocksInfo() []LockInfo {
var locksInfo []LockInfo
lm.mu.Lock()
defer lm.mu.Unlock()
for ip, state := range lm.ipLocks {
if state.Locked {
info := LockInfo{
Description: "全局 IP 锁定",
Username: "",
IP: ip,
State: &LockState{
Locked: state.Locked,
FailureCount: state.FailureCount,
LockTime: state.LockTime,
LastAttempt: state.LastAttempt,
},
}
locksInfo = append(locksInfo, info)
}
}
for username, state := range lm.userLocks {
if state.Locked {
info := LockInfo{
Description: "全局用户锁定",
Username: username,
IP: "",
State: &LockState{
Locked: state.Locked,
FailureCount: state.FailureCount,
LockTime: state.LockTime,
LastAttempt: state.LastAttempt,
},
}
locksInfo = append(locksInfo, info)
}
}
for username, ipStates := range lm.ipUserLocks {
for ip, state := range ipStates {
if state.Locked {
info := LockInfo{
Description: "单用户 IP 锁定",
Username: username,
IP: ip,
State: &LockState{
Locked: state.Locked,
FailureCount: state.FailureCount,
LockTime: state.LockTime,
LastAttempt: state.LastAttempt,
},
}
locksInfo = append(locksInfo, info)
}
}
}
return locksInfo
}
// 初始化IP白名单
func (lm *LockManager) InitIPWhitelist() {
ipWhitelist := strings.Split(base.Cfg.IPWhitelist, ",")
for _, ipWhitelist := range ipWhitelist {
ipWhitelist = strings.TrimSpace(ipWhitelist)
if ipWhitelist == "" {
continue
}
_, ipNet, err := net.ParseCIDR(ipWhitelist)
if err == nil {
lm.ipWhitelists = append(lm.ipWhitelists, IPWhitelists{CIDR: ipNet})
continue
}
ip := net.ParseIP(ipWhitelist)
if ip != nil {
lm.ipWhitelists = append(lm.ipWhitelists, IPWhitelists{IP: ip})
continue
}
}
}
// 检查 IP 是否在白名单中
func (lm *LockManager) IsWhitelisted(ip string) bool {
clientIP := net.ParseIP(ip)
if clientIP == nil {
return false
}
for _, ipWhitelist := range lm.ipWhitelists {
if ipWhitelist.CIDR != nil && ipWhitelist.CIDR.Contains(clientIP) {
return true
}
if ipWhitelist.IP != nil && ipWhitelist.IP.Equal(clientIP) {
return true
}
}
return false
}
func (lm *LockManager) StartCleanupTicker() {
lm.cleanupTicker = time.NewTicker(5 * time.Minute)
go func() {
for range lm.cleanupTicker.C {
lm.CleanupExpiredLocks()
}
}()
}
// 定期清理过期的锁定
func (lm *LockManager) CleanupExpiredLocks() {
now := time.Now()
lm.mu.Lock()
defer lm.mu.Unlock()
for ip, state := range lm.ipLocks {
if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalLockStateExpirationTime)*time.Second {
delete(lm.ipLocks, ip)
}
}
for user, state := range lm.userLocks {
if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalLockStateExpirationTime)*time.Second {
delete(lm.userLocks, user)
}
}
for user, ipMap := range lm.ipUserLocks {
for ip, state := range ipMap {
if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalLockStateExpirationTime)*time.Second {
delete(ipMap, ip)
if len(ipMap) == 0 {
delete(lm.ipUserLocks, user)
}
}
}
}
}
// 检查全局 IP 锁定
func (lm *LockManager) CheckGlobalIPLock(ip string, now time.Time) bool {
lm.mu.Lock()
defer lm.mu.Unlock()
state, exists := lm.ipLocks[ip]
if !exists {
return false
}
return lm.CheckLockState(state, now, base.Cfg.GlobalIPBanResetTime)
}
// 检查全局用户锁定
func (lm *LockManager) CheckGlobalUserLock(username string, now time.Time) bool {
// 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求····
if username == "" {
return false
}
lm.mu.Lock()
defer lm.mu.Unlock()
state, exists := lm.userLocks[username]
if !exists {
return false
}
return lm.CheckLockState(state, now, base.Cfg.GlobalUserBanResetTime)
}
// 检查单个用户的 IP 锁定
func (lm *LockManager) CheckUserIPLock(username, ip string, now time.Time) bool {
// 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求····
if username == "" {
return false
}
lm.mu.Lock()
defer lm.mu.Unlock()
userIPMap, userExists := lm.ipUserLocks[username]
if !userExists {
return false
}
state, ipExists := userIPMap[ip]
if !ipExists {
return false
}
return lm.CheckLockState(state, now, base.Cfg.BanResetTime)
}
// 更新全局 IP 锁定状态
func (lm *LockManager) UpdateGlobalIPLock(ip string, now time.Time, success bool) {
lm.mu.Lock()
defer lm.mu.Unlock()
state, exists := lm.ipLocks[ip]
if !exists {
state = &LockState{}
lm.ipLocks[ip] = state
}
lm.UpdateLockState(state, now, success, base.Cfg.MaxGlobalIPBanCount, base.Cfg.GlobalIPLockTime)
}
// 更新全局用户锁定状态
func (lm *LockManager) UpdateGlobalUserLock(username string, now time.Time, success bool) {
// 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求····
if username == "" {
return
}
lm.mu.Lock()
defer lm.mu.Unlock()
state, exists := lm.userLocks[username]
if !exists {
state = &LockState{}
lm.userLocks[username] = state
}
lm.UpdateLockState(state, now, success, base.Cfg.MaxGlobalUserBanCount, base.Cfg.GlobalUserLockTime)
}
// 更新单个用户的 IP 锁定状态
func (lm *LockManager) UpdateUserIPLock(username, ip string, now time.Time, success bool) {
// 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求····
if username == "" {
return
}
lm.mu.Lock()
defer lm.mu.Unlock()
userIPMap, userExists := lm.ipUserLocks[username]
if !userExists {
userIPMap = make(map[string]*LockState)
lm.ipUserLocks[username] = userIPMap
}
state, ipExists := userIPMap[ip]
if !ipExists {
state = &LockState{}
userIPMap[ip] = state
}
lm.UpdateLockState(state, now, success, base.Cfg.MaxBanCount, base.Cfg.LockTime)
}
// 更新锁定状态
func (lm *LockManager) UpdateLockState(state *LockState, now time.Time, success bool, maxBanCount, lockTime int) {
if success {
lm.Unlock(state) // 成功登录后解锁
} else {
state.FailureCount++
if state.FailureCount >= maxBanCount {
state.LockTime = now.Add(time.Duration(lockTime) * time.Second)
state.Locked = true // 超过阈值时锁定
}
}
state.LastAttempt = now
}
// 检查锁定状态
func (lm *LockManager) CheckLockState(state *LockState, now time.Time, resetTime int) bool {
if state == nil || state.LastAttempt.IsZero() {
return false
}
// 如果超过锁定时间,重置锁定状态
if !state.LockTime.IsZero() && now.After(state.LockTime) {
lm.Unlock(state) // 锁定期过后解锁
return false
}
// 如果超过窗口时间,重置失败计数
if now.Sub(state.LastAttempt) > time.Duration(resetTime)*time.Second {
state.FailureCount = 0
return false
}
return state.Locked
}
// 解锁
func (lm *LockManager) Unlock(state *LockState) {
state.FailureCount = 0
state.LockTime = time.Time{}
state.Locked = false
}

View File

@ -87,6 +87,7 @@ func StartAdmin() {
r.HandleFunc("/group/auth_login", GroupAuthLogin) r.HandleFunc("/group/auth_login", GroupAuthLogin)
r.HandleFunc("/statsinfo/list", StatsInfoList) r.HandleFunc("/statsinfo/list", StatsInfoList)
r.HandleFunc("/locksinfo/list", GetLocksInfo)
// pprof // pprof
if base.Cfg.Pprof { if base.Cfg.Pprof {

View File

@ -203,7 +203,8 @@ const accountMail = `<p>您好:</p>
<ul> <ul>
<li>请使用OTP软件扫描动态码二维码</li> <li>请使用OTP软件扫描动态码二维码</li>
<li>然后使用anyconnect客户端进行登陆</li> <li>然后使用anyconnect客户端进行登陆</li>
<li>登陆密码为 PIN码+动态码(中间没有+)</li> <li>登陆密码为 PIN </li>
<li>OTP密码为扫码后生成的动态码</li>
</ul> </ul>
</div> </div>
<p> <p>

View File

@ -6,28 +6,15 @@ import (
"net" "net"
"net/http" "net/http"
"strings" "strings"
"sync"
"time" "time"
"github.com/bjdgyc/anylink/admin"
"github.com/bjdgyc/anylink/base" "github.com/bjdgyc/anylink/base"
) )
// 自定义 contextKey 类型,避免键冲突 var lockManager = admin.GetLockManager()
type contextKey string
// 定义常量作为上下文的键 const loginStatusKey = "login_status"
const loginStatusKey contextKey = "login_status"
const defaultGlobalLockStateExpirationTime = 3600
func initAntiBruteForce() {
if base.Cfg.AntiBruteForce {
if base.Cfg.GlobalLockStateExpirationTime <= 0 {
base.Cfg.GlobalLockStateExpirationTime = defaultGlobalLockStateExpirationTime
}
lockManager.startCleanupTicker()
lockManager.initIPWhitelist()
}
}
// 防爆破中间件 // 防爆破中间件
func antiBruteForce(next http.Handler) http.Handler { func antiBruteForce(next http.Handler) http.Handler {
@ -53,6 +40,20 @@ func antiBruteForce(next http.Handler) http.Handler {
} }
username := cr.Auth.Username username := cr.Auth.Username
if r.URL.Path == "/otp-verification" {
sessionID, err := GetCookie(r, "auth-session-id")
if err != nil {
http.Error(w, "Invalid session, please login again", http.StatusUnauthorized)
return
}
sessionData, err := SessStore.GetAuthSession(sessionID)
if err != nil {
http.Error(w, "Invalid session, please login again", http.StatusUnauthorized)
return
}
username = sessionData.ClientRequest.Auth.Username
}
ip, _, err := net.SplitHostPort(r.RemoteAddr) // 提取纯 IP 地址,去掉端口号 ip, _, err := net.SplitHostPort(r.RemoteAddr) // 提取纯 IP 地址,去掉端口号
if err != nil { if err != nil {
http.Error(w, "Unable to parse IP address", http.StatusInternalServerError) http.Error(w, "Unable to parse IP address", http.StatusInternalServerError)
@ -61,43 +62,28 @@ func antiBruteForce(next http.Handler) http.Handler {
now := time.Now() now := time.Now()
// 检查IP是否在白名单中 // 检查IP是否在白名单中
if lockManager.isWhitelisted(ip) { if lockManager.IsWhitelisted(ip) {
r.Body = io.NopCloser(strings.NewReader(string(body))) r.Body = io.NopCloser(strings.NewReader(string(body)))
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
// // 速率限制
// lockManager.mu.RLock()
// limiter, exists := lockManager.rateLimiter[ip]
// if !exists {
// limiter = rate.NewLimiter(rate.Limit(base.Cfg.RateLimit), base.Cfg.Burst)
// lockManager.rateLimiter[ip] = limiter
// }
// lockManager.mu.RUnlock()
// if !limiter.Allow() {
// log.Printf("Rate limit exceeded for IP %s. Try again later.", ip)
// http.Error(w, "Rate limit exceeded. Try again later.", http.StatusTooManyRequests)
// return
// }
// 检查全局 IP 锁定 // 检查全局 IP 锁定
if base.Cfg.MaxGlobalIPBanCount > 0 && lockManager.checkGlobalIPLock(ip, now) { if base.Cfg.MaxGlobalIPBanCount > 0 && lockManager.CheckGlobalIPLock(ip, now) {
base.Warn("IP", ip, "is globally locked. Try again later.") base.Warn("IP", ip, "is globally locked. Try again later.")
http.Error(w, "Account globally locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests) http.Error(w, "Account globally locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests)
return return
} }
// 检查全局用户锁定 // 检查全局用户锁定
if base.Cfg.MaxGlobalUserBanCount > 0 && lockManager.checkGlobalUserLock(username, now) { if base.Cfg.MaxGlobalUserBanCount > 0 && lockManager.CheckGlobalUserLock(username, now) {
base.Warn("User", username, "is globally locked. Try again later.") base.Warn("User", username, "is globally locked. Try again later.")
http.Error(w, "Account globally locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests) http.Error(w, "Account globally locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests)
return return
} }
// 检查单个用户的 IP 锁定 // 检查单个用户的 IP 锁定
if base.Cfg.MaxBanCount > 0 && lockManager.checkUserIPLock(username, ip, now) { if base.Cfg.MaxBanCount > 0 && lockManager.CheckUserIPLock(username, ip, now) {
base.Warn("IP", ip, "is locked for user", username, "Try again later.") base.Warn("IP", ip, "is locked for user", username, "Try again later.")
http.Error(w, "Account locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests) http.Error(w, "Account locked due to too many failed attempts. Try again later.", http.StatusTooManyRequests)
return return
@ -109,296 +95,16 @@ func antiBruteForce(next http.Handler) http.Handler {
// 调用下一个处理器 // 调用下一个处理器
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
// 从 context 中获取登录状态 // 检查登录状态
loginStatus, _ := r.Context().Value(loginStatusKey).(bool) Status, _ := lockManager.LoginStatus.Load(loginStatusKey)
loginStatus, _ := Status.(bool)
// 更新用户登录状态 // 更新用户登录状态
lockManager.updateGlobalIPLock(ip, now, loginStatus) lockManager.UpdateGlobalIPLock(ip, now, loginStatus)
lockManager.updateGlobalUserLock(username, now, loginStatus) lockManager.UpdateGlobalUserLock(username, now, loginStatus)
lockManager.updateUserIPLock(username, ip, now, loginStatus) lockManager.UpdateUserIPLock(username, ip, now, loginStatus)
// 清除登录状态
lockManager.LoginStatus.Delete(loginStatusKey)
}) })
} }
type LockState struct {
FailureCount int
LockTime time.Time
LastAttempt time.Time
}
type IPWhitelists struct {
IP net.IP
CIDR *net.IPNet
}
type LockManager struct {
mu sync.Mutex
ipLocks map[string]*LockState // 全局IP锁定状态
userLocks map[string]*LockState // 全局用户锁定状态
ipUserLocks map[string]map[string]*LockState // 单用户IP锁定状态
ipWhitelists []IPWhitelists // 全局IP白名单包含IP地址和CIDR范围
// rateLimiter map[string]*rate.Limiter // 速率限制器
cleanupTicker *time.Ticker
}
var lockManager = &LockManager{
ipLocks: make(map[string]*LockState),
userLocks: make(map[string]*LockState),
ipUserLocks: make(map[string]map[string]*LockState),
ipWhitelists: make([]IPWhitelists, 0),
// rateLimiter: make(map[string]*rate.Limiter),
}
// 初始化IP白名单
func (lm *LockManager) initIPWhitelist() {
ipWhitelist := strings.Split(base.Cfg.IPWhitelist, ",")
for _, ipWhitelist := range ipWhitelist {
ipWhitelist = strings.TrimSpace(ipWhitelist)
if ipWhitelist == "" {
continue
}
_, ipNet, err := net.ParseCIDR(ipWhitelist)
if err == nil {
lm.ipWhitelists = append(lm.ipWhitelists, IPWhitelists{CIDR: ipNet})
continue
}
ip := net.ParseIP(ipWhitelist)
if ip != nil {
lm.ipWhitelists = append(lm.ipWhitelists, IPWhitelists{IP: ip})
continue
}
}
}
// 检查 IP 是否在白名单中
func (lm *LockManager) isWhitelisted(ip string) bool {
clientIP := net.ParseIP(ip)
if clientIP == nil {
return false
}
for _, ipWhitelist := range lm.ipWhitelists {
if ipWhitelist.CIDR != nil && ipWhitelist.CIDR.Contains(clientIP) {
return true
}
if ipWhitelist.IP != nil && ipWhitelist.IP.Equal(clientIP) {
return true
}
}
return false
}
func (lm *LockManager) startCleanupTicker() {
lm.cleanupTicker = time.NewTicker(5 * time.Minute)
go func() {
for range lm.cleanupTicker.C {
lm.cleanupExpiredLocks()
}
}()
}
// 定期清理过期的锁定
func (lm *LockManager) cleanupExpiredLocks() {
now := time.Now()
var ipKeys, userKeys []string
var IPuserKeys []struct{ user, ip string }
lm.mu.Lock()
for ip, state := range lm.ipLocks {
if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalLockStateExpirationTime)*time.Second {
ipKeys = append(ipKeys, ip)
}
}
for user, state := range lm.userLocks {
if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalLockStateExpirationTime)*time.Second {
userKeys = append(userKeys, user)
}
}
for user, ipMap := range lm.ipUserLocks {
for ip, state := range ipMap {
if now.Sub(state.LastAttempt) > time.Duration(base.Cfg.GlobalLockStateExpirationTime)*time.Second {
IPuserKeys = append(IPuserKeys, struct{ user, ip string }{user, ip})
}
}
}
lm.mu.Unlock()
lm.mu.Lock()
for _, ip := range ipKeys {
delete(lm.ipLocks, ip)
}
for _, user := range userKeys {
delete(lm.userLocks, user)
}
for _, key := range IPuserKeys {
delete(lm.ipUserLocks[key.user], key.ip)
if len(lm.ipUserLocks[key.user]) == 0 {
delete(lm.ipUserLocks, key.user)
}
}
lm.mu.Unlock()
}
// 检查全局 IP 锁定
func (lm *LockManager) checkGlobalIPLock(ip string, now time.Time) bool {
lm.mu.Lock()
defer lm.mu.Unlock()
state, exists := lm.ipLocks[ip]
if !exists {
return false
}
// 如果超过时间窗口,重置失败计数
lm.resetLockStateIfExpired(state, now, base.Cfg.GlobalIPBanResetTime)
if !state.LockTime.IsZero() && now.Before(state.LockTime) {
return true
}
return false
}
// 检查全局用户锁定
func (lm *LockManager) checkGlobalUserLock(username string, now time.Time) bool {
// 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求····
if username == "" {
return false
}
lm.mu.Lock()
defer lm.mu.Unlock()
state, exists := lm.userLocks[username]
if !exists {
return false
}
// 如果超过时间窗口,重置失败计数
lm.resetLockStateIfExpired(state, now, base.Cfg.GlobalUserBanResetTime)
if !state.LockTime.IsZero() && now.Before(state.LockTime) {
return true
}
return false
}
// 检查单个用户的 IP 锁定
func (lm *LockManager) checkUserIPLock(username, ip string, now time.Time) bool {
// 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求····
if username == "" {
return false
}
lm.mu.Lock()
defer lm.mu.Unlock()
userIPMap, userExists := lm.ipUserLocks[username]
if !userExists {
return false
}
state, ipExists := userIPMap[ip]
if !ipExists {
return false
}
// 如果超过时间窗口,重置失败计数
lm.resetLockStateIfExpired(state, now, base.Cfg.BanResetTime)
if !state.LockTime.IsZero() && now.Before(state.LockTime) {
return true
}
return false
}
// 更新全局 IP 锁定状态
func (lm *LockManager) updateGlobalIPLock(ip string, now time.Time, success bool) {
lm.mu.Lock()
defer lm.mu.Unlock()
state, exists := lm.ipLocks[ip]
if !exists {
state = &LockState{}
lm.ipLocks[ip] = state
}
lm.updateLockState(state, now, success, base.Cfg.MaxGlobalIPBanCount, base.Cfg.GlobalIPLockTime)
}
// 更新全局用户锁定状态
func (lm *LockManager) updateGlobalUserLock(username string, now time.Time, success bool) {
// 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求····
if username == "" {
return
}
lm.mu.Lock()
defer lm.mu.Unlock()
state, exists := lm.userLocks[username]
if !exists {
state = &LockState{}
lm.userLocks[username] = state
}
lm.updateLockState(state, now, success, base.Cfg.MaxGlobalUserBanCount, base.Cfg.GlobalUserLockTime)
}
// 更新单个用户的 IP 锁定状态
func (lm *LockManager) updateUserIPLock(username, ip string, now time.Time, success bool) {
// 我也不知道为什么cisco anyconnect每次连接会先传一个空用户请求····
if username == "" {
return
}
lm.mu.Lock()
defer lm.mu.Unlock()
userIPMap, userExists := lm.ipUserLocks[username]
if !userExists {
userIPMap = make(map[string]*LockState)
lm.ipUserLocks[username] = userIPMap
}
state, ipExists := userIPMap[ip]
if !ipExists {
state = &LockState{}
userIPMap[ip] = state
}
lm.updateLockState(state, now, success, base.Cfg.MaxBanCount, base.Cfg.LockTime)
}
// 更新锁定状态
func (lm *LockManager) updateLockState(state *LockState, now time.Time, success bool, maxBanCount, lockTime int) {
if success {
state.FailureCount = 0
state.LockTime = time.Time{}
} else {
state.FailureCount++
if state.FailureCount >= maxBanCount {
state.LockTime = now.Add(time.Duration(lockTime) * time.Second)
}
}
state.LastAttempt = now
}
// 超过窗口时间和锁定时间时重置锁定状态
func (lm *LockManager) resetLockStateIfExpired(state *LockState, now time.Time, resetTime int) {
if state == nil || state.LastAttempt.IsZero() {
return
}
// 如果超过锁定时间,重置锁定状态
if !state.LockTime.IsZero() && now.After(state.LockTime) {
state.FailureCount = 0
state.LockTime = time.Time{}
return
}
// 如果超过窗口时间,重置失败计数
if now.Sub(state.LastAttempt) > time.Duration(resetTime)*time.Second {
state.FailureCount = 0
state.LockTime = time.Time{}
}
}

View File

@ -2,7 +2,6 @@ package handler
import ( import (
"bytes" "bytes"
"context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
@ -95,7 +94,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
// TODO 用户密码校验 // TODO 用户密码校验
err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect) err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect)
if err != nil { if err != nil {
r = r.WithContext(context.WithValue(r.Context(), loginStatusKey, false)) // 传递登录失败状态 lockManager.LoginStatus.Store(loginStatusKey, false) // 记录登录失败状态
base.Warn(err, r.RemoteAddr) base.Warn(err, r.RemoteAddr)
ua.Info = err.Error() ua.Info = err.Error()
ua.Status = dbdata.UserAuthFail ua.Status = dbdata.UserAuthFail
@ -109,7 +108,6 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
tplRequest(tpl_request, w, data) tplRequest(tpl_request, w, data)
return return
} }
r = r.WithContext(context.WithValue(r.Context(), loginStatusKey, true)) // 传递登录成功状态
dbdata.UserActLogIns.Add(*ua, userAgent) dbdata.UserActLogIns.Add(*ua, userAgent)
v := &dbdata.User{} v := &dbdata.User{}
@ -121,6 +119,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
} }
// 用户otp验证 // 用户otp验证
if !v.DisableOtp { if !v.DisableOtp {
lockManager.LoginStatus.Store(loginStatusKey, true) // 重置OTP验证计数
sessionID, err := GenerateSessionID() sessionID, err := GenerateSessionID()
if err != nil { if err != nil {
base.Error("Failed to generate session ID: ", err) base.Error("Failed to generate session ID: ", err)

View File

@ -8,7 +8,6 @@ import (
"net" "net"
"net/http" "net/http"
"sync" "sync"
"sync/atomic"
"github.com/bjdgyc/anylink/base" "github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata" "github.com/bjdgyc/anylink/dbdata"
@ -18,12 +17,12 @@ import (
var SessStore = NewSessionStore() var SessStore = NewSessionStore()
const maxOtpErrCount = 3 // const maxOtpErrCount = 3
type AuthSession struct { type AuthSession struct {
ClientRequest *ClientRequest ClientRequest *ClientRequest
UserActLog *dbdata.UserActLog UserActLog *dbdata.UserActLog
OtpErrCount atomic.Uint32 // otp错误次数 // OtpErrCount atomic.Uint32 // otp错误次数
} }
// 存储临时会话信息 // 存储临时会话信息
@ -62,10 +61,10 @@ func (s *SessionStore) DeleteAuthSession(sessionID string) {
delete(s.session, sessionID) delete(s.session, sessionID)
} }
func (a *AuthSession) AddOtpErrCount(i int) int { // func (a *AuthSession) AddOtpErrCount(i int) int {
newI := a.OtpErrCount.Add(uint32(i)) // newI := a.OtpErrCount.Add(uint32(i))
return int(newI) // return int(newI)
} // }
func GenerateSessionID() (string, error) { func GenerateSessionID() (string, error) {
sessionID := utils.RandomRunes(32) sessionID := utils.RandomRunes(32)
@ -110,6 +109,8 @@ func DeleteCookie(w http.ResponseWriter, name string) {
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
} }
func CreateSession(w http.ResponseWriter, r *http.Request, authSession *AuthSession) { func CreateSession(w http.ResponseWriter, r *http.Request, authSession *AuthSession) {
lockManager.LoginStatus.Store(loginStatusKey, true) // 更新登录成功状态
cr := authSession.ClientRequest cr := authSession.ClientRequest
ua := authSession.UserActLog ua := authSession.UserActLog
@ -195,11 +196,12 @@ func LinkAuth_otp(w http.ResponseWriter, r *http.Request) {
// 动态码错误 // 动态码错误
if !dbdata.CheckOtp(username, otp, otpSecret) { if !dbdata.CheckOtp(username, otp, otpSecret) {
if sessionData.AddOtpErrCount(1) > maxOtpErrCount { // if sessionData.AddOtpErrCount(1) > maxOtpErrCount {
SessStore.DeleteAuthSession(sessionID) // SessStore.DeleteAuthSession(sessionID)
http.Error(w, "TooManyError, please login again", http.StatusBadRequest) // http.Error(w, "TooManyError, please login again", http.StatusBadRequest)
return // return
} // }
lockManager.LoginStatus.Store(loginStatusKey, false) // 记录登录失败状态
base.Warn("OTP 动态码错误", username, r.RemoteAddr) base.Warn("OTP 动态码错误", username, r.RemoteAddr)
ua.Info = "OTP 动态码错误" ua.Info = "OTP 动态码错误"

View File

@ -114,7 +114,7 @@ func initRoute() http.Handler {
r.Handle("/", antiBruteForce(http.HandlerFunc(LinkAuth))).Methods(http.MethodPost) r.Handle("/", antiBruteForce(http.HandlerFunc(LinkAuth))).Methods(http.MethodPost)
r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect) r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect)
r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet) r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet)
r.HandleFunc("/otp-verification", LinkAuth_otp) r.Handle("/otp-verification", antiBruteForce(http.HandlerFunc(LinkAuth_otp))).Methods(http.MethodPost)
r.HandleFunc(fmt.Sprintf("/profile_%s.xml", base.Cfg.ProfileName), func(w http.ResponseWriter, r *http.Request) { r.HandleFunc(fmt.Sprintf("/profile_%s.xml", base.Cfg.ProfileName), func(w http.ResponseWriter, r *http.Request) {
b, _ := os.ReadFile(base.Cfg.Profile) b, _ := os.ReadFile(base.Cfg.Profile)
w.Write(b) w.Write(b)

View File

@ -17,7 +17,7 @@ func Start() {
sessdata.Start() sessdata.Start()
cron.Start() cron.Start()
initAntiBruteForce() //初始化防爆破定时器和IP白名单 admin.InitLockManager() //初始化防爆破定时器和IP白名单
// 开启服务器转发 // 开启服务器转发
err := execCmd([]string{"sysctl -w net.ipv4.ip_forward=1"}) err := execCmd([]string{"sysctl -w net.ipv4.ip_forward=1"})

View File

@ -3,22 +3,13 @@
<el-card> <el-card>
<el-form :inline="true"> <el-form :inline="true">
<el-form-item> <el-form-item>
<el-button <el-button size="small" type="primary" icon="el-icon-plus" @click="handleEdit('')">添加
size="small"
type="primary"
icon="el-icon-plus"
@click="handleEdit('')">添加
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-dropdown size="small" placement="bottom"> <el-dropdown size="small" placement="bottom">
<el-upload <el-upload class="uploaduser" action="uploaduser" accept=".xlsx, .xls" :http-request="upLoadUser" :limit="1"
class="uploaduser" :show-file-list="false">
action="uploaduser"
accept=".xlsx, .xls"
:http-request="upLoadUser"
:limit="1"
:show-file-list="false">
<el-button size="small" icon="el-icon-upload2" type="primary">批量添加</el-button> <el-button size="small" icon="el-icon-upload2" type="primary">批量添加</el-button>
</el-upload> </el-upload>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
@ -32,79 +23,45 @@
</el-form-item> </el-form-item>
<el-form-item label="用户名或姓名或邮箱:"> <el-form-item label="用户名或姓名或邮箱:">
<el-input size="small" v-model="searchData" placeholder="请输入内容" <el-input size="small" v-model="searchData" placeholder="请输入内容"
@keydown.enter.native="searchEnterFun"></el-input> @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button <el-button size="small" type="primary" icon="el-icon-search" @click="handleSearch()">搜索
size="small"
type="primary"
icon="el-icon-search"
@click="handleSearch()">搜索
</el-button> </el-button>
<el-button <el-button size="small" icon="el-icon-refresh" @click="reset">重置搜索
size="small"
icon="el-icon-refresh"
@click="reset">重置搜索
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table <el-table ref="multipleTable" :data="tableData" border>
ref="multipleTable"
:data="tableData"
border>
<el-table-column <el-table-column sortable="true" prop="id" label="ID" width="60">
sortable="true"
prop="id"
label="ID"
width="60">
</el-table-column> </el-table-column>
<el-table-column <el-table-column prop="username" label="用户名" width="150">
prop="username"
label="用户名"
width="150">
</el-table-column> </el-table-column>
<el-table-column <el-table-column prop="nickname" label="姓名" width="100">
prop="nickname"
label="姓名"
width="100">
</el-table-column> </el-table-column>
<el-table-column <el-table-column prop="email" label="邮箱">
prop="email"
label="邮箱">
</el-table-column> </el-table-column>
<el-table-column <el-table-column prop="otp_secret" label="OTP密钥" width="110">
prop="otp_secret"
label="OTP密钥"
width="110">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <el-button v-if="!scope.row.disable_otp" type="text" icon="el-icon-view" @click="getOtpImg(scope.row)">
v-if="!scope.row.disable_otp"
type="text"
icon="el-icon-view"
@click="getOtpImg(scope.row)">
{{ scope.row.otp_secret.substring(0, 6) }} {{ scope.row.otp_secret.substring(0, 6) }}
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column prop="groups" label="用户组">
prop="groups"
label="用户组">
<template slot-scope="scope"> <template slot-scope="scope">
<el-row v-for="item in scope.row.groups" :key="item">{{ item }}</el-row> <el-row v-for="item in scope.row.groups" :key="item">{{ item }}</el-row>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column prop="status" label="状态" width="70">
prop="status"
label="状态"
width="70">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag v-if="scope.row.status === 1" type="success">可用</el-tag> <el-tag v-if="scope.row.status === 1" type="success">可用</el-tag>
<el-tag v-if="scope.row.status === 0" type="danger">停用</el-tag> <el-tag v-if="scope.row.status === 0" type="danger">停用</el-tag>
@ -112,20 +69,12 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column prop="updated_at" label="更新时间" :formatter="tableDateFormat">
prop="updated_at"
label="更新时间"
:formatter="tableDateFormat">
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="操作" width="210">
label="操作"
width="210">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <el-button size="mini" type="primary" @click="handleEdit(scope.row)">编辑
size="mini"
type="primary"
@click="handleEdit(scope.row)">编辑
</el-button> </el-button>
<!-- <el-popconfirm <!-- <el-popconfirm
@ -139,14 +88,8 @@
</el-button> </el-button>
</el-popconfirm>--> </el-popconfirm>-->
<el-popconfirm <el-popconfirm class="m-left-10" @confirm="handleDel(scope.row)" title="确定要删除用户吗?">
class="m-left-10" <el-button slot="reference" size="mini" type="danger">删除
@confirm="handleDel(scope.row)"
title="确定要删除用户吗?">
<el-button
slot="reference"
size="mini"
type="danger">删除
</el-button> </el-button>
</el-popconfirm> </el-popconfirm>
@ -156,34 +99,20 @@
<div class="sh-20"></div> <div class="sh-20"></div>
<el-pagination <el-pagination background layout="prev, pager, next" :pager-count="11" @current-change="pageChange"
background :current-page="page" :total="count">
layout="prev, pager, next"
:pager-count="11"
@current-change="pageChange"
:current-page="page"
:total="count">
</el-pagination> </el-pagination>
</el-card> </el-card>
<el-dialog <el-dialog title="OTP密钥" :visible.sync="otpImgData.visible" width="350px" center>
title="OTP密钥"
:visible.sync="otpImgData.visible"
width="350px"
center>
<div style="text-align: center">{{ otpImgData.username }} : {{ otpImgData.nickname }}</div> <div style="text-align: center">{{ otpImgData.username }} : {{ otpImgData.nickname }}</div>
<img :src="otpImgData.base64Img" alt="otp-img"/> <img :src="otpImgData.base64Img" alt="otp-img" />
</el-dialog> </el-dialog>
<!--新增修改弹出框--> <!--新增修改弹出框-->
<el-dialog <el-dialog :close-on-click-modal="false" title="用户" :visible="user_edit_dialog" @close="disVisible" width="650px"
:close-on-click-modal="false" center>
title="用户"
:visible="user_edit_dialog"
@close="disVisible"
width="650px"
center>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm"> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
<el-form-item label="用户ID" prop="id"> <el-form-item label="用户ID" prop="id">
@ -204,21 +133,13 @@
</el-form-item> </el-form-item>
<el-form-item label="过期时间" prop="limittime"> <el-form-item label="过期时间" prop="limittime">
<el-date-picker <el-date-picker v-model="ruleForm.limittime" type="date" size="small" align="center" style="width:130px"
v-model="ruleForm.limittime" :picker-options="pickerOptions" placeholder="选择日期">
type="date"
size="small"
align="center"
style="width:130px"
:picker-options="pickerOptions"
placeholder="选择日期">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
<el-form-item label="禁用OTP" prop="disable_otp"> <el-form-item label="禁用OTP" prop="disable_otp">
<el-switch <el-switch v-model="ruleForm.disable_otp" active-text="开启OTP后用户密码为PIN码,OTP密码为扫码后生成的动态码">
v-model="ruleForm.disable_otp"
active-text="开启OTP后用户密码为【PIN码+OTP动态码】(中间没有+号)">
</el-switch> </el-switch>
</el-form-item> </el-form-item>
@ -233,8 +154,7 @@
</el-form-item> </el-form-item>
<el-form-item label="发送邮件" prop="send_email"> <el-form-item label="发送邮件" prop="send_email">
<el-switch <el-switch v-model="ruleForm.send_email">
v-model="ruleForm.send_email">
</el-switch> </el-switch>
</el-form-item> </el-form-item>
@ -286,7 +206,7 @@ export default {
} }
}, },
searchData: '', searchData: '',
otpImgData: {visible: false, username: '', nickname: '', base64Img: ''}, otpImgData: { visible: false, username: '', nickname: '', base64Img: '' },
ruleForm: { ruleForm: {
send_email: true, send_email: true,
status: 1, status: 1,
@ -294,30 +214,30 @@ export default {
}, },
rules: { rules: {
username: [ username: [
{required: true, message: '请输入用户名', trigger: 'blur'}, { required: true, message: '请输入用户名', trigger: 'blur' },
{max: 50, message: '长度小于 50 个字符', trigger: 'blur'} { max: 50, message: '长度小于 50 个字符', trigger: 'blur' }
], ],
nickname: [ nickname: [
{required: true, message: '请输入用户姓名', trigger: 'blur'} { required: true, message: '请输入用户姓名', trigger: 'blur' }
], ],
email: [ email: [
{required: true, message: '请输入用户邮箱', trigger: 'blur'}, { required: true, message: '请输入用户邮箱', trigger: 'blur' },
{type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change']} { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
], ],
password: [ password: [
{min: 6, message: '长度大于 6 个字符', trigger: 'blur'} { min: 6, message: '长度大于 6 个字符', trigger: 'blur' }
], ],
pin_code: [ pin_code: [
{min: 6, message: 'PIN码大于 6 个字符', trigger: 'blur'} { min: 6, message: 'PIN码大于 6 个字符', trigger: 'blur' }
], ],
date1: [ date1: [
{type: 'date', required: true, message: '请选择日期', trigger: 'change'} { type: 'date', required: true, message: '请选择日期', trigger: 'change' }
], ],
groups: [ groups: [
{type: 'array', required: true, message: '请至少选择一个组', trigger: 'change'} { type: 'array', required: true, message: '请至少选择一个组', trigger: 'change' }
], ],
status: [ status: [
{required: true} { required: true }
], ],
}, },
} }
@ -473,6 +393,4 @@ export default {
} }
</script> </script>
<style scoped> <style scoped></style>
</style>