Enhance authentication logging and update MyBatis configuration

This commit is contained in:
zyh
2025-08-24 16:52:20 +08:00
parent 51d6319121
commit bc1f10381a
20 changed files with 122 additions and 36 deletions

View File

@@ -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<LoginResponse> 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<Object> 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);
}

View File

@@ -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<String, Object>();
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<String, Object>();
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());
}};
}
}

View File

@@ -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);
}

View File

@@ -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; }
}

View File

@@ -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();
}
}

View File

@@ -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<LoginResponse> 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<LoginResponse> validatePasswordAndBuild(UserAccount acc, String userType, String rawPwd) {
private Mono<LoginResponse> 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);

View File

@@ -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:

View File

@@ -21,5 +21,11 @@
AND user_type = #{userType}
LIMIT 1
</select>
</mapper>
<select id="findByUsername" resultMap="UserAccountMap">
SELECT id, user_type, username, display_name, password_hash, role, status, points_balance, created_at, updated_at
FROM user_account
WHERE username = #{username}
LIMIT 1
</select>
</mapper>