Paulo-Uchoa/sentinel

GitHub: Paulo-Uchoa/sentinel

一个基于 Spring Boot 3 + Angular 的安全认证服务,通过 HttpOnly cookie、refresh token 轮换与重用检测等机制实现生产级身份验证。

Stars: 0 | Forks: 0

# Sentinel ![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg) ![Java](https://img.shields.io/badge/Java-17-orange.svg) ![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.3-brightgreen.svg) ![Angular](https://img.shields.io/badge/Angular-21-red.svg) 大多数教程将 JWT 存储在 `localStorage` 中,任何 XSS payload 都可以读取它们。Sentinel 将 token 保留在 **完全脱离 JavaScript 的地方**:一个短生命周期的 access token 和一个轮换的 refresh token 存在于 `HttpOnly` cookie 中,并通过 double-submit token 进行 CSRF 保护。它展示了 真正身份验证所需的核心模式 — **带有重用检测的 refresh token 轮换、暴力破解锁定、 Argon2id 哈希、安全标头和审计跟踪** — 而不仅仅是“签发一个 JWT 然后祈祷不出事”。 ## 为什么它很有趣 - 🍪 **Token 永不接触 JavaScript** — access token 和 refresh token 均为 `HttpOnly`、`Secure`、 `SameSite` cookie。XSS payload 无法窃取它们。 - 🔁 **带有重用检测的 refresh token 轮换** — 每次刷新都会签发一个全新的 token 并 撤销旧 token。如果重放已经使用过(被盗)的 token,**整个 token 族将被 撤销**,会话也会被终止。这是其核心特性。 - 🛡️ **CSRF 防御** — 因为身份验证依赖于 cookie,每个改变状态的请求都必须回传一个 double-submit `XSRF-TOKEN`。 - 🔒 **暴力破解保护** — 账户在多次登录失败后会自动锁定。 - 🧂 **Argon2id 密码哈希** — 通过委派编码器实现的现代、内存密集型哈希。 - 🧾 **审计跟踪** — 每个身份验证事件(登录、失败、锁定、刷新、**检测到重用**、登出) 都会被持久化。 - 🪖 **强化的响应** — CSP、HSTS、`X-Frame-Options: DENY`、referrer policy、无状态会话。 ## refresh token 轮换 + 重用检测的工作原理 Refresh token 是不透明的随机值;仅存储它们的 **SHA-256 哈希**。每次登录都会启动一个 token **族**。轮换会在每次使用时替换 token;重放旧 token 意味着它已被盗。 ``` sequenceDiagram participant C as Client (cookies) participant S as Sentinel C->>S: POST /login S-->>C: access (15m) + refresh R1 [family F] C->>S: POST /refresh (R1) S->>S: R1 valid -> rotate S-->>C: new access + refresh R2 [family F], R1 revoked Note over C,S: attacker stole R1 and replays it C->>S: POST /refresh (R1) S->>S: R1 already revoked -> REUSE DETECTED S->>S: revoke entire family F (R2 included) S-->>C: 401 + cookies cleared (re-login required) ``` ## Cookie 与 CSRF 模型 | Cookie | 标志 | 作用域 | 用途 | |--------|-------|-------|---------| | `access_token` | `HttpOnly`, `Secure`*, `SameSite=Strict` | `/` | 短生命周期 (15 分钟) 签名的 JWT | | `refresh_token` | `HttpOnly`, `Secure`*, `SameSite=Strict` | `/api/auth` | 长生命周期 (7 天) 不透明,轮换使用 | | `XSRF-TOKEN` | 可由 JS 读取, `SameSite` | `/` | Double-submit CSRF token | \* `Secure` 通过配置在生产环境 (HTTPS) 中启用;默认在本地 HTTP 中关闭。 SPA 读取 `XSRF-TOKEN` 并在每个更改状态的请求中将其作为 `X-XSRF-TOKEN` 标头回传; 如果它们不匹配,服务器将拒绝该请求。登录和注册免除 CSRF(因为此时没有 可被滥用的环境会话);而刷新和登出则**不**免除。 ## 威胁模型 | 威胁 | 缓解措施 | |--------|------------| | **XSS 窃取 token** | Token 为 `HttpOnly` — 无法从 JavaScript 读取 | | **CSRF** | Double-submit `XSRF-TOKEN` + `SameSite` cookie | | **被盗的 refresh token** | 轮换 + 重用检测会撤销整个 token 族 | | **暴力破解 / 凭据填充** | N 次失败尝试后锁定账户 | | **密码数据库泄露** | Argon2id (内存密集型) 哈希,绝不使用明文 | | **用户枚举** | 通用的“邮箱或密码无效”提示 + 对未知用户保持恒定工作量 | | **点击劫持** | `X-Frame-Options: DENY` + CSP `frame-ancestors 'none'` | | **Token 篡改** | Access token 是 HMAC 签名的 JWT | ## 技术栈 | 关注点 | 技术 | |------------|-------------------------------------------------------------------| | 后端 | Java 17, Spring Boot 3.3, Spring Security 6 | | Token | JWT (jjwt) access token,不透明轮换的 refresh token | | 哈希 | 通过 `DelegatingPasswordEncoder` 实现的 Argon2id (BouncyCastle) | | 持久化| PostgreSQL, Spring Data JPA, Flyway | | 前端 | Angular 21, TypeScript, standalone components, signals | | 文档 | springdoc-openapi (Swagger UI) | | 测试 | JUnit 5, Mockito, Spring Security Test, Testcontainers | | 工具 | Maven, Docker, GitHub Actions | ## 快速开始 ### 后端 + 数据库 (Docker) ``` docker compose up --build ``` - API: http://localhost:8080 - Swagger UI: http://localhost:8080/swagger-ui.html ### 零配置启动后端 (内存数据库 H2) ``` cd backend ./mvnw spring-boot:run -Dspring-boot.run.profiles=dev ``` ### 前端 (Angular) ``` cd frontend npm install npm start ``` 应用运行在 http://localhost:4200,并与 http://localhost:8080 的 API 进行通信。 ## API 概览 | 方法 | Endpoint | 身份验证 | 描述 | |--------|------------------------|------|----------------------------------------------| | POST | `/api/auth/register` | — | 创建账户,设置身份验证 cookie | | POST | `/api/auth/login` | — | 验证身份,设置身份验证 cookie | | POST | `/api/auth/refresh` | cookie + CSRF | 轮换 token(检测重用) | | POST | `/api/auth/logout` | cookie + CSRF | 撤销会话,清除 cookie | | GET | `/api/auth/me` | access cookie | 当前已验证的用户 | | GET | `/api/auth/csrf` | — | 初始化 `XSRF-TOKEN` cookie | ## 测试 ``` cd backend ./mvnw verify ``` - **单元测试** — `RefreshTokenService` 的轮换/重用逻辑和 `AuthService` 的锁定规则。 - **Web 测试** — 通过 MockMvc 的完整 cookie + CSRF 流程(注册 → `/me`,轮换 → 重用 → 401,锁定)。 - **集成测试** — 通过 Testcontainers 在带有 Flyway 的真实 PostgreSQL 上执行相同的流程 (**如果没有 Docker 会自动跳过**,在 CI 中运行)。 ## 项目结构 ``` sentinel/ ├── backend/ # Spring Boot auth service │ └── src/main/java/com/paulouchoa/sentinel │ ├── auth/ # controller, service, DTOs, exceptions │ ├── token/ # JWT, refresh rotation + reuse detection, cookies │ ├── security/ # cookie auth filter, CSRF filter, entry point │ ├── user/ # user entity & repository │ ├── audit/ # auth event trail │ ├── config/ # security, properties, OpenAPI │ └── common/ # error handling ├── frontend/ # Angular SPA (login, register, guarded dashboard) ├── docker-compose.yml └── .github/workflows/ci.yml ``` ## 生产环境说明 - 设置 `SENTINEL_COOKIE_SECURE=true` (HTTPS) 和一个强健的 `SENTINEL_JWT_SECRET` (32+ 字节)。 - 将 SPA 和 API 部署在同一站点上,以便 `SameSite=Strict` cookie 能够流通;考虑为 access token 使用 `__Host-` cookie 前缀。 - 除了针对每个账户的锁定外,在边缘部署 TLS 和速率限制器。 ## 路线图 - [ ] 电子邮件验证和密码重置 - [ ] 活动会话管理(列出/撤销设备) - [ ] 基于 TOTP 的双因素身份验证 - [ ] 定期清理过期的 refresh token
标签:Angular, Grype, Spring Boot, Token管理, Web安全开发, 域名枚举, 测试用例, 请求拦截