secure_verification.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. package controller
  2. import (
  3. "fmt"
  4. "net/http"
  5. "time"
  6. "github.com/QuantumNous/new-api/common"
  7. "github.com/QuantumNous/new-api/model"
  8. passkeysvc "github.com/QuantumNous/new-api/service/passkey"
  9. "github.com/QuantumNous/new-api/setting/system_setting"
  10. "github.com/gin-contrib/sessions"
  11. "github.com/gin-gonic/gin"
  12. )
  13. const (
  14. // SecureVerificationSessionKey 安全验证的 session key
  15. SecureVerificationSessionKey = "secure_verified_at"
  16. // SecureVerificationTimeout 验证有效期(秒)
  17. SecureVerificationTimeout = 300 // 5分钟
  18. )
  19. type UniversalVerifyRequest struct {
  20. Method string `json:"method"` // "2fa" 或 "passkey"
  21. Code string `json:"code,omitempty"`
  22. }
  23. type VerificationStatusResponse struct {
  24. Verified bool `json:"verified"`
  25. ExpiresAt int64 `json:"expires_at,omitempty"`
  26. }
  27. // UniversalVerify 通用验证接口
  28. // 支持 2FA 和 Passkey 验证,验证成功后在 session 中记录时间戳
  29. func UniversalVerify(c *gin.Context) {
  30. userId := c.GetInt("id")
  31. if userId == 0 {
  32. c.JSON(http.StatusUnauthorized, gin.H{
  33. "success": false,
  34. "message": "未登录",
  35. })
  36. return
  37. }
  38. var req UniversalVerifyRequest
  39. if err := c.ShouldBindJSON(&req); err != nil {
  40. common.ApiError(c, fmt.Errorf("参数错误: %v", err))
  41. return
  42. }
  43. // 获取用户信息
  44. user := &model.User{Id: userId}
  45. if err := user.FillUserById(); err != nil {
  46. common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err))
  47. return
  48. }
  49. if user.Status != common.UserStatusEnabled {
  50. common.ApiError(c, fmt.Errorf("该用户已被禁用"))
  51. return
  52. }
  53. // 检查用户的验证方式
  54. twoFA, _ := model.GetTwoFAByUserId(userId)
  55. has2FA := twoFA != nil && twoFA.IsEnabled
  56. passkey, passkeyErr := model.GetPasskeyByUserID(userId)
  57. hasPasskey := passkeyErr == nil && passkey != nil
  58. if !has2FA && !hasPasskey {
  59. common.ApiError(c, fmt.Errorf("用户未启用2FA或Passkey"))
  60. return
  61. }
  62. // 根据验证方式进行验证
  63. var verified bool
  64. var verifyMethod string
  65. switch req.Method {
  66. case "2fa":
  67. if !has2FA {
  68. common.ApiError(c, fmt.Errorf("用户未启用2FA"))
  69. return
  70. }
  71. if req.Code == "" {
  72. common.ApiError(c, fmt.Errorf("验证码不能为空"))
  73. return
  74. }
  75. verified = validateTwoFactorAuth(twoFA, req.Code)
  76. verifyMethod = "2FA"
  77. case "passkey":
  78. if !hasPasskey {
  79. common.ApiError(c, fmt.Errorf("用户未启用Passkey"))
  80. return
  81. }
  82. // Passkey 验证需要先调用 PasskeyVerifyBegin 和 PasskeyVerifyFinish
  83. // 这里只是验证 Passkey 验证流程是否已经完成
  84. // 实际上,前端应该先调用这两个接口,然后再调用本接口
  85. verified = true // Passkey 验证逻辑已在 PasskeyVerifyFinish 中完成
  86. verifyMethod = "Passkey"
  87. default:
  88. common.ApiError(c, fmt.Errorf("不支持的验证方式: %s", req.Method))
  89. return
  90. }
  91. if !verified {
  92. common.ApiError(c, fmt.Errorf("验证失败,请检查验证码"))
  93. return
  94. }
  95. // 验证成功,在 session 中记录时间戳
  96. session := sessions.Default(c)
  97. now := time.Now().Unix()
  98. session.Set(SecureVerificationSessionKey, now)
  99. if err := session.Save(); err != nil {
  100. common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err))
  101. return
  102. }
  103. // 记录日志
  104. model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("通用安全验证成功 (验证方式: %s)", verifyMethod))
  105. c.JSON(http.StatusOK, gin.H{
  106. "success": true,
  107. "message": "验证成功",
  108. "data": gin.H{
  109. "verified": true,
  110. "expires_at": now + SecureVerificationTimeout,
  111. },
  112. })
  113. }
  114. // GetVerificationStatus 获取验证状态
  115. func GetVerificationStatus(c *gin.Context) {
  116. userId := c.GetInt("id")
  117. if userId == 0 {
  118. c.JSON(http.StatusUnauthorized, gin.H{
  119. "success": false,
  120. "message": "未登录",
  121. })
  122. return
  123. }
  124. session := sessions.Default(c)
  125. verifiedAtRaw := session.Get(SecureVerificationSessionKey)
  126. if verifiedAtRaw == nil {
  127. c.JSON(http.StatusOK, gin.H{
  128. "success": true,
  129. "message": "",
  130. "data": VerificationStatusResponse{
  131. Verified: false,
  132. },
  133. })
  134. return
  135. }
  136. verifiedAt, ok := verifiedAtRaw.(int64)
  137. if !ok {
  138. c.JSON(http.StatusOK, gin.H{
  139. "success": true,
  140. "message": "",
  141. "data": VerificationStatusResponse{
  142. Verified: false,
  143. },
  144. })
  145. return
  146. }
  147. elapsed := time.Now().Unix() - verifiedAt
  148. if elapsed >= SecureVerificationTimeout {
  149. // 验证已过期
  150. session.Delete(SecureVerificationSessionKey)
  151. _ = session.Save()
  152. c.JSON(http.StatusOK, gin.H{
  153. "success": true,
  154. "message": "",
  155. "data": VerificationStatusResponse{
  156. Verified: false,
  157. },
  158. })
  159. return
  160. }
  161. c.JSON(http.StatusOK, gin.H{
  162. "success": true,
  163. "message": "",
  164. "data": VerificationStatusResponse{
  165. Verified: true,
  166. ExpiresAt: verifiedAt + SecureVerificationTimeout,
  167. },
  168. })
  169. }
  170. // CheckSecureVerification 检查是否已通过安全验证
  171. // 返回 true 表示验证有效,false 表示需要重新验证
  172. func CheckSecureVerification(c *gin.Context) bool {
  173. session := sessions.Default(c)
  174. verifiedAtRaw := session.Get(SecureVerificationSessionKey)
  175. if verifiedAtRaw == nil {
  176. return false
  177. }
  178. verifiedAt, ok := verifiedAtRaw.(int64)
  179. if !ok {
  180. return false
  181. }
  182. elapsed := time.Now().Unix() - verifiedAt
  183. if elapsed >= SecureVerificationTimeout {
  184. // 验证已过期,清除 session
  185. session.Delete(SecureVerificationSessionKey)
  186. _ = session.Save()
  187. return false
  188. }
  189. return true
  190. }
  191. // PasskeyVerifyAndSetSession Passkey 验证完成后设置 session
  192. // 这是一个辅助函数,供 PasskeyVerifyFinish 调用
  193. func PasskeyVerifyAndSetSession(c *gin.Context) {
  194. session := sessions.Default(c)
  195. now := time.Now().Unix()
  196. session.Set(SecureVerificationSessionKey, now)
  197. _ = session.Save()
  198. }
  199. // PasskeyVerifyForSecure 用于安全验证的 Passkey 验证流程
  200. // 整合了 begin 和 finish 流程
  201. func PasskeyVerifyForSecure(c *gin.Context) {
  202. if !system_setting.GetPasskeySettings().Enabled {
  203. c.JSON(http.StatusOK, gin.H{
  204. "success": false,
  205. "message": "管理员未启用 Passkey 登录",
  206. })
  207. return
  208. }
  209. userId := c.GetInt("id")
  210. if userId == 0 {
  211. c.JSON(http.StatusUnauthorized, gin.H{
  212. "success": false,
  213. "message": "未登录",
  214. })
  215. return
  216. }
  217. user := &model.User{Id: userId}
  218. if err := user.FillUserById(); err != nil {
  219. common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err))
  220. return
  221. }
  222. if user.Status != common.UserStatusEnabled {
  223. common.ApiError(c, fmt.Errorf("该用户已被禁用"))
  224. return
  225. }
  226. credential, err := model.GetPasskeyByUserID(userId)
  227. if err != nil {
  228. c.JSON(http.StatusOK, gin.H{
  229. "success": false,
  230. "message": "该用户尚未绑定 Passkey",
  231. })
  232. return
  233. }
  234. wa, err := passkeysvc.BuildWebAuthn(c.Request)
  235. if err != nil {
  236. common.ApiError(c, err)
  237. return
  238. }
  239. waUser := passkeysvc.NewWebAuthnUser(user, credential)
  240. sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey)
  241. if err != nil {
  242. common.ApiError(c, err)
  243. return
  244. }
  245. _, err = wa.FinishLogin(waUser, *sessionData, c.Request)
  246. if err != nil {
  247. common.ApiError(c, err)
  248. return
  249. }
  250. // 更新凭证的最后使用时间
  251. now := time.Now()
  252. credential.LastUsedAt = &now
  253. if err := model.UpsertPasskeyCredential(credential); err != nil {
  254. common.ApiError(c, err)
  255. return
  256. }
  257. // 验证成功,设置 session
  258. PasskeyVerifyAndSetSession(c)
  259. // 记录日志
  260. model.RecordLog(userId, model.LogTypeSystem, "Passkey 安全验证成功")
  261. c.JSON(http.StatusOK, gin.H{
  262. "success": true,
  263. "message": "Passkey 验证成功",
  264. "data": gin.H{
  265. "verified": true,
  266. "expires_at": time.Now().Unix() + SecureVerificationTimeout,
  267. },
  268. })
  269. }