LoadUp Auth - 认证与授权架构¶
概述¶
LoadUp Auth 是基于 Gateway 和 UPMS 模块构建的统一认证授权体系,采用前后端分离架构,支持多种认证方式和灵活的权限控制。
🎯 核心特性¶
- ✅ 多种认证方式: 用户名密码、手机号验证码、邮箱、第三方OAuth
- ✅ 无状态JWT: 基于JWT的无状态认证,支持水平扩展
- ✅ 双Token机制: AccessToken + RefreshToken,安全性与用户体验兼顾
- ✅ Gateway认证: 在网关层统一处理认证,后端服务无需关心
- ✅ 方法级授权: 基于AOP的细粒度权限控制
- ✅ 第三方登录: 支持微信、QQ、GitHub等多种社交平台
- ✅ 安全加固: 登录锁定、验证码、防重放攻击等多重安全机制
🏗️ 架构设计¶
整体架构¶
┌─────────────────────────────────────────────────────────────┐
│ Client Layer │
│ (Web/Mobile/Mini Program/Desktop) │
└────────────────────────┬────────────────────────────────────┘
│
│ HTTP Request (with JWT Token)
▼
┌─────────────────────────────────────────────────────────────┐
│ LoadUp Gateway │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SecurityAction (认证层) │ │
│ │ - JWT验证 │ │
│ │ - Token解析 │ │
│ │ - 用户上下文设置 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ProxyAction (转发层) │ │
│ │ - 设置 UserContext │ │
│ │ - 调用业务Bean │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Business Service Layer │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Authorization Component (授权层) │ │
│ │ - @RequireRole AOP拦截 │ │
│ │ - @RequirePermission AOP拦截 │ │
│ │ - UserContext获取当前用户 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ UPMS Service (用户权限管理) │ │
│ │ - 用户管理 │ │
│ │ - 角色管理 │ │
│ │ - 权限管理 │ │
│ │ - OAuth绑定管理 │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
认证流程¶
1. 用户名密码登录¶
sequenceDiagram
participant C as 客户端
participant G as Gateway
participant U as UPMS
participant D as Database
C->>G: POST /api/v1/auth/login
G->>G: RouteAction: 路由匹配
G->>G: SecurityAction: securityCode=OFF (登录接口不需要认证)
G->>U: ProxyAction: 转发到 authService.login()
U->>D: 查询用户信息
D->>U: 返回用户数据
U->>U: BCrypt验证密码
U->>U: 检查账号状态(锁定/停用)
U->>U: 生成JWT Token (access + refresh)
U->>D: 记录登录日志
U->>G: 返回Token和用户信息
G->>C: 返回响应
2. JWT Token验证流程¶
sequenceDiagram
participant C as 客户端
participant G as Gateway
participant S as SecurityAction
participant U as UPMS Service
C->>G: 请求业务接口 (Header: Authorization: Bearer xxx)
G->>S: SecurityAction拦截
S->>S: 从Header提取Token
S->>S: 验证Token签名和有效期
S->>S: 解析Token获取userId、roles等
alt Token有效
S->>S: 将用户信息存入 request.attributes
S->>G: 继续执行后续Action
G->>U: ProxyAction设置UserContext
U->>U: 执行业务逻辑(@RequireRole检查)
U->>G: 返回业务结果
G->>C: 返回响应
else Token无效/过期
S->>C: 返回401 Unauthorized
end
第三方登录架构¶
OAuth2.0 标准流程¶
LoadUp Auth 采用标准的 OAuth 2.0 授权码模式实现第三方登录:
sequenceDiagram
participant U as 用户
participant C as 客户端
participant G as Gateway
participant A as UPMS Auth
participant O as OAuth Provider
participant D as Database
Note over U,D: 第一步:获取授权URL
U->>C: 点击"微信登录"
C->>G: POST /oauth/authorization-url {provider: WECHAT_OPEN}
G->>A: 转发请求
A->>D: 查询provider配置
D->>A: 返回配置(appId, appSecret等)
A->>A: 生成随机state并缓存(5分钟)
A->>A: 拼接授权URL
A->>C: 返回authorizationUrl
C->>U: 展示二维码或跳转授权页
Note over U,D: 第二步:用户授权
U->>O: 扫码或同意授权
O->>O: 用户确认授权
O->>C: 302重定向到redirectUri?code=xxx&state=yyy
Note over U,D: 第三步:处理回调
C->>G: POST /oauth/callback {provider, code, state}
G->>A: 转发请求
A->>A: 验证state(防CSRF)
A->>O: 用code换取access_token
O->>A: 返回access_token
A->>O: 用access_token获取用户信息
O->>A: 返回openId、昵称、头像等
A->>D: 根据provider+openId查询绑定
alt 已绑定
A->>A: 生成系统JWT Token
A->>D: 更新最后登录时间
A->>C: 返回accessToken和用户信息
C->>U: 登录成功,跳转到首页
else 未绑定
A->>A: 生成临时bindToken(5分钟)
A->>C: 返回bindToken和第三方用户信息
C->>U: 提示绑定已有账号或创建新账号
end
Note over U,D: 第四步:绑定账号(如需要)
U->>C: 输入用户名密码或创建新账号
C->>G: POST /oauth/bind-existing {bindToken, username, password}
G->>A: 转发请求
A->>A: 验证bindToken
A->>A: 验证用户名密码
A->>D: 创建绑定记录(user_id + provider + open_id)
A->>A: 生成系统JWT Token
A->>C: 返回accessToken和用户信息
C->>U: 登录成功
支持的OAuth Provider¶
| Provider | 编码 | 授权方式 | 特点 |
|---|---|---|---|
| 微信开放平台 | WECHAT_OPEN |
扫码登录 | 支持UnionID统一账号体系 |
| 微信公众号 | WECHAT_MP |
网页授权 | 仅限公众号内使用 |
QQ |
QQ互联 | 需申请QQ互联开发者资质 | |
| GitHub | GITHUB |
OAuth Apps | 适合技术类应用 |
GOOGLE |
Google Sign-In | 国际化应用首选 | |
| 微博 | WEIBO |
微博开放平台 | 社交属性强 |
| 支付宝 | ALIPAY |
支付宝登录 | 适合金融类应用 |
| 钉钉 | DINGTALK |
钉钉登录 | 企业内部应用 |
| 企业微信 | WECHAT_WORK |
企业微信授权 | 企业应用 |
Provider SPI 扩展机制¶
系统提供了统一的 OAuthProvider SPI 接口,支持快速扩展新的登录渠道:
public interface OAuthProvider {
/**
* 获取渠道编码(唯一标识)
*/
String getProviderCode();
/**
* 构建授权URL
* @param config 平台配置
* @param redirectUri 回调地址
* @param state 防CSRF随机串
* @return 完整的授权URL
*/
String buildAuthorizationUrl(OAuthConfig config, String redirectUri, String state);
/**
* 用授权码换取访问令牌
* @param config 平台配置
* @param code 授权码
* @return OAuth令牌(access_token, refresh_token等)
*/
OAuthToken getAccessToken(OAuthConfig config, String code);
/**
* 获取第三方用户信息
* @param config 平台配置
* @param accessToken 访问令牌
* @return 用户信息(openId, 昵称, 头像等)
*/
OAuthUserInfo getUserInfo(OAuthConfig config, String accessToken);
/**
* 刷新访问令牌(可选)
*/
default OAuthToken refreshToken(OAuthConfig config, String refreshToken) {
throw new UnsupportedOperationException("Token refresh not supported");
}
}
添加新Provider示例:
@Component
public class GiteeOAuthProvider implements OAuthProvider {
private final RestTemplate restTemplate;
@Override
public String getProviderCode() {
return "GITEE";
}
@Override
public String buildAuthorizationUrl(OAuthConfig config, String redirectUri, String state) {
return UriComponentsBuilder
.fromHttpUrl("https://gitee.com/oauth/authorize")
.queryParam("client_id", config.getAppId())
.queryParam("redirect_uri", redirectUri)
.queryParam("response_type", "code")
.queryParam("state", state)
.build()
.toUriString();
}
@Override
public OAuthToken getAccessToken(OAuthConfig config, String code) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", config.getAppId());
params.add("client_secret", config.getAppSecret());
params.add("code", code);
params.add("redirect_uri", config.getRedirectUri());
GiteeTokenResponse response = restTemplate.postForObject(
"https://gitee.com/oauth/token",
params,
GiteeTokenResponse.class
);
return OAuthToken.builder()
.accessToken(response.getAccessToken())
.refreshToken(response.getRefreshToken())
.expiresIn(response.getExpiresIn())
.build();
}
@Override
public OAuthUserInfo getUserInfo(OAuthConfig config, String accessToken) {
String url = "https://gitee.com/api/v5/user?access_token=" + accessToken;
GiteeUser giteeUser = restTemplate.getForObject(url, GiteeUser.class);
return OAuthUserInfo.builder()
.openId(giteeUser.getId().toString())
.nickname(giteeUser.getName())
.avatar(giteeUser.getAvatarUrl())
.email(giteeUser.getEmail())
.build();
}
}
注册为Spring Bean后,系统会自动发现并支持 GITEE 渠道。
安全机制¶
1. JWT Token 结构¶
Access Token Payload:
{
"sub": "1", // 用户ID
"username": "admin", // 用户名
"roles": ["ADMIN", "USER"], // 角色列表
"iat": 1709107200, // 签发时间
"exp": 1709193600, // 过期时间(24小时)
"jti": "uuid" // Token唯一标识
}
Refresh Token Payload:
2. 安全策略配置¶
upms:
security:
# JWT配置
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key} # 从环境变量读取
access-token-expiration: 86400000 # 24小时
refresh-token-expiration: 604800000 # 7天
issuer: loadup-auth
# 登录安全
login:
max-fail-attempts: 5 # 最大失败次数
lock-duration-minutes: 30 # 锁定时长
enable-captcha: true # 启用验证码
captcha-threshold: 3 # 失败3次后要求验证码
# 密码策略
password:
min-length: 8
require-uppercase: true
require-lowercase: true
require-digit: true
require-special-char: false
# 白名单(不需要认证的路径)
whitelist:
- /api/v1/auth/login
- /api/v1/auth/register
- /api/v1/auth/oauth/**
- /public/**
- /health
- /actuator/**
3. 防重放攻击¶
- Nonce机制: 登录请求携带一次性随机数
- Timestamp验证: 请求时间戳在5分钟内有效
- JTI唯一性: 每个Token包含唯一标识,支持黑名单撤销
4. 敏感操作保护¶
对于密码修改、账号绑定等敏感操作,需要二次验证:
@PostMapping("/change-password")
@RequireRole("USER")
public Result changePassword(@RequestBody ChangePasswordRequest request) {
// 1. 验证原密码
userService.verifyCurrentPassword(UserContext.getUserId(), request.getOldPassword());
// 2. 或验证短信验证码
// captchaService.verify(request.getPhone(), request.getSmsCode());
// 3. 执行修改
userService.changePassword(UserContext.getUserId(), request.getNewPassword());
return Result.success();
}
📡 完整认证流程示例¶
场景1:用户名密码登录¶
请求:
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "admin123",
"captchaKey": "cap-key-123",
"captchaCode": "ABCD"
}'
响应:
{
"result": {
"success": true
},
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 86400000,
"userInfo": {
"id": 1,
"username": "admin",
"nickname": "管理员",
"avatar": "https://example.com/avatar.jpg",
"roles": ["ADMIN"],
"permissions": ["user:create", "user:delete", ...]
}
}
}
场景2:微信扫码登录¶
步骤1: 获取授权URL
curl -X POST http://localhost:8080/api/v1/auth/oauth/authorization-url \
-H "Content-Type: application/json" \
-d '{
"provider": "WECHAT_OPEN",
"redirectUri": "https://yourapp.com/oauth/callback"
}'
响应包含二维码URL,前端展示二维码供用户扫码。
步骤2: 用户扫码授权后,微信回调到 redirectUri
步骤3: 处理回调
curl -X POST http://localhost:8080/api/v1/auth/oauth/callback \
-H "Content-Type: application/json" \
-d '{
"provider": "WECHAT_OPEN",
"code": "CODE_FROM_WECHAT",
"state": "STATE_FROM_STEP1"
}'
响应(已绑定):
响应(未绑定):
{
"result": {"success": true},
"data": {
"bound": false,
"bindToken": "temp-bind-token-5min",
"oauthUserInfo": {
"provider": "WECHAT_OPEN",
"openId": "wechat-open-id",
"nickname": "微信昵称",
"avatar": "https://wx.qlogo.cn/..."
}
}
}
步骤4: 绑定已有账号(如未绑定)
curl -X POST http://localhost:8080/api/v1/auth/oauth/bind-existing \
-H "Content-Type: application/json" \
-d '{
"bindToken": "temp-bind-token-5min",
"username": "myaccount",
"password": "mypassword"
}'
场景3:使用Token访问受保护资源¶
curl -X POST http://localhost:8080/api/v1/users/query \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"page": 1,
"size": 10
}'
Gateway的SecurityAction会自动验证Token,并将用户信息注入到请求上下文中。
场景4:刷新Token¶
curl -X POST http://localhost:8080/api/v1/auth/refresh-token \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "refresh-token-here"
}'
🔧 配置指南¶
开发环境配置¶
# application-dev.yml
upms:
security:
jwt:
secret: dev-secret-key-for-testing-only
access-token-expiration: 86400000
login:
max-fail-attempts: 10 # 开发环境放宽限制
enable-captcha: false # 关闭验证码便于测试
oauth:
providers:
github:
app-id: your-github-dev-client-id
app-secret: your-github-dev-secret
enabled: true
生产环境配置¶
# application-prod.yml
upms:
security:
jwt:
secret: ${JWT_SECRET} # 从环境变量读取
access-token-expiration: 3600000 # 1小时(更安全)
login:
max-fail-attempts: 3
lock-duration-minutes: 60
enable-captcha: true
oauth:
providers:
wechat-open:
app-id: ${WECHAT_APP_ID}
app-secret: ${WECHAT_APP_SECRET}
enabled: true
# 其他provider...
bind-token-expiration: 300 # 5分钟
auto-create-account: false # 禁止自动创建账号
📊 监控与审计¶
登录日志¶
每次登录(成功或失败)都会记录到 user_login_log 表:
| 字段 | 说明 |
|---|---|
| user_id | 用户ID(失败时可能为null) |
| username | 登录用户名 |
| login_type | 登录类型:PASSWORD/OAUTH/SMS |
| provider | OAuth渠道(如WECHAT_OPEN) |
| ip_address | 登录IP |
| user_agent | 浏览器UA |
| status | SUCCESS/FAILED |
| failure_reason | 失败原因 |
| login_time | 登录时间 |
操作审计¶
使用 @OperationLog 注解记录敏感操作:
@OperationLog(
type = "BIND_OAUTH",
module = "用户中心",
description = "绑定微信账号"
)
public void bindWechat(String userId, String openId) {
// 业务逻辑
}
🧪 测试¶
单元测试¶
@SpringBootTest
class AuthServiceTest {
@Autowired
private AuthService authService;
@Test
void login_success_whenValidCredentials() {
LoginRequest request = new LoginRequest();
request.setUsername("admin");
request.setPassword("admin123");
LoginResponse response = authService.login(request);
assertNotNull(response.getAccessToken());
assertNotNull(response.getRefreshToken());
}
@Test
void login_locked_whenExceedMaxAttempts() {
// 模拟多次失败登录
for (int i = 0; i < 5; i++) {
try {
authService.login(new LoginRequest("admin", "wrong"));
} catch (Exception ignored) {}
}
// 第6次应该提示账号已锁定
assertThrows(AccountLockedException.class, () -> {
authService.login(new LoginRequest("admin", "admin123"));
});
}
}
集成测试(OAuth)¶
@SpringBootTest(webEnvironment = RANDOM_PORT)
@EnableTestContainers(ContainerType.MYSQL)
class OAuthIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private WechatOAuthProvider wechatProvider;
@Test
void oauthLogin_success_whenAlreadyBound() {
// Mock 微信OAuth响应
when(wechatProvider.getAccessToken(any(), eq("mock-code")))
.thenReturn(new OAuthToken("mock-access-token", ...));
when(wechatProvider.getUserInfo(any(), eq("mock-access-token")))
.thenReturn(new OAuthUserInfo("open-id-123", "微信用户", ...));
// 调用回调接口
OAuthCallbackRequest request = new OAuthCallbackRequest();
request.setProvider("WECHAT_OPEN");
request.setCode("mock-code");
request.setState("mock-state");
ResponseEntity<Result<OAuthCallbackResponse>> response =
restTemplate.postForEntity("/api/v1/auth/oauth/callback", request, ...);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertTrue(response.getBody().getData().isBound());
assertNotNull(response.getBody().getData().getAccessToken());
}
}
🔗 相关文档¶
- Gateway 文档 - 网关认证实现
- UPMS 文档 - 用户权限管理
- Authorization 组件 - 方法级授权
- Signature 组件 - OAuth签名验证
许可证¶
Apache 2.0 License
Built with ❤️ by LoadUp Framework Team