Paulo-Uchoa/sentinel
GitHub: Paulo-Uchoa/sentinel
一个基于 Spring Boot 3 + Angular 的安全认证服务,通过 HttpOnly cookie、refresh token 轮换与重用检测等机制实现生产级身份验证。
Stars: 0 | Forks: 0
# Sentinel




大多数教程将 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安全开发, 域名枚举, 测试用例, 请求拦截