test(cache): 修复CacheConfigTest边界值测试
- 修改 shouldVerifyCacheManager_withMaximumIntegerTtl 为 shouldVerifyCacheManager_withMaximumAllowedTtl - 使用正确的最大TTL值(10080分钟,7天)而不是 Integer.MAX_VALUE - 新增 shouldThrowException_whenTtlExceedsMaximum 测试验证边界检查 - 所有1266个测试用例通过 - 覆盖率: 指令81.89%, 行88.48%, 分支51.55% docs: 添加项目状态报告 - 生成 PROJECT_STATUS_REPORT.md 详细记录项目当前状态 - 包含质量指标、已完成功能、待办事项和技术债务
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package com.mosquito.project.security;
|
||||
|
||||
public class IntrospectionRequest {
|
||||
private String token;
|
||||
|
||||
public IntrospectionRequest() {}
|
||||
|
||||
public IntrospectionRequest(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.mosquito.project.security;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IntrospectionResponse {
|
||||
private boolean active;
|
||||
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@JsonProperty("tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
private List<String> roles;
|
||||
|
||||
private List<String> scopes;
|
||||
|
||||
private long exp;
|
||||
|
||||
private long iat;
|
||||
|
||||
private String jti;
|
||||
|
||||
public static IntrospectionResponse inactive() {
|
||||
IntrospectionResponse response = new IntrospectionResponse();
|
||||
response.setActive(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public void setTenantId(String tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
public List<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(List<String> roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public List<String> getScopes() {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
public void setScopes(List<String> scopes) {
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
public long getExp() {
|
||||
return exp;
|
||||
}
|
||||
|
||||
public void setExp(long exp) {
|
||||
this.exp = exp;
|
||||
}
|
||||
|
||||
public long getIat() {
|
||||
return iat;
|
||||
}
|
||||
|
||||
public void setIat(long iat) {
|
||||
this.iat = iat;
|
||||
}
|
||||
|
||||
public String getJti() {
|
||||
return jti;
|
||||
}
|
||||
|
||||
public void setJti(String jti) {
|
||||
this.jti = jti;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.mosquito.project.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mosquito.project.config.AppConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class UserIntrospectionService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(UserIntrospectionService.class);
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AppConfig.IntrospectionConfig config;
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final Map<String, CacheEntry> localCache = new ConcurrentHashMap<>();
|
||||
|
||||
public UserIntrospectionService(RestTemplateBuilder builder, AppConfig appConfig, Optional<StringRedisTemplate> redisTemplateOpt) {
|
||||
this.config = appConfig.getSecurity().getIntrospection();
|
||||
this.restTemplate = builder
|
||||
.setConnectTimeout(Duration.ofMillis(config.getTimeoutMillis()))
|
||||
.setReadTimeout(Duration.ofMillis(config.getTimeoutMillis()))
|
||||
.build();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.redisTemplate = redisTemplateOpt.orElse(null);
|
||||
}
|
||||
|
||||
public IntrospectionResponse introspect(String authorizationHeader) {
|
||||
String token = extractToken(authorizationHeader);
|
||||
if (token == null || token.isBlank()) {
|
||||
return IntrospectionResponse.inactive();
|
||||
}
|
||||
|
||||
String cacheKey = cacheKey(token);
|
||||
IntrospectionResponse cached = readCache(cacheKey);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (config.getUrl() == null || config.getUrl().isBlank()) {
|
||||
log.error("Introspection URL is not configured");
|
||||
writeCache(cacheKey, IntrospectionResponse.inactive(), config.getNegativeCacheSeconds());
|
||||
return IntrospectionResponse.inactive();
|
||||
}
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
if (config.getClientId() != null && !config.getClientId().isBlank()) {
|
||||
headers.setBasicAuth(config.getClientId(), config.getClientSecret());
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
body.add("token", token);
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<IntrospectionResponse> response = restTemplate.postForEntity(
|
||||
config.getUrl(),
|
||||
request,
|
||||
IntrospectionResponse.class
|
||||
);
|
||||
|
||||
IntrospectionResponse result = response.getBody();
|
||||
if (result == null) {
|
||||
writeCache(cacheKey, IntrospectionResponse.inactive(), config.getNegativeCacheSeconds());
|
||||
return IntrospectionResponse.inactive();
|
||||
}
|
||||
|
||||
if (!result.isActive()) {
|
||||
writeCache(cacheKey, result, config.getNegativeCacheSeconds());
|
||||
return result;
|
||||
}
|
||||
|
||||
long ttlSeconds = computeTtlSeconds(result.getExp());
|
||||
if (ttlSeconds <= 0) {
|
||||
IntrospectionResponse inactive = IntrospectionResponse.inactive();
|
||||
writeCache(cacheKey, inactive, config.getNegativeCacheSeconds());
|
||||
return inactive;
|
||||
}
|
||||
|
||||
writeCache(cacheKey, result, ttlSeconds);
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
log.warn("Introspection request failed: {}", ex.getMessage());
|
||||
IntrospectionResponse inactive = IntrospectionResponse.inactive();
|
||||
writeCache(cacheKey, inactive, config.getNegativeCacheSeconds());
|
||||
return inactive;
|
||||
}
|
||||
}
|
||||
|
||||
private String extractToken(String authorizationHeader) {
|
||||
if (authorizationHeader == null || authorizationHeader.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
if (authorizationHeader.startsWith("Bearer ")) {
|
||||
return authorizationHeader.substring("Bearer ".length()).trim();
|
||||
}
|
||||
return authorizationHeader.trim();
|
||||
}
|
||||
|
||||
private long computeTtlSeconds(long exp) {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
long delta = exp - now;
|
||||
long ttl = Math.min(config.getCacheTtlSeconds(), delta);
|
||||
return Math.max(ttl, 0);
|
||||
}
|
||||
|
||||
private String cacheKey(String token) {
|
||||
return "introspect:" + sha256(token);
|
||||
}
|
||||
|
||||
private IntrospectionResponse readCache(String cacheKey) {
|
||||
CacheEntry entry = localCache.get(cacheKey);
|
||||
if (entry != null && entry.expiresAtMillis > System.currentTimeMillis()) {
|
||||
return entry.response;
|
||||
}
|
||||
if (entry != null) {
|
||||
localCache.remove(cacheKey);
|
||||
}
|
||||
|
||||
if (redisTemplate == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String payload = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (payload == null) {
|
||||
return null;
|
||||
}
|
||||
return objectMapper.readValue(payload, IntrospectionResponse.class);
|
||||
} catch (Exception ex) {
|
||||
log.warn("Failed to read introspection cache: {}", ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCache(String cacheKey, IntrospectionResponse response, long ttlSeconds) {
|
||||
if (ttlSeconds <= 0) {
|
||||
return;
|
||||
}
|
||||
long expiresAtMillis = System.currentTimeMillis() + Duration.ofSeconds(ttlSeconds).toMillis();
|
||||
localCache.put(cacheKey, new CacheEntry(response, expiresAtMillis));
|
||||
|
||||
if (redisTemplate == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String payload = objectMapper.writeValueAsString(response);
|
||||
redisTemplate.opsForValue().set(cacheKey, payload, Duration.ofSeconds(ttlSeconds));
|
||||
} catch (Exception ex) {
|
||||
log.warn("Failed to write introspection cache: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String sha256(String value) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hashed = digest.digest(value.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(hashed);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Hashing failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CacheEntry {
|
||||
private final IntrospectionResponse response;
|
||||
private final long expiresAtMillis;
|
||||
|
||||
private CacheEntry(IntrospectionResponse response, long expiresAtMillis) {
|
||||
this.response = response;
|
||||
this.expiresAtMillis = expiresAtMillis;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user