diff --git a/docs/开发文档.md b/docs/开发文档.md index c2a8c8e..702104f 100644 --- a/docs/开发文档.md +++ b/docs/开发文档.md @@ -41,7 +41,7 @@ - 建议:后续改为 Flyway 迁移(`V001__init.sql` 起步),避免手工执行 SQL ## 安全与认证 -- 登录:`POST /api/auth/login`(`userType=admin|agent` + `username` + `password`) +- 登录:`POST /api/auth/login`(`username` + `password`,角色自动识别) - 自我信息:`GET /api/auth/me`(Authorization: Bearer ) - JWT:HS256;配置在 `security.jwt.*`(`application.yml`) - 密码:默认 `BCrypt`;兼容 `PLAIN:` 前缀以便初始化迁移 @@ -103,7 +103,7 @@ - [ ] 乐观锁/并发扣点优化、观测/告警 ## 接口清单(节选) -- `POST /api/auth/login`:登录获取 JWT +- `POST /api/auth/login`:登录获取 JWT(仅需 `username`、`password`) - `GET /api/auth/me`:当前用户信息 - `GET /api/link/{codeNo}`:查询链接元数据(待实现) - `POST /api/link/{codeNo}/select-region`:选择区服(待实现) @@ -119,4 +119,3 @@ - 默认管理员以 `PLAIN:` 存储密码,仅用于初始化。上线前必须改为 BCrypt: - 方案:使用项目内 `BCryptPasswordEncoder` 生成哈希,更新 `user_account.password_hash` - 统一返回体与 RBAC 规则将在后续迭代落地 - diff --git a/src/main/java/com/gameplatform/server/controller/auth/AuthController.java b/src/main/java/com/gameplatform/server/controller/auth/AuthController.java index 77bdbb0..952eb2e 100644 --- a/src/main/java/com/gameplatform/server/controller/auth/AuthController.java +++ b/src/main/java/com/gameplatform/server/controller/auth/AuthController.java @@ -4,6 +4,8 @@ import com.gameplatform.server.model.dto.auth.LoginRequest; import com.gameplatform.server.model.dto.auth.LoginResponse; import com.gameplatform.server.security.JwtService; import io.jsonwebtoken.Claims; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import jakarta.validation.Valid; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -13,6 +15,7 @@ import reactor.core.publisher.Mono; @RestController @RequestMapping("/api/auth") public class AuthController { + private static final Logger log = LoggerFactory.getLogger(AuthController.class); private final com.gameplatform.server.service.auth.AuthService authService; private final JwtService jwtService; @@ -25,6 +28,7 @@ public class AuthController { @PostMapping("/login") @ResponseStatus(HttpStatus.OK) public Mono login(@Valid @RequestBody LoginRequest req) { + log.info("/api/auth/login called username={}", req.getUsername()); return authService.login(req); } @@ -32,6 +36,7 @@ public class AuthController { public Mono me(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) { String token = authorization != null && authorization.startsWith("Bearer ") ? authorization.substring(7) : authorization; return Mono.fromCallable(() -> jwtService.parse(token)) + .doOnError(e -> log.warn("/api/auth/me parse token error: {}", e.toString())) .map(this::claimsToMe); } diff --git a/src/main/java/com/gameplatform/server/exception/GlobalExceptionHandler.java b/src/main/java/com/gameplatform/server/exception/GlobalExceptionHandler.java index b74f1c1..aa0db4a 100644 --- a/src/main/java/com/gameplatform/server/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/gameplatform/server/exception/GlobalExceptionHandler.java @@ -1,6 +1,13 @@ package com.gameplatform.server.exception; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.support.WebExchangeBindException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebInputException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -10,22 +17,61 @@ import java.util.LinkedHashMap; @RestControllerAdvice public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Object handleBadRequest(IllegalArgumentException e) { + log.info("400 BadRequest: {}", e.getMessage()); return body(HttpStatus.BAD_REQUEST.value(), e.getMessage()); } @ExceptionHandler(IllegalStateException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public Object handleForbidden(IllegalStateException e) { + log.info("403 Forbidden: {}", e.getMessage()); return body(HttpStatus.FORBIDDEN.value(), e.getMessage()); } + @ExceptionHandler(WebExchangeBindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Object handleBindException(WebExchangeBindException e) { + log.info("400 ValidationError: {}", e.getMessage()); + var details = new java.util.LinkedHashMap(); + e.getFieldErrors().forEach(fe -> details.put(fe.getField(), fe.getDefaultMessage())); + e.getGlobalErrors().forEach(ge -> details.put(ge.getObjectName(), ge.getDefaultMessage())); + return body(HttpStatus.BAD_REQUEST.value(), "参数校验失败", details); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Object handleConstraintViolation(ConstraintViolationException e) { + log.info("400 ConstraintViolation: {}", e.getMessage()); + var details = new java.util.LinkedHashMap(); + for (ConstraintViolation v : e.getConstraintViolations()) { + details.put(String.valueOf(v.getPropertyPath()), v.getMessage()); + } + return body(HttpStatus.BAD_REQUEST.value(), "参数校验失败", details); + } + + @ExceptionHandler(ServerWebInputException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Object handleInput(ServerWebInputException e) { + log.info("400 InputError: {}", e.getMessage()); + return body(HttpStatus.BAD_REQUEST.value(), "请求解析失败: " + e.getReason()); + } + + @ExceptionHandler(ResponseStatusException.class) + public Object handleRse(ResponseStatusException e) { + var status = e.getStatusCode(); + log.info("{} ResponseStatusException: {}", status, e.getReason()); + return body(status.value(), e.getReason()); + } + @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Object handleOther(Exception e) { + log.error("500 InternalServerError", e); return body(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误"); } @@ -36,5 +82,13 @@ public class GlobalExceptionHandler { put("timestamp", Instant.now().toString()); }}; } -} + private Object body(int code, String message, Object details) { + return new LinkedHashMap<>() {{ + put("code", code); + put("message", message); + put("details", details); + put("timestamp", Instant.now().toString()); + }}; + } +} diff --git a/src/main/java/com/gameplatform/server/mapper/account/UserAccountMapper.java b/src/main/java/com/gameplatform/server/mapper/account/UserAccountMapper.java index 3884cbb..4fd66ed 100644 --- a/src/main/java/com/gameplatform/server/mapper/account/UserAccountMapper.java +++ b/src/main/java/com/gameplatform/server/mapper/account/UserAccountMapper.java @@ -6,5 +6,5 @@ import org.apache.ibatis.annotations.Param; public interface UserAccountMapper { UserAccount findByUsernameAndType(@Param("username") String username, @Param("userType") String userType); + UserAccount findByUsername(@Param("username") String username); } - diff --git a/src/main/java/com/gameplatform/server/model/dto/auth/LoginRequest.java b/src/main/java/com/gameplatform/server/model/dto/auth/LoginRequest.java index 9dc7997..08ba76c 100644 --- a/src/main/java/com/gameplatform/server/model/dto/auth/LoginRequest.java +++ b/src/main/java/com/gameplatform/server/model/dto/auth/LoginRequest.java @@ -3,19 +3,13 @@ package com.gameplatform.server.model.dto.auth; import jakarta.validation.constraints.NotBlank; public class LoginRequest { - // userType: admin | agent @NotBlank - private String userType; - @NotBlank - private String username; // admin: username, agent: loginAccount + private String username; // 统一登录名(admin/agent 共用) @NotBlank private String password; - public String getUserType() { return userType; } - public void setUserType(String userType) { this.userType = userType; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } - diff --git a/src/main/java/com/gameplatform/server/security/JwtService.java b/src/main/java/com/gameplatform/server/security/JwtService.java index 221b0ec..1c90e75 100644 --- a/src/main/java/com/gameplatform/server/security/JwtService.java +++ b/src/main/java/com/gameplatform/server/security/JwtService.java @@ -4,6 +4,8 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -15,7 +17,7 @@ import java.util.Map; @Component public class JwtService { - + private static final Logger log = LoggerFactory.getLogger(JwtService.class); private final SecretKey key; private final long accessTokenMinutes; @@ -39,11 +41,14 @@ public class JwtService { if (extra != null) { extra.forEach(builder::claim); } - return builder.signWith(key, SignatureAlgorithm.HS256).compact(); + String token = builder.signWith(key, SignatureAlgorithm.HS256).compact(); + if (log.isDebugEnabled()) { + log.debug("JWT generated subject={}, userType={}, userId={}, username={} expInMin={}", subject, userType, userId, username, accessTokenMinutes); + } + return token; } public io.jsonwebtoken.Claims parse(String token) { return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); } } - diff --git a/src/main/java/com/gameplatform/server/service/auth/AuthService.java b/src/main/java/com/gameplatform/server/service/auth/AuthService.java index ad6fb2e..4161414 100644 --- a/src/main/java/com/gameplatform/server/service/auth/AuthService.java +++ b/src/main/java/com/gameplatform/server/service/auth/AuthService.java @@ -5,6 +5,8 @@ import com.gameplatform.server.model.dto.auth.LoginRequest; import com.gameplatform.server.model.dto.auth.LoginResponse; import com.gameplatform.server.model.entity.account.UserAccount; import com.gameplatform.server.security.JwtService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -14,6 +16,7 @@ import java.util.Map; @Service public class AuthService { + private static final Logger log = LoggerFactory.getLogger(AuthService.class); private final UserAccountMapper userAccountMapper; private final PasswordEncoder passwordEncoder; private final JwtService jwtService; @@ -27,22 +30,25 @@ public class AuthService { } public Mono login(LoginRequest req) { - String userType = normalizeType(req.getUserType()); - return Mono.fromCallable(() -> userAccountMapper.findByUsernameAndType(req.getUsername(), userType)) + log.info("login attempt username={}", req.getUsername()); + long start = System.currentTimeMillis(); + return Mono.fromCallable(() -> userAccountMapper.findByUsername(req.getUsername())) .subscribeOn(Schedulers.boundedElastic()) - .flatMap(acc -> validatePasswordAndBuild(acc, userType, req.getPassword())); + .doOnNext(acc -> { + if (acc == null) { + log.warn("login account not found username={}", req.getUsername()); + } else { + log.debug("login account loaded id={}, status={}, role={} userType={}", acc.getId(), acc.getStatus(), acc.getRole(), acc.getUserType()); + } + }) + .flatMap(acc -> validatePasswordAndBuild(acc, req.getPassword())) + .doOnSuccess(r -> log.info("login success username={}, tookMs={}", req.getUsername(), (System.currentTimeMillis()-start))) + .doOnError(e -> log.warn("login failed username={}, err={}, tookMs={}", req.getUsername(), e.toString(), (System.currentTimeMillis()-start))); } - private String normalizeType(String t) { - if (t == null) return ""; - t = t.trim().toLowerCase(); - if ("admin".equals(t)) return "ADMIN"; - if ("agent".equals(t)) return "AGENT"; - throw new IllegalArgumentException("unsupported userType: " + t); - } - - private Mono validatePasswordAndBuild(UserAccount acc, String userType, String rawPwd) { + private Mono validatePasswordAndBuild(UserAccount acc, String rawPwd) { if (acc == null || acc.getPasswordHash() == null) { + log.debug("validatePasswordAndBuild: account missing or no password hash"); return Mono.error(new IllegalArgumentException("用户名或密码错误")); } boolean ok; @@ -54,19 +60,24 @@ public class AuthService { } else { ok = false; } - if (!ok) return Mono.error(new IllegalArgumentException("用户名或密码错误")); + if (!ok) { + log.debug("validatePasswordAndBuild: password not match for user id={}", acc.getId()); + return Mono.error(new IllegalArgumentException("用户名或密码错误")); + } if (!"ENABLED".equalsIgnoreCase(acc.getStatus())) { + log.debug("validatePasswordAndBuild: account disabled id={}", acc.getId()); return Mono.error(new IllegalStateException("账户已禁用")); } + String userType = acc.getUserType() == null ? "agent" : acc.getUserType().toLowerCase(); String token = jwtService.generateToken( - userType.toLowerCase() + ":" + acc.getId(), - userType.toLowerCase(), acc.getId(), acc.getUsername(), - userType.equals("ADMIN") ? Map.of("role", acc.getRole()) : Map.of("displayName", acc.getDisplayName()) + userType + ":" + acc.getId(), + userType, acc.getId(), acc.getUsername(), + "admin".equals(userType) ? Map.of("role", acc.getRole()) : Map.of("displayName", acc.getDisplayName()) ); LoginResponse resp = new LoginResponse(); resp.setAccessToken(token); - resp.setUserType(userType.toLowerCase()); + resp.setUserType(userType); resp.setUserId(acc.getId()); resp.setUsername(acc.getUsername()); resp.setExpiresIn(60L * 30); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 128e22e..594fce6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,7 +13,7 @@ spring: connection-timeout: 30000 mybatis: - mapper-locations: classpath:mapper/*.xml + mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.gameplatform.server.model configuration: map-underscore-to-camel-case: true @@ -31,6 +31,9 @@ logging: level: root: info com.gameplatform.server: debug + org.mybatis: debug + org.apache.ibatis: debug + com.zaxxer.hikari: info security: jwt: diff --git a/src/main/resources/mapper/account/UserAccountMapper.xml b/src/main/resources/mapper/account/UserAccountMapper.xml index 18bbde8..3736788 100644 --- a/src/main/resources/mapper/account/UserAccountMapper.xml +++ b/src/main/resources/mapper/account/UserAccountMapper.xml @@ -21,5 +21,11 @@ AND user_type = #{userType} LIMIT 1 - + + diff --git a/target/classes/application.yml b/target/classes/application.yml index 128e22e..594fce6 100644 --- a/target/classes/application.yml +++ b/target/classes/application.yml @@ -13,7 +13,7 @@ spring: connection-timeout: 30000 mybatis: - mapper-locations: classpath:mapper/*.xml + mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.gameplatform.server.model configuration: map-underscore-to-camel-case: true @@ -31,6 +31,9 @@ logging: level: root: info com.gameplatform.server: debug + org.mybatis: debug + org.apache.ibatis: debug + com.zaxxer.hikari: info security: jwt: diff --git a/target/classes/com/gameplatform/server/controller/auth/AuthController$1.class b/target/classes/com/gameplatform/server/controller/auth/AuthController$1.class index b0336a4..542d1f9 100644 Binary files a/target/classes/com/gameplatform/server/controller/auth/AuthController$1.class and b/target/classes/com/gameplatform/server/controller/auth/AuthController$1.class differ diff --git a/target/classes/com/gameplatform/server/controller/auth/AuthController.class b/target/classes/com/gameplatform/server/controller/auth/AuthController.class index 4bba42f..5a46935 100644 Binary files a/target/classes/com/gameplatform/server/controller/auth/AuthController.class and b/target/classes/com/gameplatform/server/controller/auth/AuthController.class differ diff --git a/target/classes/com/gameplatform/server/exception/GlobalExceptionHandler$1.class b/target/classes/com/gameplatform/server/exception/GlobalExceptionHandler$1.class index 813e541..315d986 100644 Binary files a/target/classes/com/gameplatform/server/exception/GlobalExceptionHandler$1.class and b/target/classes/com/gameplatform/server/exception/GlobalExceptionHandler$1.class differ diff --git a/target/classes/com/gameplatform/server/exception/GlobalExceptionHandler.class b/target/classes/com/gameplatform/server/exception/GlobalExceptionHandler.class index 8e7751d..216ec5d 100644 Binary files a/target/classes/com/gameplatform/server/exception/GlobalExceptionHandler.class and b/target/classes/com/gameplatform/server/exception/GlobalExceptionHandler.class differ diff --git a/target/classes/com/gameplatform/server/mapper/account/UserAccountMapper.class b/target/classes/com/gameplatform/server/mapper/account/UserAccountMapper.class index b5b52af..c186692 100644 Binary files a/target/classes/com/gameplatform/server/mapper/account/UserAccountMapper.class and b/target/classes/com/gameplatform/server/mapper/account/UserAccountMapper.class differ diff --git a/target/classes/com/gameplatform/server/model/dto/auth/LoginRequest.class b/target/classes/com/gameplatform/server/model/dto/auth/LoginRequest.class index 7d38703..bf762f2 100644 Binary files a/target/classes/com/gameplatform/server/model/dto/auth/LoginRequest.class and b/target/classes/com/gameplatform/server/model/dto/auth/LoginRequest.class differ diff --git a/target/classes/com/gameplatform/server/security/JwtService.class b/target/classes/com/gameplatform/server/security/JwtService.class index 8c612a2..e741cfc 100644 Binary files a/target/classes/com/gameplatform/server/security/JwtService.class and b/target/classes/com/gameplatform/server/security/JwtService.class differ diff --git a/target/classes/com/gameplatform/server/security/SecurityConfig.class b/target/classes/com/gameplatform/server/security/SecurityConfig.class index 3b8504e..c97bdc4 100644 Binary files a/target/classes/com/gameplatform/server/security/SecurityConfig.class and b/target/classes/com/gameplatform/server/security/SecurityConfig.class differ diff --git a/target/classes/com/gameplatform/server/service/auth/AuthService.class b/target/classes/com/gameplatform/server/service/auth/AuthService.class index 4e2944a..225381c 100644 Binary files a/target/classes/com/gameplatform/server/service/auth/AuthService.class and b/target/classes/com/gameplatform/server/service/auth/AuthService.class differ diff --git a/target/classes/mapper/account/UserAccountMapper.xml b/target/classes/mapper/account/UserAccountMapper.xml index 18bbde8..3736788 100644 --- a/target/classes/mapper/account/UserAccountMapper.xml +++ b/target/classes/mapper/account/UserAccountMapper.xml @@ -21,5 +21,11 @@ AND user_type = #{userType} LIMIT 1 - + +