Files
VibeCoding/WebRTCSignalServer/src/main/java/com/ttstd/signaling/controller/AuthController.java
TongTongStudio ded6fe5ab7 feat(android): add device activation flow and update dependencies
重构项目架构,引入启动页(SplashActivity)检查激活状态,未激活设备引导至激活页(ActivationActivity)完成provision+token流程。集成Retrofit+RxJava3网络层、Room数据库、Lifecycle组件、MMKV等依赖,升级OkHttp/Gson版本,并将构建配置从旧项目全面迁移至新项目结构。
2026-08-03 00:25:51 +08:00

213 lines
9.5 KiB
Java

package com.ttstd.signaling.controller;
import com.ttstd.signaling.controller.dto.AuthDtos;
import com.ttstd.signaling.model.AuthPrincipal;
import com.ttstd.signaling.model.UserAccount;
import com.ttstd.signaling.security.AuthException;
import com.ttstd.signaling.security.RateLimiter;
import com.ttstd.signaling.service.AccountService;
import com.ttstd.signaling.service.TokenPair;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 主控端账号认证接口。
*/
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
/** 登录限流:单 IP 5 分钟内最多 10 次 */
private static final int LOGIN_LIMIT = 10;
private static final long LOGIN_WINDOW_SECONDS = 300;
private final AccountService accountService;
private final RateLimiter rateLimiter;
public AuthController(AccountService accountService, RateLimiter rateLimiter) {
this.accountService = accountService;
this.rateLimiter = rateLimiter;
}
@PostMapping("/register")
public ResponseEntity<Map<String, Object>> register(@Valid @RequestBody AuthDtos.RegisterRequest request,
HttpServletRequest httpRequest) {
String ip = clientIp(httpRequest);
if (!rateLimiter.tryAcquire("register:" + ip, 5, 3600)) {
throw AuthException.tooManyRequests("注册过于频繁,请稍后再试");
}
UserAccount account = accountService.register(request.username(), request.password());
Map<String, Object> body = new LinkedHashMap<>();
body.put("userId", account.getUserId());
body.put("username", account.getUsername());
return ResponseEntity.ok(body);
}
@PostMapping("/login")
public ResponseEntity<AuthDtos.TokenResponse> login(@Valid @RequestBody AuthDtos.LoginRequest request,
HttpServletRequest httpRequest) {
String ip = clientIp(httpRequest);
if (!rateLimiter.tryAcquire("login:" + ip, LOGIN_LIMIT, LOGIN_WINDOW_SECONDS)) {
throw AuthException.tooManyRequests("登录尝试过于频繁,请稍后再试");
}
TokenPair pair = accountService.login(
request.username(), request.password(), request.totpCode(),
ip, userAgent(httpRequest));
rateLimiter.reset("login:" + ip);
return ResponseEntity.ok(toResponse(pair));
}
@PostMapping("/refresh")
public ResponseEntity<AuthDtos.TokenResponse> refresh(@Valid @RequestBody AuthDtos.RefreshRequest request,
HttpServletRequest httpRequest) {
TokenPair pair = accountService.refresh(
request.refreshToken(), clientIp(httpRequest), userAgent(httpRequest));
return ResponseEntity.ok(toResponse(pair));
}
@PostMapping("/logout")
public ResponseEntity<Map<String, Object>> logout(HttpServletRequest httpRequest) {
AuthPrincipal principal = requirePrincipal(httpRequest);
accountService.logout(principal.sessionId());
return ResponseEntity.ok(Map.of("success", true));
}
@PostMapping("/logout-all")
public ResponseEntity<Map<String, Object>> logoutAll(HttpServletRequest httpRequest) {
AuthPrincipal principal = requirePrincipal(httpRequest);
accountService.revokeAllSessions(principal.principalId(), "用户主动退出全部设备");
return ResponseEntity.ok(Map.of("success", true));
}
@PostMapping("/change-password")
public ResponseEntity<Map<String, Object>> changePassword(
@Valid @RequestBody AuthDtos.ChangePasswordRequest request,
HttpServletRequest httpRequest) {
AuthPrincipal principal = requirePrincipal(httpRequest);
accountService.changePassword(principal.principalId(),
request.oldPassword(), request.newPassword());
return ResponseEntity.ok(Map.of("success", true, "message", "密码已修改,请重新登录"));
}
// ==================== TOTP 双因子 ====================
/**
* 生成 TOTP 密钥。返回 otpauth URI 供客户端渲染二维码;
* 此时尚未生效,需再调用 /totp/enable 完成绑定。
*/
@PostMapping("/totp/setup")
public ResponseEntity<Map<String, Object>> totpSetup(HttpServletRequest httpRequest) {
AuthPrincipal principal = requirePrincipal(httpRequest);
Map<String, String> result = accountService.setupTotp(principal.principalId());
Map<String, Object> body = new LinkedHashMap<>();
body.put("secret", result.get("secret"));
body.put("otpauthUri", result.get("otpauthUri"));
body.put("notice", "请用认证器扫码后调用 /api/auth/totp/enable 提交动态码完成绑定");
return ResponseEntity.ok(body);
}
/** 提交一次动态码,正式启用双因子。 */
@PostMapping("/totp/enable")
public ResponseEntity<Map<String, Object>> totpEnable(
@Valid @RequestBody AuthDtos.TotpEnableRequest request,
HttpServletRequest httpRequest) {
AuthPrincipal principal = requirePrincipal(httpRequest);
accountService.enableTotp(principal.principalId(), request.code());
return ResponseEntity.ok(Map.of("success", true, "message", "双因子认证已启用"));
}
/** 关闭双因子,需同时校验密码与动态码。 */
@PostMapping("/totp/disable")
public ResponseEntity<Map<String, Object>> totpDisable(
@Valid @RequestBody AuthDtos.TotpDisableRequest request,
HttpServletRequest httpRequest) {
AuthPrincipal principal = requirePrincipal(httpRequest);
accountService.disableTotp(principal.principalId(), request.password(), request.code());
return ResponseEntity.ok(Map.of("success", true, "message", "双因子认证已关闭"));
}
/** 查询当前登录身份。 */
@GetMapping("/me")
public ResponseEntity<Map<String, Object>> me(HttpServletRequest httpRequest) {
AuthPrincipal principal = requirePrincipal(httpRequest);
Map<String, Object> body = new LinkedHashMap<>();
body.put("principalId", principal.principalId());
body.put("principalType", principal.principalType().name());
body.put("displayName", principal.displayName());
body.put("sessionId", principal.sessionId());
body.put("signalDeviceId", principal.deviceId());
// 便于客户端展示"是否已开启双因子",仅对用户身份有效
if (principal.isUser()) {
body.put("totpEnabled",
accountService.requireUser(principal.principalId()).isTotpEnabled());
}
return ResponseEntity.ok(body);
}
/** 查询当前账号的活跃会话列表。 */
@GetMapping("/sessions")
public ResponseEntity<List<Map<String, Object>>> sessions(HttpServletRequest httpRequest) {
AuthPrincipal principal = requirePrincipal(httpRequest);
List<Map<String, Object>> body = accountService.listSessions(principal.principalId())
.stream()
.map(s -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("sessionId", s.getSessionId());
item.put("ip", s.getIp());
item.put("userAgent", s.getUserAgent());
item.put("createdAt", s.getCreatedAt().toEpochMilli());
item.put("lastSeenAt", s.getLastSeenAt().toEpochMilli());
item.put("current", s.getSessionId().equals(principal.sessionId()));
return item;
})
.toList();
return ResponseEntity.ok(body);
}
private AuthDtos.TokenResponse toResponse(TokenPair pair) {
return new AuthDtos.TokenResponse(
pair.accessToken(), pair.refreshToken(), pair.expiresInSeconds(),
pair.sessionId(), pair.principalId(), pair.displayName());
}
private AuthPrincipal requirePrincipal(HttpServletRequest request) {
Object principal = request.getAttribute(
com.ttstd.signaling.config.BearerAuthFilter.ATTR_PRINCIPAL);
if (principal instanceof AuthPrincipal auth) {
return auth;
}
throw AuthException.unauthorized("缺少有效的访问令牌");
}
static String clientIp(HttpServletRequest request) {
String forwarded = request.getHeader("X-Forwarded-For");
if (forwarded != null && !forwarded.isBlank()) {
int comma = forwarded.indexOf(',');
return (comma > 0 ? forwarded.substring(0, comma) : forwarded).trim();
}
return request.getRemoteAddr();
}
static String userAgent(HttpServletRequest request) {
String ua = request.getHeader("User-Agent");
if (ua == null) {
return "unknown";
}
return ua.length() > 256 ? ua.substring(0, 256) : ua;
}
}