高级主题与最佳实践¶
目录¶
多租户认证¶
什么是多租户?¶
多租户(Multi-Tenancy) 是一种软件架构,单个应用实例可以服务多个租户(客户),每个租户的数据相互隔离。
多租户隔离策略¶
- 数据库级隔离:每个租户独立数据库
- Schema级隔离:共享数据库,独立Schema
- 表级隔离:共享表,通过tenant_id字段区分
实现多租户认证¶
方式1:基于子域名¶
/**
* 租户识别器
*/
@Component
public class TenantIdentifierResolver {
/**
* 从请求中提取租户标识
*/
public String resolveTenantIdentifier(HttpServletRequest request) {
// 方式1: 从子域名提取
String host = request.getServerName();
if (host.contains(".")) {
String subdomain = host.substring(0, host.indexOf("."));
if (!subdomain.equals("www")) {
return subdomain;
}
}
// 方式2: 从请求头提取
String tenantHeader = request.getHeader("X-Tenant-ID");
if (tenantHeader != null) {
return tenantHeader;
}
// 方式3: 从JWT Token提取
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getCredentials() instanceof Jwt) {
Jwt jwt = (Jwt) auth.getCredentials();
return jwt.getClaimAsString("tenant_id");
}
return "default";
}
}
方式2:基于JWT Token¶
/**
* 多租户JWT Token Provider
*/
@Component
public class MultiTenantJwtTokenProvider {
@Autowired
private TenantService tenantService;
/**
* 生成包含租户信息的Token
*/
public String generateToken(Authentication authentication, String tenantId) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 验证用户是否属于该租户
if (!tenantService.userBelongsToTenant(userDetails.getUsername(), tenantId)) {
throw new IllegalArgumentException("User does not belong to tenant");
}
Date now = new Date();
Date expiryDate = new Date(now.getTime() + 3600000);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("tenant_id", tenantId)
.claim("tenant_name", tenantService.getTenantName(tenantId))
.claim("authorities", userDetails.getAuthorities())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
}
数据隔离¶
/**
* 租户上下文
*/
public class TenantContext {
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static String getTenantId() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
}
}
/**
* 租户识别过滤器
*/
@Component
public class TenantIdentifierFilter extends OncePerRequestFilter {
@Autowired
private TenantIdentifierResolver tenantIdentifierResolver;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String tenantId = tenantIdentifierResolver.resolveTenantIdentifier(request);
TenantContext.setTenantId(tenantId);
filterChain.doFilter(request, response);
} finally {
TenantContext.clear();
}
}
}
/**
* JPA数据过滤
*/
@Entity
@Table(name = "orders")
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = "string"))
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "tenant_id", nullable = false)
private String tenantId;
// 其他字段...
}
/**
* 自动设置租户ID
*/
@Component
public class TenantEntityListener {
@PrePersist
public void setTenantId(Object entity) {
if (entity instanceof TenantAware) {
TenantAware tenantAware = (TenantAware) entity;
if (tenantAware.getTenantId() == null) {
tenantAware.setTenantId(TenantContext.getTenantId());
}
}
}
}
分布式会话管理¶
为什么需要分布式会话?¶
在微服务或集群环境中,用户请求可能被路由到不同服务器,需要共享会话数据。
解决方案对比¶
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Sticky Session | 简单,无需额外存储 | 单点故障,负载不均 | 小规模应用 |
| Session复制 | 高可用 | 网络开销大,不适合大集群 | 中小规模 |
| 集中式存储 | 扩展性好,高可用 | 需要额外组件 | 大规模生产环境 |
| JWT Token | 无状态,易扩展 | 无法主动吊销 | 微服务、API |
使用Redis实现分布式会话¶
Spring Session + Redis¶
<dependencies>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
/**
* Spring Session配置
*/
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // 30分钟
public class RedisSessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName("localhost");
config.setPort(6379);
config.setPassword("password");
return new LettuceConnectionFactory(config);
}
/**
* Session序列化器
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
/**
* Cookie序列化策略
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("SESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); // 跨子域
serializer.setUseHttpOnlyCookie(true);
serializer.setUseSecureCookie(true);
serializer.setSameSite("Lax");
return serializer;
}
}
# application.properties
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.session.store-type=redis
spring.session.redis.namespace=spring:session
自定义Session存储¶
/**
* 自定义Session Repository
*/
@Component
public class CustomSessionRepository implements SessionRepository<CustomSession> {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String SESSION_PREFIX = "session:";
private static final int DEFAULT_MAX_INACTIVE_INTERVAL = 1800;
@Override
public CustomSession createSession() {
CustomSession session = new CustomSession();
session.setId(UUID.randomUUID().toString());
session.setMaxInactiveInterval(Duration.ofSeconds(DEFAULT_MAX_INACTIVE_INTERVAL));
session.setCreationTime(Instant.now());
session.setLastAccessedTime(Instant.now());
return session;
}
@Override
public void save(CustomSession session) {
String key = SESSION_PREFIX + session.getId();
session.setLastAccessedTime(Instant.now());
redisTemplate.opsForHash().putAll(key, session.toMap());
redisTemplate.expire(key, session.getMaxInactiveInterval());
}
@Override
public CustomSession findById(String id) {
String key = SESSION_PREFIX + id;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
if (entries.isEmpty()) {
return null;
}
CustomSession session = CustomSession.fromMap(entries);
// 检查是否过期
if (session.isExpired()) {
deleteById(id);
return null;
}
return session;
}
@Override
public void deleteById(String id) {
String key = SESSION_PREFIX + id;
redisTemplate.delete(key);
}
}
Session活动监控¶
/**
* Session活动监控
*/
@Component
public class SessionActivityMonitor {
@Autowired
private SessionRegistry sessionRegistry;
/**
* 获取活跃会话数
*/
public int getActiveSessionCount() {
return sessionRegistry.getAllPrincipals().stream()
.mapToInt(principal -> sessionRegistry.getAllSessions(principal, false).size())
.sum();
}
/**
* 获取用户会话
*/
public List<SessionInfo> getUserSessions(String username) {
List<SessionInfo> sessions = new ArrayList<>();
for (Object principal : sessionRegistry.getAllPrincipals()) {
if (principal.toString().equals(username)) {
List<SessionInformation> userSessions =
sessionRegistry.getAllSessions(principal, false);
for (SessionInformation si : userSessions) {
SessionInfo info = new SessionInfo();
info.setSessionId(si.getSessionId());
info.setLastRequest(si.getLastRequest());
info.setExpired(si.isExpired());
sessions.add(info);
}
}
}
return sessions;
}
/**
* 定期清理过期会话
*/
@Scheduled(fixedRate = 60000) // 每分钟
public void cleanupExpiredSessions() {
for (Object principal : sessionRegistry.getAllPrincipals()) {
List<SessionInformation> sessions =
sessionRegistry.getAllSessions(principal, true); // 包含过期
for (SessionInformation si : sessions) {
if (si.isExpired()) {
sessionRegistry.removeSessionInformation(si.getSessionId());
}
}
}
}
}
微服务安全架构¶
架构模式¶
1. 边缘认证模式(Gateway Authentication)¶
┌──────────────┐
User ──────────────────→ │ API Gateway │ ← JWT验证
└──────┬───────┘
│
┌────────────┼────────────┐
│ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│Service│ │Service│ │Service│
│ A │ │ B │ │ C │
└───────┘ └───────┘ └───────┘
特点: - 认证在网关统一处理 - 内部服务信任网关 - 简化服务开发
实现:
/**
* Gateway JWT过滤器
*/
@Component
public class GatewayJwtFilter implements GlobalFilter, Ordered {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 公开路径跳过认证
if (isPublicPath(request.getPath().value())) {
return chain.filter(exchange);
}
// 提取并验证JWT
String token = extractToken(request);
if (token == null || !tokenProvider.validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 从Token提取用户信息,添加到请求头传递给后端服务
String username = tokenProvider.getUsernameFromToken(token);
String userId = tokenProvider.getClaimFromToken(token, "user_id");
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", userId)
.header("X-Username", username)
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
}
@Override
public int getOrder() {
return -100; // 高优先级
}
}
/**
* 后端服务获取用户信息
*/
@Component
public class UserContextInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
String userId = request.getHeader("X-User-Id");
String username = request.getHeader("X-Username");
// 设置到上下文
UserContext.setUserId(userId);
UserContext.setUsername(username);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
UserContext.clear();
}
}
2. 服务间认证模式(Service-to-Service Authentication)¶
实现:
/**
* 服务间调用客户端
*/
@Configuration
public class ServiceClientConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(OAuth2AuthorizedClientManager clientManager) {
RestTemplate restTemplate = new RestTemplate();
// 添加OAuth2拦截器
restTemplate.getInterceptors().add((request, body, execution) -> {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId("service-client")
.principal("service-account")
.build();
OAuth2AuthorizedClient client = clientManager.authorize(authorizeRequest);
if (client != null) {
request.getHeaders().setBearerAuth(
client.getAccessToken().getTokenValue()
);
}
return execution.execute(request, body);
});
return restTemplate;
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager clientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository
);
clientManager.setAuthorizedClientProvider(authorizedClientProvider);
return clientManager;
}
}
# 服务配置
spring:
security:
oauth2:
client:
registration:
service-client:
client-id: service-a
client-secret: service-a-secret
authorization-grant-type: client_credentials
scope: service.read,service.write
provider:
auth-server:
token-uri: http://auth-server:9000/oauth2/token
分布式追踪与安全¶
/**
* 在分布式追踪中记录安全信息
*/
@Component
public class SecurityTracingAspect {
@Autowired
private Tracer tracer;
@Around("@annotation(PreAuthorize)")
public Object traceSecurityCheck(ProceedingJoinPoint joinPoint) throws Throwable {
Span span = tracer.currentSpan();
if (span != null) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
span.tag("security.user", auth.getName());
span.tag("security.authorities",
auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(",")));
}
}
return joinPoint.proceed();
}
}
性能优化策略¶
1. 缓存认证信息¶
/**
* 缓存UserDetails
*/
@Service
public class CachedUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Cacheable(value = "userDetails", key = "#username")
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return buildUserDetails(user);
}
@CacheEvict(value = "userDetails", key = "#username")
public void evictUserCache(String username) {
// 缓存失效(用户信息更新时调用)
}
}
2. 批量权限检查¶
/**
* 批量权限检查
*/
@Service
public class BatchAuthorizationService {
@Autowired
private PermissionRepository permissionRepository;
/**
* 批量检查权限
*/
public Map<String, Boolean> checkPermissionsBatch(String userId, List<String> permissions) {
// 一次查询获取用户所有权限
Set<String> userPermissions = permissionRepository.findByUserId(userId);
// 批量判断
return permissions.stream()
.collect(Collectors.toMap(
permission -> permission,
permission -> userPermissions.contains(permission)
));
}
}
3. JWT签名算法选择¶
性能对比(相对速度):
HMAC (HS256): ⭐⭐⭐⭐⭐ (最快)
ECDSA (ES256): ⭐⭐⭐⭐
RSA (RS256): ⭐⭐⭐
安全性对比:
ECDSA (ES256): ⭐⭐⭐⭐⭐ (相同密钥长度下最安全)
RSA (RS256): ⭐⭐⭐⭐
HMAC (HS256): ⭐⭐⭐ (需要安全传输密钥)
推荐:
- 内部系统:HMAC (HS256)
- 公开API:ECDSA (ES256) 或 RSA (RS256)
4. 数据库查询优化¶
/**
* 优化用户权限查询
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// ❌ 错误:N+1查询问题
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(String username);
// ✅ 正确:使用JOIN FETCH一次性加载
@Query("SELECT u FROM User u " +
"LEFT JOIN FETCH u.roles r " +
"LEFT JOIN FETCH r.permissions " +
"WHERE u.username = :username")
User findByUsernameWithRolesAndPermissions(@Param("username") String username);
// 或使用EntityGraph
@EntityGraph(attributePaths = {"roles", "roles.permissions"})
Optional<User> findWithRolesByUsername(String username);
}
5. 连接池配置¶
# HikariCP配置
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
# Redis连接池
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.min-idle=5
审计日志与监控¶
审计日志¶
/**
* 审计事件
*/
@Entity
@Table(name = "audit_logs")
@Data
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String action; // LOGIN, LOGOUT, CREATE, UPDATE, DELETE
private String resource; // users, orders, etc.
private String resourceId;
private String ipAddress;
private String userAgent;
private boolean success;
private String failureReason;
@Column(columnDefinition = "TEXT")
private String details; // JSON格式的详细信息
@CreationTimestamp
private LocalDateTime timestamp;
}
/**
* 审计服务
*/
@Service
@Async
public class AuditService {
@Autowired
private AuditLogRepository auditLogRepository;
/**
* 记录登录事件
*/
public void logLogin(String username, String ipAddress, boolean success, String failureReason) {
AuditLog log = new AuditLog();
log.setUsername(username);
log.setAction("LOGIN");
log.setIpAddress(ipAddress);
log.setSuccess(success);
log.setFailureReason(failureReason);
auditLogRepository.save(log);
}
/**
* 记录资源访问
*/
public void logResourceAccess(String username, String action, String resource,
String resourceId, boolean success) {
AuditLog log = new AuditLog();
log.setUsername(username);
log.setAction(action);
log.setResource(resource);
log.setResourceId(resourceId);
log.setSuccess(success);
auditLogRepository.save(log);
}
}
/**
* 审计切面
*/
@Aspect
@Component
public class AuditAspect {
@Autowired
private AuditService auditService;
@AfterReturning(
pointcut = "@annotation(audited)",
returning = "result"
)
public void auditSuccess(JoinPoint joinPoint, Audited audited, Object result) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth != null ? auth.getName() : "anonymous";
auditService.logResourceAccess(
username,
audited.action(),
audited.resource(),
extractResourceId(joinPoint, result),
true
);
}
@AfterThrowing(
pointcut = "@annotation(audited)",
throwing = "ex"
)
public void auditFailure(JoinPoint joinPoint, Audited audited, Exception ex) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth != null ? auth.getName() : "anonymous";
auditService.logResourceAccess(
username,
audited.action(),
audited.resource(),
null,
false
);
}
}
/**
* 审计注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
String action();
String resource();
}
/**
* 使用示例
*/
@Service
public class UserService {
@Audited(action = "CREATE", resource = "users")
public User createUser(User user) {
return userRepository.save(user);
}
@Audited(action = "DELETE", resource = "users")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
监控指标¶
/**
* 安全监控指标
*/
@Component
public class SecurityMetrics {
private final Counter loginAttempts;
private final Counter loginFailures;
private final Counter loginSuccesses;
private final Counter accessDenied;
private final Gauge activeUsers;
public SecurityMetrics(MeterRegistry registry) {
this.loginAttempts = Counter.builder("security.login.attempts")
.description("Total login attempts")
.register(registry);
this.loginFailures = Counter.builder("security.login.failures")
.tag("reason", "invalid_credentials")
.description("Failed login attempts")
.register(registry);
this.loginSuccesses = Counter.builder("security.login.successes")
.description("Successful logins")
.register(registry);
this.accessDenied = Counter.builder("security.access.denied")
.description("Access denied events")
.register(registry);
this.activeUsers = Gauge.builder("security.users.active", this, SecurityMetrics::getActiveUserCount)
.description("Number of active users")
.register(registry);
}
public void recordLoginAttempt() {
loginAttempts.increment();
}
public void recordLoginSuccess() {
loginSuccesses.increment();
}
public void recordLoginFailure() {
loginFailures.increment();
}
public void recordAccessDenied() {
accessDenied.increment();
}
private double getActiveUserCount() {
// 实现活跃用户数统计
return sessionRegistry.getAllPrincipals().size();
}
}
安全告警¶
/**
* 安全告警服务
*/
@Service
public class SecurityAlertService {
@Autowired
private NotificationService notificationService;
@Autowired
private AuditLogRepository auditLogRepository;
/**
* 检测暴力破解攻击
*/
@Scheduled(fixedRate = 60000) // 每分钟
public void detectBruteForceAttacks() {
LocalDateTime oneMinuteAgo = LocalDateTime.now().minusMinutes(1);
// 查找1分钟内失败次数超过5次的IP
List<Map<String, Object>> suspiciousIps = auditLogRepository
.findFailedLoginsByIpSince(oneMinuteAgo, 5);
for (Map<String, Object> entry : suspiciousIps) {
String ipAddress = (String) entry.get("ipAddress");
Long failureCount = (Long) entry.get("failureCount");
notificationService.sendAlert(
"可疑的暴力破解攻击",
String.format("IP %s 在1分钟内尝试登录失败 %d 次", ipAddress, failureCount)
);
// 可以考虑自动封禁IP
// ipBlocklistService.blockIp(ipAddress, Duration.ofHours(1));
}
}
/**
* 检测异常访问模式
*/
@Scheduled(fixedRate = 300000) // 每5分钟
public void detectAnomalousAccess() {
LocalDateTime fiveMinutesAgo = LocalDateTime.now().minusMinutes(5);
// 检测短时间内大量资源访问
List<Map<String, Object>> heavyUsers = auditLogRepository
.findUsersWithHighActivitySince(fiveMinutesAgo, 100);
for (Map<String, Object> entry : heavyUsers) {
String username = (String) entry.get("username");
Long requestCount = (Long) entry.get("requestCount");
notificationService.sendAlert(
"异常访问模式",
String.format("用户 %s 在5分钟内发起了 %d 次请求", username, requestCount)
);
}
}
}
安全最佳实践汇总¶
1. 密码安全¶
/**
* 密码安全最佳实践
*/
public class PasswordSecurityBestPractices {
// ✅ 使用强加密算法
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength 12
}
// ✅ 密码复杂度验证
public boolean validatePasswordStrength(String password) {
return password.length() >= 12 &&
password.matches(".*[A-Z].*") &&
password.matches(".*[a-z].*") &&
password.matches(".*[0-9].*") &&
password.matches(".*[!@#$%^&*].*");
}
// ✅ 密码历史记录(防止重用)
public boolean checkPasswordHistory(String userId, String newPassword) {
List<String> previousPasswords = passwordHistoryRepository
.findLastNPasswords(userId, 5);
return previousPasswords.stream()
.noneMatch(oldPassword ->
passwordEncoder.matches(newPassword, oldPassword));
}
// ✅ 强制定期更换密码
public boolean isPasswordExpired(User user) {
return user.getPasswordChangedAt()
.isBefore(LocalDateTime.now().minusDays(90));
}
}
2. Token安全¶
/**
* Token安全最佳实践
*/
public class TokenSecurityBestPractices {
// ✅ 短期Access Token + 长期Refresh Token
private static final long ACCESS_TOKEN_VALIDITY = 15 * 60 * 1000; // 15分钟
private static final long REFRESH_TOKEN_VALIDITY = 7 * 24 * 60 * 60 * 1000; // 7天
// ✅ Token轮换(每次刷新时生成新的Refresh Token)
public TokenPair refreshToken(String refreshToken) {
if (!validateRefreshToken(refreshToken)) {
throw new InvalidTokenException();
}
// 生成新的Token对
String newAccessToken = generateAccessToken(user);
String newRefreshToken = generateRefreshToken(user);
// 废弃旧的Refresh Token
refreshTokenRepository.deleteByToken(refreshToken);
refreshTokenRepository.save(newRefreshToken);
return new TokenPair(newAccessToken, newRefreshToken);
}
// ✅ Token黑名单(提前吊销)
public void revokeToken(String token) {
Claims claims = parseToken(token);
long expirationTime = claims.getExpiration().getTime();
long ttl = expirationTime - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(
"blacklist:" + token,
"1",
ttl,
TimeUnit.MILLISECONDS
);
}
}
}
3. API安全¶
/**
* API安全最佳实践
*/
@Configuration
public class ApiSecurityConfig {
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
// ✅ HTTPS Only
.requiresChannel(channel -> channel
.anyRequest().requiresSecure()
)
// ✅ CORS配置
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// ✅ 安全响应头
.headers(headers -> headers
.contentSecurityPolicy("default-src 'self'")
.frameOptions().deny()
.xssProtection().enable()
.contentTypeOptions().enable()
.httpStrictTransportSecurity()
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
// ✅ 速率限制
.addFilterBefore(rateLimitFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// ✅ CORS配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://trusted-domain.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setExposedHeaders(Arrays.asList("X-Total-Count"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}
4. 数据安全¶
/**
* 数据安全最佳实践
*/
public class DataSecurityBestPractices {
// ✅ 敏感数据加密存储
@Component
public class DataEncryptionService {
private final Cipher cipher;
private final SecretKey secretKey;
public String encrypt(String plaintext) {
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encrypted = cipher.doFinal(plaintext.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
}
public String decrypt(String ciphertext) {
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decoded = Base64.getDecoder().decode(ciphertext);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted);
}
}
// ✅ 数据脱敏
public String maskSensitiveData(String data, DataType type) {
switch (type) {
case PHONE:
return data.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
case EMAIL:
return data.replaceAll("(^\\w{1,3}).*(@.*)", "$1***$2");
case ID_CARD:
return data.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
default:
return data;
}
}
// ✅ SQL注入防护
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 使用参数化查询
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
// ❌ 不要使用字符串拼接
// @Query("SELECT u FROM User u WHERE u.username = '" + username + "'")
}
}
5. 日志安全¶
/**
* 日志安全最佳实践
*/
@Component
public class SecureLogging {
private static final Logger logger = LoggerFactory.getLogger(SecureLogging.class);
// ✅ 不要记录敏感信息
public void logUserAction(User user, String action) {
logger.info("User action: user={}, action={}",
user.getUsername(), // ✅ OK
action);
// ❌ 不要记录密码、Token等
// logger.info("Password: {}", user.getPassword());
// logger.info("Token: {}", token);
}
// ✅ 使用日志级别
public void logSecurityEvent(SecurityEvent event) {
switch (event.getSeverity()) {
case CRITICAL:
logger.error("Security event: {}", event);
break;
case WARNING:
logger.warn("Security event: {}", event);
break;
case INFO:
logger.info("Security event: {}", event);
break;
}
}
// ✅ 结构化日志
public void logStructured(String username, String action, boolean success) {
logger.info("action={}, username={}, success={}, timestamp={}",
action, username, success, Instant.now());
}
}
总结¶
本章介绍了认证授权的高级主题:
- 多租户认证:数据隔离、租户识别、上下文管理
- 分布式会话:Redis Session、Session监控、集群管理
- 微服务安全:网关认证、服务间调用、分布式追踪
- 性能优化:缓存策略、批量处理、数据库优化
- 审计监控:日志记录、指标监控、安全告警
- 最佳实践:密码、Token、API、数据、日志安全
关键要点: - 安全是系统性工程,需要多层防护 - 性能和安全需要平衡 - 持续监控和审计至关重要 - 遵循最小权限原则 - 定期安全审查和更新
继续学习: - 返回首页:认证授权基础 - OWASP Top 10 - Spring Security官方文档
完整学习路径¶
- 认证授权基础 - 核心概念
- 认证协议与标准 - OAuth2、JWT、SSO
- Java认证框架对比 - 框架选型
- Spring Security核心架构 - 架构原理
- Spring Security实战配置 - 实践配置
- Spring Security OAuth2集成 - OAuth2实现
- 代码示例集 - 完整示例
- 高级主题与最佳实践 - 本章
恭喜你完成认证授权知识体系的学习! 🎉