-
一、监控体系设计1.1 关键监控指标java@Componentpublic class SSOMetricsCollector { private final MeterRegistry meterRegistry; // 计数器指标 private final Counter loginSuccessCounter; private final Counter loginFailureCounter; private final Counter tokenIssuedCounter; private final Counter tokenRefreshedCounter; private final Counter tokenRevokedCounter; // 计时器指标 private final Timer loginProcessingTimer; private final Timer tokenValidationTimer; private final Timer userInfoRetrievalTimer; // 仪表盘指标 private final Gauge activeSessionsGauge; private final Gauge activeTokensGauge; private final Gauge concurrentRequestsGauge; public SSOMetricsCollector(MeterRegistry meterRegistry, SessionService sessionService, TokenService tokenService) { this.meterRegistry = meterRegistry; // 初始化计数器 loginSuccessCounter = Counter.builder("sso.login.success") .description("Successful login attempts") .tag("type", "total") .register(meterRegistry); loginFailureCounter = Counter.builder("sso.login.failure") .description("Failed login attempts") .tag("type", "total") .register(meterRegistry); tokenIssuedCounter = Counter.builder("sso.token.issued") .description("Tokens issued") .register(meterRegistry); // 初始化计时器 loginProcessingTimer = Timer.builder("sso.login.duration") .description("Time spent processing login requests") .publishPercentiles(0.5, 0.95, 0.99) .register(meterRegistry); // 初始化仪表盘 activeSessionsGauge = Gauge.builder("sso.sessions.active", sessionService::getActiveSessionCount) .description("Number of active sessions") .register(meterRegistry); activeTokensGauge = Gauge.builder("sso.tokens.active", tokenService::getActiveTokenCount) .description("Number of active tokens") .register(meterRegistry); } public void recordLoginSuccess(String username, String clientId, long duration) { loginSuccessCounter.increment(); // 按用户和客户端统计 meterRegistry.counter("sso.login.success", "username", username, "client_id", clientId ).increment(); loginProcessingTimer.record(duration, TimeUnit.MILLISECONDS); } public void recordLoginFailure(String username, String clientId, String reason) { loginFailureCounter.increment(); meterRegistry.counter("sso.login.failure", "username", username, "client_id", clientId, "reason", reason ).increment(); } public void recordTokenIssued(String tokenType, String username, String clientId) { tokenIssuedCounter.increment(); meterRegistry.counter("sso.token.issued.detail", "token_type", tokenType, "username", username, "client_id", clientId ).increment(); } public Map<String, Object> getHealthMetrics() { Map<String, Object> metrics = new HashMap<>(); // 系统健康指标 metrics.put("status", "UP"); metrics.put("timestamp", Instant.now().toString()); // 性能指标 metrics.put("login_success_rate", calculateSuccessRate()); metrics.put("avg_login_duration_ms", getAverageLoginDuration()); metrics.put("active_sessions", activeSessionsGauge.value()); metrics.put("active_tokens", activeTokensGauge.value()); // 系统资源指标 Runtime runtime = Runtime.getRuntime(); metrics.put("memory_used_mb", (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024); metrics.put("memory_max_mb", runtime.maxMemory() / 1024 / 1024); metrics.put("available_processors", runtime.availableProcessors()); return metrics; } private double calculateSuccessRate() { double success = loginSuccessCounter.count(); double failure = loginFailureCounter.count(); double total = success + failure; return total > 0 ? (success / total) * 100 : 0; } private double getAverageLoginDuration() { return loginProcessingTimer.mean(TimeUnit.MILLISECONDS); }}1.2 实时监控端点java@RestController@RequestMapping("/api/monitor")public class MonitorController { @Autowired private SSOMetricsCollector metricsCollector; @Autowired private AuditLogService auditLogService; @GetMapping("/health") public ResponseEntity<Map<String, Object>> health() { Map<String, Object> healthInfo = new HashMap<>(); healthInfo.put("service", "SSO Service"); healthInfo.put("status", "UP"); healthInfo.put("version", "1.0.0"); healthInfo.put("timestamp", Instant.now().toString()); // 添加指标数据 healthInfo.put("metrics", metricsCollector.getHealthMetrics()); // 检查依赖服务 healthInfo.put("dependencies", checkDependencies()); return ResponseEntity.ok(healthInfo); } @GetMapping("/metrics") public ResponseEntity<Map<String, Object>> metrics() { Map<String, Object> metrics = new HashMap<>(); // JVM指标 metrics.put("jvm", getJvmMetrics()); // 应用指标 metrics.put("application", getApplicationMetrics()); // 业务指标 metrics.put("business", getBusinessMetrics()); // 性能指标 metrics.put("performance", getPerformanceMetrics()); return ResponseEntity.ok(metrics); } @GetMapping("/audit/recent") public ResponseEntity<Page<AuditLog>> getRecentAuditLogs( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "50") int size) { Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "timestamp")); Page<AuditLog> logs = auditLogService.getRecentLogs(pageable); return ResponseEntity.ok(logs); } @GetMapping("/audit/search") public ResponseEntity<List<AuditLog>> searchAuditLogs( @RequestParam(required = false) String username, @RequestParam(required = false) String action, @RequestParam(required = false) String resource, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate) { List<AuditLog> logs = auditLogService.searchLogs( username, action, resource, fromDate, toDate); return ResponseEntity.ok(logs); } @GetMapping("/alerts") public ResponseEntity<List<Alert>> getActiveAlerts() { List<Alert> alerts = alertService.getActiveAlerts(); return ResponseEntity.ok(alerts); } private Map<String, Object> getJvmMetrics() { Map<String, Object> jvmMetrics = new HashMap<>(); Runtime runtime = Runtime.getRuntime(); jvmMetrics.put("memory", Map.of( "used", runtime.totalMemory() - runtime.freeMemory(), "max", runtime.maxMemory(), "free", runtime.freeMemory() )); jvmMetrics.put("threads", Map.of( "active", Thread.activeCount(), "peak", ManagementFactory.getThreadMXBean().getPeakThreadCount() )); jvmMetrics.put("gc", getGarbageCollectionMetrics()); return jvmMetrics; } private Map<String, Object> getGarbageCollectionMetrics() { Map<String, Object> gcMetrics = new HashMap<>(); for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { gcMetrics.put(gcBean.getName(), Map.of( "count", gcBean.getCollectionCount(), "time", gcBean.getCollectionTime() )); } return gcMetrics; }}二、结构化日志2.1 Logback配置xml<?xml version="1.0" encoding="UTF-8"?><configuration> <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/> <property name="JSON_LOG_PATTERN" value='{"timestamp":"%d{yyyy-MM-dd HH:mm:ss.SSS}", "level":"%level", "thread":"%thread", "logger":"%logger", "message":"%msg", "tenant":"%X{tenantId}", "user":"%X{username}", "traceId":"%X{traceId}", "spanId":"%X{spanId}"}%n'/> <!-- 控制台输出(开发环境) --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <!-- JSON文件输出(生产环境) --> <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/sso-service.json</file> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <customFields>{"service":"sso-service","environment":"${ENV:-dev}"}</customFields> <includeContext>true</includeContext> <includeMdc>true</includeMdc> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/sso-service-%d{yyyy-MM-dd}.json.gz</fileNamePattern> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> </appender> <!-- 审计日志单独文件 --> <appender name="AUDIT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/audit.log</file> <encoder> <pattern>${JSON_LOG_PATTERN}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/audit-%d{yyyy-MM-dd}.log.gz</fileNamePattern> <maxHistory>90</maxHistory> </rollingPolicy> </appender> <!-- 安全日志单独文件 --> <appender name="SECURITY_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/security.log</file> <encoder> <pattern>${JSON_LOG_PATTERN}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/security-%d{yyyy-MM-dd}.log.gz</fileNamePattern> <maxHistory>90</maxHistory> </rollingPolicy> </appender> <!-- Logger配置 --> <logger name="com.example.sso.audit" level="INFO" additivity="false"> <appender-ref ref="AUDIT_FILE"/> </logger> <logger name="com.example.sso.security" level="WARN" additivity="false"> <appender-ref ref="SECURITY_FILE"/> </logger> <!-- 第三方库日志级别 --> <logger name="org.springframework.security" level="INFO"/> <logger name="org.springframework.web" level="INFO"/> <logger name="org.hibernate" level="WARN"/> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="JSON_FILE"/> </root> </configuration>2.2 审计日志服务java@Service@Slf4jpublic class AuditLogService { @Autowired private AuditLogRepository auditLogRepository; @Autowired private MDCService mdcService; public void logAuthenticationSuccess(String username, String clientId, String ipAddress, String userAgent) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(username) .action("LOGIN_SUCCESS") .resource("AUTHENTICATION") .details(Map.of( "client_id", clientId, "ip_address", ipAddress, "user_agent", userAgent, "result", "SUCCESS" )) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); // 同时输出到日志文件 log.info("用户 {} 登录成功,客户端: {},IP: {}", username, clientId, ipAddress); } public void logAuthenticationFailure(String username, String clientId, String ipAddress, String userAgent, String failureReason) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(username) .action("LOGIN_FAILURE") .resource("AUTHENTICATION") .details(Map.of( "client_id", clientId, "ip_address", ipAddress, "user_agent", userAgent, "failure_reason", failureReason, "result", "FAILURE" )) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); // 安全告警日志 log.warn("用户 {} 登录失败,原因: {},IP: {}", username, failureReason, ipAddress); } public void logTokenIssued(String username, String tokenType, String clientId, Map<String, Object> tokenDetails) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(username) .action("TOKEN_ISSUED") .resource("TOKEN") .details(Map.of( "token_type", tokenType, "client_id", clientId, "token_details", tokenDetails )) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); } public void logAccess(String username, String resource, String action, boolean allowed, Map<String, Object> accessDetails) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(username) .action(action) .resource(resource) .details(Map.of( "access_allowed", allowed, "access_details", accessDetails )) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); } public void logAdminAction(String adminUsername, String action, String targetResource, Map<String, Object> actionDetails) { AuditLog log = AuditLog.builder() .timestamp(Instant.now()) .username(adminUsername) .action("ADMIN_" + action) .resource(targetResource) .details(actionDetails) .tenantId(mdcService.getTenantId()) .traceId(mdcService.getTraceId()) .build(); auditLogRepository.save(log); // 管理员操作需要特别记录 log.info("管理员 {} 执行操作: {},目标资源: {}", adminUsername, action, targetResource); } public Page<AuditLog> searchLogs(String username, String action, String resource, LocalDate fromDate, LocalDate toDate, Pageable pageable) { Specification<AuditLog> spec = Specification.where(null); if (username != null) { spec = spec.and((root, query, cb) -> cb.equal(root.get("username"), username)); } if (action != null) { spec = spec.and((root, query, cb) -> cb.equal(root.get("action"), action)); } if (resource != null) { spec = spec.and((root, query, cb) -> cb.equal(root.get("resource"), resource)); } if (fromDate != null) { spec = spec.and((root, query, cb) -> cb.greaterThanOrEqualTo(root.get("timestamp"), fromDate.atStartOfDay())); } if (toDate != null) { spec = spec.and((root, query, cb) -> cb.lessThan(root.get("timestamp"), toDate.plusDays(1).atStartOfDay())); } return auditLogRepository.findAll(spec, pageable); } public List<AuditStat> getAuditStatistics(LocalDate fromDate, LocalDate toDate) { List<AuditStat> stats = new ArrayList<>(); // 按操作类型统计 List<Object[]> actionStats = auditLogRepository .countByActionType(fromDate.atStartOfDay(), toDate.plusDays(1).atStartOfDay()); for (Object[] row : actionStats) { AuditStat stat = new AuditStat(); stat.setCategory("ACTION_TYPE"); stat.setKey((String) row[0]); stat.setValue(((Long) row[1]).intValue()); stats.add(stat); } // 按用户统计 List<Object[]> userStats = auditLogRepository .countByUser(fromDate.atStartOfDay(), toDate.plusDays(1).atStartOfDay(), 10); // 前10个 for (Object[] row : userStats) { AuditStat stat = new AuditStat(); stat.setCategory("TOP_USERS"); stat.setKey((String) row[0]); stat.setValue(((Long) row[1]).intValue()); stats.add(stat); } // 按时间段统计 List<Object[]> hourlyStats = auditLogRepository .countByHour(fromDate.atStartOfDay(), toDate.plusDays(1).atStartOfDay()); for (Object[] row : hourlyStats) { AuditStat stat = new AuditStat(); stat.setCategory("HOURLY_DISTRIBUTION"); stat.setKey("HOUR_" + row[0]); stat.setValue(((Long) row[1]).intValue()); stats.add(stat); } return stats; }}三、安全审计跟踪3.1 合规性审计java@Componentpublic class ComplianceAuditService { @Autowired private AuditLogRepository auditLogRepository; @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行 public void generateDailyComplianceReport() { LocalDate yesterday = LocalDate.now().minusDays(1); ComplianceReport report = new ComplianceReport(); report.setReportDate(yesterday); report.setGeneratedAt(Instant.now()); // 收集审计数据 report.setLoginStats(getLoginStatistics(yesterday)); report.setAccessStats(getAccessStatistics(yesterday)); report.setSecurityEvents(getSecurityEvents(yesterday)); report.setAdminActions(getAdminActions(yesterday)); // 检查合规性规则 report.setComplianceChecks(runComplianceChecks(yesterday)); // 生成报告文件 String reportContent = generateReportContent(report); saveReportToStorage(reportContent, yesterday); // 发送报告给相关人员 sendReportByEmail(report); log.info("生成合规性报告完成: {}", yesterday); } private Map<String, Object> getLoginStatistics(LocalDate date) { Map<String, Object> stats = new HashMap<>(); // 总登录次数 Long totalLogins = auditLogRepository.countLoginsByDate( date.atStartOfDay(), date.plusDays(1).atStartOfDay()); stats.put("total_logins", totalLogins); // 成功/失败次数 Long successLogins = auditLogRepository.countSuccessfulLoginsByDate( date.atStartOfDay(), date.plusDays(1).atStartOfDay()); Long failedLogins = auditLogRepository.countFailedLoginsByDate( date.atStartOfDay(), date.plusDays(1).atStartOfDay()); stats.put("successful_logins", successLogins); stats.put("failed_logins", failedLogins); stats.put("success_rate", totalLogins > 0 ? (double) successLogins / totalLogins * 100 : 0); // 唯一用户数 Long uniqueUsers = auditLogRepository.countUniqueUsersByDate( date.atStartOfDay(), date.plusDays(1).atStartOfDay()); stats.put("unique_users", uniqueUsers); return stats; } private List<ComplianceCheck> runComplianceChecks(LocalDate date) { List<ComplianceCheck> checks = new ArrayList<>(); // 检查1: 是否有多次失败登录 ComplianceCheck check1 = new ComplianceCheck(); check1.setCheckName("多次失败登录检查"); check1.setDescription("检查是否有账户存在多次失败登录"); List<FailedLoginAttempt> failedAttempts = auditLogRepository .findExcessiveFailedLogins(date.atStartOfDay(), date.plusDays(1).atStartOfDay(), 5); check1.setPassed(failedAttempts.isEmpty()); check1.setDetails(failedAttempts); checks.add(check1); // 检查2: 管理员操作审计 ComplianceCheck check2 = new ComplianceCheck(); check2.setCheckName("管理员操作审计"); check2.setDescription("检查管理员关键操作是否都有记录"); List<AdminAction> adminActions = auditLogRepository .findAdminActionsWithoutAudit(date.atStartOfDay(), date.plusDays(1).atStartOfDay()); check2.setPassed(adminActions.isEmpty()); check2.setDetails(adminActions); checks.add(check2); // 检查3: 令牌使用合规性 ComplianceCheck check3 = new ComplianceCheck(); check3.setCheckName("令牌合规性检查"); check3.setDescription("检查令牌颁发和使用是否符合策略"); List<TokenViolation> tokenViolations = tokenService .findTokenPolicyViolations(date.atStartOfDay(), date.plusDays(1).atStartOfDay()); check3.setPassed(tokenViolations.isEmpty()); check3.setDetails(tokenViolations); checks.add(check3); return checks; }}3.2 实时告警系统java@Servicepublic class RealTimeAlertService { @Autowired private AlertRepository alertRepository; @Autowired private NotificationService notificationService; private final Map<String, AlertRule> alertRules = new ConcurrentHashMap<>(); @PostConstruct public void initAlertRules() { // 初始化告警规则 alertRules.put("MULTIPLE_FAILED_LOGINS", AlertRule.builder() .ruleId("MULTIPLE_FAILED_LOGINS") .name("多次登录失败") .description("同一账户在短时间内多次登录失败") .threshold(5) .timeWindow(300) // 5分钟 .severity(AlertSeverity.HIGH) .enabled(true) .build()); alertRules.put("SUSPICIOUS_IP", AlertRule.builder() .ruleId("SUSPICIOUS_IP") .name("可疑IP地址") .description("来自异常地理位置的登录") .threshold(1) .timeWindow(3600) // 1小时 .severity(AlertSeverity.MEDIUM) .enabled(true) .build()); alertRules.put("TOKEN_ABUSE", AlertRule.builder() .ruleId("TOKEN_ABUSE") .name("令牌滥用") .description("同一令牌在短时间内被频繁使用") .threshold(100) .timeWindow(60) // 1分钟 .severity(AlertSeverity.HIGH) .enabled(true) .build()); } @EventListener public void handleLoginFailure(AuthenticationFailureEvent event) { String username = event.getUsername(); String ipAddress = event.getIpAddress(); // 检查是否触发告警规则 checkMultipleFailedLogins(username, ipAddress); checkSuspiciousLocation(username, ipAddress); } @EventListener public void handleTokenUsage(TokenUsageEvent event) { String token = event.getToken(); String username = event.getUsername(); // 检查令牌使用频率 checkTokenUsageFrequency(token, username); } private void checkMultipleFailedLogins(String username, String ipAddress) { AlertRule rule = alertRules.get("MULTIPLE_FAILED_LOGINS"); if (!rule.isEnabled()) { return; } // 查询最近失败次数 long failedCount = auditLogRepository.countFailedLogins( username, ipAddress, rule.getTimeWindow()); if (failedCount >= rule.getThreshold()) { // 触发告警 Alert alert = Alert.builder() .alertId(UUID.randomUUID().toString()) .ruleId(rule.getRuleId()) .severity(rule.getSeverity()) .title("多次登录失败告警") .description(String.format("用户 %s 在 %d 分钟内登录失败 %d 次", username, rule.getTimeWindow() / 60, failedCount)) .details(Map.of( "username", username, "ip_address", ipAddress, "failed_count", failedCount, "time_window_minutes", rule.getTimeWindow() / 60 )) .triggeredAt(Instant.now()) .status(AlertStatus.ACTIVE) .build(); saveAlert(alert); sendAlertNotifications(alert); } } private void checkSuspiciousLocation(String username, String ipAddress) { // 检查IP地理位置 GeoLocation location = ipGeoService.getLocation(ipAddress); if (location == null) { return; } // 获取用户常用登录地点 List<GeoLocation> usualLocations = userService.getUsualLocations(username); // 检查是否与常用地点差异较大 boolean isSuspicious = usualLocations.stream() .noneMatch(usual -> isNearby(usual, location, 100)); // 100公里内 if (isSuspicious) { Alert alert = Alert.builder() .alertId(UUID.randomUUID().toString()) .ruleId("SUSPICIOUS_IP") .severity(AlertSeverity.MEDIUM) .title("可疑地理位置登录") .description(String.format("用户 %s 从异常位置登录: %s", username, location.getCity())) .details(Map.of( "username", username, "ip_address", ipAddress, "location", location.toString(), "usual_locations", usualLocations.toString() )) .triggeredAt(Instant.now()) .status(AlertStatus.ACTIVE) .build(); saveAlert(alert); sendAlertNotifications(alert); } } private void checkTokenUsageFrequency(String token, String username) { AlertRule rule = alertRules.get("TOKEN_ABUSE"); if (!rule.isEnabled()) { return; } // 检查令牌使用频率 long usageCount = tokenUsageService.getUsageCount( token, rule.getTimeWindow()); if (usageCount >= rule.getThreshold()) { Alert alert = Alert.builder() .alertId(UUID.randomUUID().toString()) .ruleId(rule.getRuleId()) .severity(rule.getSeverity()) .title("令牌滥用告警") .description(String.format("令牌在 %d 秒内被使用 %d 次", rule.getTimeWindow(), usageCount)) .details(Map.of( "username", username, "token", maskToken(token), "usage_count", usageCount, "time_window_seconds", rule.getTimeWindow() )) .triggeredAt(Instant.now()) .status(AlertStatus.ACTIVE) .build(); saveAlert(alert); sendAlertNotifications(alert); // 可以自动采取措施,如临时锁定账户 userService.temporarilyLockAccount(username, 300); // 锁定5分钟 } } private void saveAlert(Alert alert) { alertRepository.save(alert); log.warn("触发安全告警: {}", alert.getTitle()); } private void sendAlertNotifications(Alert alert) { // 发送给安全团队 notificationService.sendSecurityAlert(alert); // 发送给用户(如果是其账户相关) if (alert.getDetails().containsKey("username")) { String username = (String) alert.getDetails().get("username"); notificationService.sendUserSecurityAlert(username, alert); } // 记录到审计日志 auditLogService.logSecurityAlert(alert); } private String maskToken(String token) { if (token == null || token.length() <= 8) { return "***"; } return token.substring(0, 4) + "..." + token.substring(token.length() - 4); } private boolean isNearby(GeoLocation loc1, GeoLocation loc2, double maxDistanceKm) { // 计算两个地理位置之间的距离 double distance = calculateDistance(loc1, loc2); return distance <= maxDistanceKm; }}
-
一、多租户架构设计1.1 租户隔离策略java@Configurationpublic class MultiTenantConfig { @Bean public CurrentTenantIdentifierResolver tenantIdentifierResolver() { return new CurrentTenantIdentifierResolver() { @Override public String resolveCurrentTenantIdentifier() { // 从请求头获取租户ID RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes instanceof ServletRequestAttributes) { HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); String tenantId = request.getHeader("X-Tenant-Id"); if (tenantId == null) { // 从域名解析租户 tenantId = extractTenantFromDomain(request.getServerName()); } return tenantId != null ? tenantId : "default"; } return "default"; } @Override public boolean validateExistingCurrentSessions() { return false; } }; } @Bean public MultiTenantConnectionProvider multiTenantConnectionProvider() { return new MultiTenantConnectionProvider() { @Override public Connection getConnection(String tenantIdentifier) throws SQLException { DataSource dataSource = getTenantDataSource(tenantIdentifier); return dataSource.getConnection(); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { connection.close(); } @Override public boolean supportsAggressiveRelease() { return true; } private DataSource getTenantDataSource(String tenantId) { // 根据租户ID获取对应的数据源 return dataSourceMap.get(tenantId); } }; } @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory( MultiTenantConnectionProvider multiTenantConnectionProvider, CurrentTenantIdentifierResolver tenantIdentifierResolver) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource()); em.setPackagesToScan("com.example.sso"); em.setJpaVendorAdapter(jpaVendorAdapter()); Map<String, Object> properties = new HashMap<>(); properties.put("hibernate.multiTenancy", "DATABASE"); properties.put("hibernate.tenant_identifier_resolver", tenantIdentifierResolver); properties.put("hibernate.multi_tenant_connection_provider", multiTenantConnectionProvider); em.setJpaPropertyMap(properties); return em; }}1.2 租户数据路由java@Componentpublic class TenantDataRouter { private final Map<String, TenantConfig> tenantConfigs = new ConcurrentHashMap<>(); public TenantConfig getTenantConfig(String tenantId) { return tenantConfigs.computeIfAbsent(tenantId, this::loadTenantConfig); } public String getTenantDatabase(String tenantId) { TenantConfig config = getTenantConfig(tenantId); if (config.getDatabaseStrategy() == DatabaseStrategy.SHARED) { return "shared_db"; } else if (config.getDatabaseStrategy() == DatabaseStrategy.ISOLATED) { return "tenant_db_" + tenantId; } else { // 分片策略 int shard = Math.abs(tenantId.hashCode()) % 10; return "shard_db_" + shard; } } public String getTenantRedisPrefix(String tenantId) { return "tenant:" + tenantId + ":"; } public String getTenantStoragePath(String tenantId) { TenantConfig config = getTenantConfig(tenantId); if (config.getStorageStrategy() == StorageStrategy.SHARED) { return "/storage/shared/" + tenantId + "/"; } else { return "/storage/tenants/" + tenantId + "/"; } } private TenantConfig loadTenantConfig(String tenantId) { // 从数据库或配置中心加载租户配置 TenantConfig config = new TenantConfig(); config.setTenantId(tenantId); config.setDatabaseStrategy(DatabaseStrategy.ISOLATED); config.setStorageStrategy(StorageStrategy.SHARED); config.setCustomLoginPageEnabled(true); config.setBrandingEnabled(true); config.setMaxUsers(1000); config.setActive(true); return config; }}二、租户定制化2.1 动态登录页面java@Controller@RequestMapping("/{tenantId}/login")public class TenantLoginController { @Autowired private TenantService tenantService; @GetMapping public String loginPage(@PathVariable String tenantId, Model model, HttpServletRequest request) { TenantConfig tenantConfig = tenantService.getTenantConfig(tenantId); if (!tenantConfig.isActive()) { return "error/tenant-inactive"; } // 设置租户特定的属性 model.addAttribute("tenantId", tenantId); model.addAttribute("tenantName", tenantConfig.getTenantName()); model.addAttribute("logoUrl", tenantConfig.getLogoUrl()); model.addAttribute("primaryColor", tenantConfig.getPrimaryColor()); model.addAttribute("backgroundColor", tenantConfig.getBackgroundColor()); // 自定义字段 if (tenantConfig.getCustomFields() != null) { model.addAttribute("customFields", tenantConfig.getCustomFields()); } // 检查是否有自定义模板 String templatePath = findTenantTemplate(tenantId); if (templatePath != null) { return templatePath; } return "login/default"; } @PostMapping public String processLogin(@PathVariable String tenantId, @RequestParam String username, @RequestParam String password, HttpServletRequest request) { // 验证租户状态 if (!tenantService.isTenantActive(tenantId)) { return "redirect:/error/tenant-inactive"; } // 租户特定的认证逻辑 Authentication authentication = tenantAuthenticationService .authenticate(tenantId, username, password); if (authentication != null && authentication.isAuthenticated()) { // 设置租户上下文 TenantContext.setCurrentTenant(tenantId); // 生成租户特定的令牌 String token = tokenService.generateTenantToken( authentication, tenantId ); // 重定向到租户首页 return "redirect:/" + tenantId + "/dashboard"; } return "redirect:/" + tenantId + "/login?error"; } private String findTenantTemplate(String tenantId) { // 检查是否存在租户自定义模板 String[] templateLocations = { "classpath:templates/tenants/" + tenantId + "/login.html", "classpath:templates/tenants/" + tenantId + "/login.ftl", "file:/var/www/templates/" + tenantId + "/login.html" }; for (String location : templateLocations) { Resource resource = resourceLoader.getResource(location); if (resource.exists()) { return "tenants/" + tenantId + "/login"; } } return null; }}2.2 租户品牌管理java@Servicepublic class TenantBrandingService { @Autowired private StorageService storageService; public TenantBranding getBranding(String tenantId) { TenantBranding branding = cacheService.get("branding:" + tenantId, TenantBranding.class); if (branding == null) { branding = loadBrandingFromStorage(tenantId); cacheService.put("branding:" + tenantId, branding, 1, TimeUnit.HOURS); } return branding; } public void updateBranding(String tenantId, BrandingUpdateRequest request) { TenantBranding branding = new TenantBranding(); branding.setTenantId(tenantId); branding.setLogoUrl(request.getLogoUrl()); branding.setFaviconUrl(request.getFaviconUrl()); branding.setPrimaryColor(request.getPrimaryColor()); branding.setSecondaryColor(request.getSecondaryColor()); branding.setFontFamily(request.getFontFamily()); branding.setCustomCss(request.getCustomCss()); branding.setCustomJs(request.getCustomJs()); // 上传Logo文件 if (request.getLogoFile() != null && !request.getLogoFile().isEmpty()) { String logoPath = uploadBrandingFile(tenantId, "logo", request.getLogoFile()); branding.setLogoUrl(logoPath); } // 保存到存储 saveBrandingToStorage(tenantId, branding); // 清除缓存 cacheService.evict("branding:" + tenantId); // 生成CSS文件 generateBrandingCss(tenantId, branding); } private String uploadBrandingFile(String tenantId, String type, MultipartFile file) { String fileName = type + "_" + System.currentTimeMillis() + getFileExtension(file.getOriginalFilename()); String path = "tenants/" + tenantId + "/branding/" + fileName; storageService.upload(path, file); return storageService.getPublicUrl(path); } private void generateBrandingCss(String tenantId, TenantBranding branding) { String css = String.format(""" :root { --primary-color: %s; --secondary-color: %s; --font-family: %s; } .tenant-%s .header { background-color: %s; } .tenant-%s .btn-primary { background-color: %s; border-color: %s; } %s """, branding.getPrimaryColor(), branding.getSecondaryColor(), branding.getFontFamily(), tenantId, branding.getPrimaryColor(), tenantId, branding.getPrimaryColor(), branding.getSecondaryColor(), branding.getCustomCss() ); String cssPath = "tenants/" + tenantId + "/branding/custom.css"; storageService.upload(cssPath, css.getBytes()); }}三、租户配额管理java@Servicepublic class TenantQuotaService { @Autowired private TenantUsageRepository usageRepository; @Autowired private TenantQuotaRepository quotaRepository; public boolean checkUserQuota(String tenantId) { TenantQuota quota = quotaRepository.findByTenantId(tenantId); TenantUsage usage = usageRepository.findByTenantId(tenantId); if (quota == null || usage == null) { return true; // 无限制 } if (quota.getMaxUsers() > 0 && usage.getActiveUsers() >= quota.getMaxUsers()) { return false; } return true; } public boolean checkApiQuota(String tenantId, String apiEndpoint) { TenantQuota quota = quotaRepository.findByTenantId(tenantId); if (quota == null) { return true; } // 检查API调用频率限制 String rateLimitKey = "ratelimit:" + tenantId + ":" + apiEndpoint; Long count = redisTemplate.opsForValue().increment(rateLimitKey); if (count == 1) { redisTemplate.expire(rateLimitKey, 60, TimeUnit.SECONDS); } Integer rateLimit = quota.getApiRateLimits().get(apiEndpoint); if (rateLimit != null && count > rateLimit) { return false; } return true; } public boolean checkStorageQuota(String tenantId, long additionalBytes) { TenantQuota quota = quotaRepository.findByTenantId(tenantId); TenantUsage usage = usageRepository.findByTenantId(tenantId); if (quota == null || usage == null) { return true; } long newUsage = usage.getStorageUsed() + additionalBytes; if (quota.getMaxStorage() > 0 && newUsage > quota.getMaxStorage()) { return false; } return true; } public void recordApiCall(String tenantId, String apiEndpoint) { // 记录API调用 String key = "api_usage:" + tenantId + ":" + LocalDate.now().format(DateTimeFormatter.ISO_DATE); redisTemplate.opsForHash().increment(key, apiEndpoint, 1); redisTemplate.expire(key, 30, TimeUnit.DAYS); // 更新每日统计 updateDailyUsage(tenantId); } public TenantUsageStats getUsageStats(String tenantId, LocalDate startDate, LocalDate endDate) { TenantUsageStats stats = new TenantUsageStats(); stats.setTenantId(tenantId); stats.setPeriod(startDate + " - " + endDate); // 获取活跃用户数 stats.setActiveUsers(getActiveUsersCount(tenantId)); // 获取API调用统计 stats.setApiCalls(getApiCallsCount(tenantId, startDate, endDate)); // 获取存储使用量 stats.setStorageUsed(getStorageUsage(tenantId)); // 获取登录统计 stats.setLoginCount(getLoginCount(tenantId, startDate, endDate)); return stats; }}四、租户管理APIjava@RestController@RequestMapping("/api/admin/tenants")@PreAuthorize("hasRole('SUPER_ADMIN')")public class TenantAdminController { @Autowired private TenantService tenantService; @PostMapping public ResponseEntity<TenantResponse> createTenant( @RequestBody @Valid CreateTenantRequest request) { Tenant tenant = tenantService.createTenant(request); TenantResponse response = TenantResponse.builder() .tenantId(tenant.getTenantId()) .name(tenant.getName()) .status(tenant.getStatus()) .createdAt(tenant.getCreatedAt()) .adminEmail(tenant.getAdminEmail()) .build(); return ResponseEntity.status(HttpStatus.CREATED) .body(response); } @PutMapping("/{tenantId}") public ResponseEntity<TenantResponse> updateTenant( @PathVariable String tenantId, @RequestBody @Valid UpdateTenantRequest request) { Tenant tenant = tenantService.updateTenant(tenantId, request); TenantResponse response = TenantResponse.builder() .tenantId(tenant.getTenantId()) .name(tenant.getName()) .status(tenant.getStatus()) .updatedAt(tenant.getUpdatedAt()) .build(); return ResponseEntity.ok(response); } @PostMapping("/{tenantId}/suspend") public ResponseEntity<Void> suspendTenant( @PathVariable String tenantId, @RequestParam(required = false) String reason) { tenantService.suspendTenant(tenantId, reason); // 通知租户管理员 notificationService.sendTenantSuspendedNotification(tenantId, reason); return ResponseEntity.ok().build(); } @PostMapping("/{tenantId}/activate") public ResponseEntity<Void> activateTenant(@PathVariable String tenantId) { tenantService.activateTenant(tenantId); return ResponseEntity.ok().build(); } @GetMapping("/{tenantId}/usage") public ResponseEntity<TenantUsageResponse> getTenantUsage( @PathVariable String tenantId, @RequestParam(defaultValue = "30") int days) { LocalDate endDate = LocalDate.now(); LocalDate startDate = endDate.minusDays(days); TenantUsageResponse usage = tenantService.getTenantUsage( tenantId, startDate, endDate); return ResponseEntity.ok(usage); } @PostMapping("/{tenantId}/quota") public ResponseEntity<Void> updateQuota( @PathVariable String tenantId, @RequestBody @Valid QuotaUpdateRequest request) { tenantService.updateQuota(tenantId, request); // 记录配额变更 auditService.logQuotaChange(tenantId, request); return ResponseEntity.ok().build(); } @GetMapping("/{tenantId}/billing") public ResponseEntity<BillingInfoResponse> getBillingInfo( @PathVariable String tenantId, @RequestParam(defaultValue = "current") String period) { BillingInfoResponse billing = billingService.getBillingInfo( tenantId, period); return ResponseEntity.ok(billing); }}五、租户间数据隔离java@Componentpublic class TenantDataFilter { @Autowired private TenantContext tenantContext; public <T> Specification<T> tenantSpecification() { return (root, query, criteriaBuilder) -> { String currentTenant = tenantContext.getCurrentTenant(); if (currentTenant == null || "super_admin".equals(currentTenant)) { return criteriaBuilder.conjunction(); // 超级管理员可以看到所有 } return criteriaBuilder.equal(root.get("tenantId"), currentTenant); }; } public Predicate addTenantFilter(CriteriaBuilder cb, Root<?> root, String tenantId) { return cb.equal(root.get("tenantId"), tenantId); } public void validateTenantAccess(String resourceTenantId) { String currentTenant = tenantContext.getCurrentTenant(); if (!resourceTenantId.equals(currentTenant) && !"super_admin".equals(currentTenant)) { throw new AccessDeniedException("无权访问其他租户数据"); } }}// JPA Repository使用示例@Repositorypublic interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> { default List<User> findAllByTenant() { return findAll(tenantDataFilter.tenantSpecification()); } default Page<User> findPageByTenant(Pageable pageable) { return findAll(tenantDataFilter.tenantSpecification(), pageable); } @Query("SELECT u FROM User u WHERE u.tenantId = :tenantId AND u.username = :username") User findByTenantAndUsername(@Param("tenantId") String tenantId, @Param("username") String username);}
-
一、集群部署架构1.1 Redis集群配置yamlspring: redis: cluster: nodes: - redis-node1:6379 - redis-node2:6379 - redis-node3:6379 max-redirects: 3 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 shutdown-timeout: 100ms1.2 会话同步策略java@Configurationpublic class SessionConfig { @Bean public HttpSessionStrategy httpSessionStrategy() { return new HeaderHttpSessionStrategy(); } @Bean public FindByIndexNameSessionRepository<?> sessionRepository() { MapSessionRepository sessionRepository = new MapSessionRepository(); // 配置会话同步 RedisIndexedSessionRepository redisSessionRepository = new RedisIndexedSessionRepository(redisConnectionFactory()); redisSessionRepository.setDefaultMaxInactiveInterval(1800); redisSessionRepository.setRedisKeyNamespace("spring:session"); return redisSessionRepository; }}二、负载均衡策略2.1 Nginx配置示例nginxupstream sso_servers { least_conn; # 最少连接策略 server sso1.example.com:8080 weight=3; server sso2.example.com:8080 weight=2; server sso3.example.com:8080 weight=1; keepalive 32;}server { listen 443 ssl; server_name sso.example.com; ssl_certificate /etc/ssl/certs/sso.crt; ssl_certificate_key /etc/ssl/private/sso.key; location / { proxy_pass http://sso_servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 健康检查 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; proxy_connect_timeout 5s; proxy_send_timeout 10s; proxy_read_timeout 10s; } # 健康检查端点 location /health { access_log off; return 200 "healthy\n"; }}三、缓存优化3.1 多级缓存策略java@Servicepublic class MultiLevelCacheService { @Autowired private RedisTemplate<String, Object> redisTemplate; private final Cache<String, Object> localCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(5, TimeUnit.MINUTES) .build(); public Object getWithCache(String key, Supplier<Object> loader) { // 1. 检查本地缓存 Object value = localCache.getIfPresent(key); if (value != null) { return value; } // 2. 检查Redis缓存 value = redisTemplate.opsForValue().get(key); if (value != null) { localCache.put(key, value); return value; } // 3. 从数据源加载 value = loader.get(); if (value != null) { // 异步写入缓存 CompletableFuture.runAsync(() -> { redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES); localCache.put(key, value); }); } return value; } public void refreshCache(String key) { localCache.invalidate(key); redisTemplate.delete(key); }}3.2 令牌缓存优化java@Componentpublic class TokenCacheManager { private final Cache<String, Claims> tokenCache = Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(10, TimeUnit.MINUTES) .recordStats() .build(); public Claims getCachedClaims(String token) { return tokenCache.getIfPresent(token); } public void cacheClaims(String token, Claims claims) { tokenCache.put(token, claims); } public void invalidateToken(String token) { tokenCache.invalidate(token); } public CacheStats getStats() { return tokenCache.stats(); }}四、数据库优化4.1 分库分表策略java@Componentpublic class ShardingStrategy { public String getDatabaseName(String userId) { // 根据用户ID哈希分库 int hash = Math.abs(userId.hashCode()); int dbIndex = hash % 4; // 4个数据库 return "sso_db_" + dbIndex; } public String getTableName(String userId, String tablePrefix) { // 根据时间分表 LocalDateTime now = LocalDateTime.now(); String month = now.format(DateTimeFormatter.ofPattern("yyyyMM")); return tablePrefix + "_" + month; }}4.2 读写分离配置java@Configurationpublic class DataSourceConfig { @Bean @Primary public DataSource dataSource() { HikariDataSource master = createDataSource("master"); HikariDataSource slave = createDataSource("slave"); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", master); targetDataSources.put("slave", slave); RoutingDataSource routingDataSource = new RoutingDataSource(); routingDataSource.setDefaultTargetDataSource(master); routingDataSource.setTargetDataSources(targetDataSources); return routingDataSource; } @Bean public AbstractRoutingDataSource routingDataSource() { return new ReadWriteRoutingDataSource(); }}public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); return isReadOnly ? "slave" : "master"; }}五、异步处理5.1 异步认证处理java@Servicepublic class AsyncAuthService { @Autowired private ThreadPoolTaskExecutor authTaskExecutor; @Async("authTaskExecutor") public CompletableFuture<AuthResult> authenticateAsync(LoginRequest request) { // 异步执行认证逻辑 AuthResult result = performAuthentication(request); return CompletableFuture.completedFuture(result); } @Bean public ThreadPoolTaskExecutor authTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(100); executor.setThreadNamePrefix("auth-executor-"); executor.initialize(); return executor; }}六、性能监控6.1 Micrometer监控配置java@Configurationpublic class MetricsConfig { @Bean public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() { return registry -> registry.config() .commonTags("application", "sso-service") .commonTags("environment", System.getenv("ENV")); } @Bean public TimedAspect timedAspect(MeterRegistry registry) { return new TimedAspect(registry); }}@Servicepublic class AuthMetricsService { private final MeterRegistry meterRegistry; private final Counter loginSuccessCounter; private final Counter loginFailureCounter; private final Timer loginTimer; public AuthMetricsService(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; loginSuccessCounter = Counter.builder("sso.login.success") .description("Successful login attempts") .register(meterRegistry); loginFailureCounter = Counter.builder("sso.login.failure") .description("Failed login attempts") .register(meterRegistry); loginTimer = Timer.builder("sso.login.duration") .description("Login processing time") .register(meterRegistry); } public void recordLoginSuccess(String username) { loginSuccessCounter.increment(); meterRegistry.counter("sso.login.user", "username", username).increment(); } public Timer.Sample startLoginTimer() { return Timer.start(meterRegistry); } public void stopLoginTimer(Timer.Sample sample, String status) { sample.stop(loginTimer); }}
-
一、CSRF防护机制1.1 Spring Security CSRF配置java@Configuration@EnableWebSecuritypublic class CsrfSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) .ignoringRequestMatchers( "/api/auth/login", "/api/auth/logout", "/webhook/**" ) ) .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); return http.build(); }}@Componentpublic class CsrfCookieFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); if (csrfToken != null) { Cookie cookie = new Cookie("XSRF-TOKEN", csrfToken.getToken()); cookie.setPath("/"); cookie.setHttpOnly(false); cookie.setSecure(request.isSecure()); response.addCookie(cookie); } filterChain.doFilter(request, response); }}1.2 自定义CSRF策略java@Componentpublic class CustomCsrfTokenRepository implements CsrfTokenRepository { private final RedisTemplate<String, String> redisTemplate; private static final String CSRF_KEY_PREFIX = "csrf:"; @Override public CsrfToken generateToken(HttpServletRequest request) { String tokenId = UUID.randomUUID().toString(); return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", tokenId); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { String sessionId = request.getSession().getId(); if (token == null) { redisTemplate.delete(CSRF_KEY_PREFIX + sessionId); } else { redisTemplate.opsForValue().set( CSRF_KEY_PREFIX + sessionId, token.getToken(), 30, TimeUnit.MINUTES ); } } @Override public CsrfToken loadToken(HttpServletRequest request) { String sessionId = request.getSession().getId(); String token = redisTemplate.opsForValue().get(CSRF_KEY_PREFIX + sessionId); if (token != null) { return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token); } return null; }}二、XSS攻击防护2.1 输入输出过滤java@Componentpublic class XssFilter { private static final HtmlPolicyBuilder POLICY_BUILDER = new HtmlPolicyBuilder() .allowElements("a", "b", "i", "em", "strong", "p", "br") .allowAttributes("href").onElements("a") .requireRelNofollowOnLinks(); public String sanitize(String html) { if (StringUtils.isEmpty(html)) { return html; } PolicyFactory policy = POLICY_BUILDER.toFactory(); return policy.sanitize(html); } public Map<String, Object> sanitizeMap(Map<String, Object> data) { return data.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue() instanceof String ? sanitize((String) entry.getValue()) : entry.getValue() )); }}2.2 响应头安全配置java@Configurationpublic class SecurityHeadersConfig { @Bean public SecurityFilterChain securityHeadersFilter(HttpSecurity http) throws Exception { http .headers(headers -> headers .contentSecurityPolicy(csp -> csp .policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'") ) .xssProtection(xss -> xss .headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK) ) .httpStrictTransportSecurity(hsts -> hsts .includeSubDomains(true) .maxAgeInSeconds(31536000) ) .frameOptions(frame -> frame .sameOrigin() ) .contentTypeOptions(contentType -> {}) ); return http.build(); }}三、重放攻击防范3.1 Nonce机制实现java@Servicepublic class NonceService { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String NONCE_KEY_PREFIX = "nonce:"; private static final long NONCE_EXPIRE_SECONDS = 300; // 5分钟 public String generateNonce(String clientId) { String nonce = UUID.randomUUID().toString(); String key = NONCE_KEY_PREFIX + clientId + ":" + nonce; redisTemplate.opsForValue().set( key, String.valueOf(System.currentTimeMillis()), NONCE_EXPIRE_SECONDS, TimeUnit.SECONDS ); return nonce; } public boolean validateNonce(String clientId, String nonce) { String key = NONCE_KEY_PREFIX + clientId + ":" + nonce; if (!Boolean.TRUE.equals(redisTemplate.hasKey(key))) { return false; } // 使用后删除,防止重用 redisTemplate.delete(key); return true; }}3.2 时间戳验证java@Componentpublic class TimestampValidator { private static final long MAX_TIME_DIFF = 5 * 60 * 1000; // 5分钟 public boolean validateTimestamp(long requestTimestamp) { long currentTime = System.currentTimeMillis(); long timeDiff = Math.abs(currentTime - requestTimestamp); return timeDiff <= MAX_TIME_DIFF; } public boolean validateRequest(String clientId, long timestamp, String nonce, String signature) { // 1. 验证时间戳 if (!validateTimestamp(timestamp)) { return false; } // 2. 验证Nonce NonceService nonceService = new NonceService(); if (!nonceService.validateNonce(clientId, nonce)) { return false; } // 3. 验证签名 String dataToSign = clientId + timestamp + nonce; String expectedSignature = calculateSignature(dataToSign); return expectedSignature.equals(signature); } private String calculateSignature(String data) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(hash); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("计算签名失败", e); } }}四、暴力破解防护4.1 登录尝试限制java@Servicepublic class LoginAttemptService { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String ATTEMPT_KEY_PREFIX = "login:attempts:"; private static final String BLOCKED_KEY_PREFIX = "login:blocked:"; private static final int MAX_ATTEMPTS = 5; private static final long BLOCK_DURATION = 15 * 60 * 1000; // 15分钟 public void loginSucceeded(String username, String ipAddress) { String key = ATTEMPT_KEY_PREFIX + username + ":" + ipAddress; redisTemplate.delete(key); } public void loginFailed(String username, String ipAddress) { String key = ATTEMPT_KEY_PREFIX + username + ":" + ipAddress; String attemptsStr = redisTemplate.opsForValue().get(key); int attempts = attemptsStr != null ? Integer.parseInt(attemptsStr) : 0; attempts++; redisTemplate.opsForValue().set(key, String.valueOf(attempts), 1, TimeUnit.HOURS); if (attempts >= MAX_ATTEMPTS) { blockUser(username, ipAddress); } } public boolean isBlocked(String username, String ipAddress) { String key = BLOCKED_KEY_PREFIX + username + ":" + ipAddress; if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) { // 检查是否超过封锁时间 Long ttl = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); return ttl != null && ttl > 0; } return false; } private void blockUser(String username, String ipAddress) { String key = BLOCKED_KEY_PREFIX + username + ":" + ipAddress; redisTemplate.opsForValue().set(key, "blocked", BLOCK_DURATION, TimeUnit.MILLISECONDS); }}4.2 验证码集成java@Servicepublic class CaptchaService { @Autowired private RedisTemplate<String, String> redisTemplate; public CaptchaResponse generateCaptcha(String sessionId) { // 生成验证码文本 String captchaText = generateRandomText(6); // 生成验证码图片 BufferedImage image = generateCaptchaImage(captchaText); // 存储验证码 String key = "captcha:" + sessionId; redisTemplate.opsForValue().set(key, captchaText, 5, TimeUnit.MINUTES); // 转换为Base64 String imageBase64 = imageToBase64(image); return new CaptchaResponse(imageBase64, sessionId); } public boolean validateCaptcha(String sessionId, String userInput) { String key = "captcha:" + sessionId; String storedCaptcha = redisTemplate.opsForValue().get(key); if (storedCaptcha == null) { return false; } // 验证后删除 redisTemplate.delete(key); return storedCaptcha.equalsIgnoreCase(userInput); } public boolean isCaptchaRequired(String username, String ipAddress) { LoginAttemptService attemptService = new LoginAttemptService(); return attemptService.isBlocked(username, ipAddress); }}五、会话安全5.1 会话固定防护java@Configurationpublic class SessionFixationProtectionConfig { @Bean public SecurityFilterChain sessionFixationFilter(HttpSecurity http) throws Exception { http .sessionManagement(session -> session .sessionFixation(sessionFixation -> sessionFixation .migrateSession() ) .maximumSessions(1) .maxSessionsPreventsLogin(false) .expiredUrl("/login?expired") ); return http.build(); }}5.2 会话劫持检测java@Componentpublic class SessionHijackingDetector { public boolean detectHijacking(HttpServletRequest request, HttpSession session) { // 检查User-Agent String currentUserAgent = request.getHeader("User-Agent"); String storedUserAgent = (String) session.getAttribute("userAgent"); if (storedUserAgent != null && !storedUserAgent.equals(currentUserAgent)) { return true; } // 检查IP地址 String currentIp = getClientIp(request); String storedIp = (String) session.getAttribute("clientIp"); if (storedIp != null && !storedIp.equals(currentIp)) { return true; } // 更新会话属性 session.setAttribute("userAgent", currentUserAgent); session.setAttribute("clientIp", currentIp); return false; } private String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; }}六、安全最佳实践6.1 定期安全扫描java@Component@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行public class SecurityScanner { public void scanForVulnerabilities() { // 1. 检查弱密码用户 scanWeakPasswords(); // 2. 检查过期会话 cleanExpiredSessions(); // 3. 检查异常登录 analyzeLoginPatterns(); // 4. 生成安全报告 generateSecurityReport(); } private void scanWeakPasswords() { // 实现弱密码检测逻辑 } private void cleanExpiredSessions() { // 清理过期会话 }}
-
一、微服务SSO架构1.1 网关统一认证text┌─────────┐ ┌─────────────┐ ┌─────────────┐│ 客户端 │──▶│ API网关 │──▶│ 认证服务 │└─────────┘ └─────────────┘ └─────────────┘ │ │ ┌──────▼──────┐ ┌──────▼──────┐ │ 服务A │ │ 用户服务 │ └─────────────┘ └─────────────┘ │ ┌──────▼──────┐ │ 服务B │ └─────────────┘二、Spring Cloud Gateway集成2.1 网关配置yamlspring: cloud: gateway: routes: - id: auth-service uri: http://localhost:8081 predicates: - Path=/auth/** filters: - StripPrefix=1 - id: user-service uri: http://localhost:8082 predicates: - Path=/api/users/** filters: - name: JwtAuthFilter args: jwt-secret: ${JWT_SECRET} - id: product-service uri: http://localhost:8083 predicates: - Path=/api/products/** filters: - name: JwtAuthFilter - AddRequestHeader=X-User-Id, ${user.id}2.2 网关JWT过滤器java@Componentpublic class JwtAuthFilter extends AbstractGatewayFilterFactory<JwtAuthFilter.Config> { @Autowired private JwtTokenProvider tokenProvider; public JwtAuthFilter() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); // 检查是否为公开端点 if (isPublicEndpoint(request.getPath().toString())) { return chain.filter(exchange); } // 获取令牌 String token = extractToken(request); if (token == null || !tokenProvider.validateToken(token)) { return unauthorized(exchange, "无效的认证令牌"); } // 验证通过,添加用户信息到请求头 String username = tokenProvider.getUsername(token); List<String> roles = tokenProvider.getRoles(token); ServerHttpRequest mutatedRequest = request.mutate() .header("X-User-Id", username) .header("X-User-Roles", String.join(",", roles)) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); }; } private boolean isPublicEndpoint(String path) { return path.startsWith("/auth/login") || path.startsWith("/auth/register") || path.startsWith("/public/"); } private String extractToken(ServerHttpRequest request) { List<String> authHeaders = request.getHeaders().get("Authorization"); if (authHeaders != null && !authHeaders.isEmpty()) { String authHeader = authHeaders.get(0); if (authHeader.startsWith("Bearer ")) { return authHeader.substring(7); } } return null; } private Mono<Void> unauthorized(ServerWebExchange exchange, String message) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); String body = String.format("{\"error\": \"Unauthorized\", \"message\": \"%s\"}", message); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes()); return response.writeWith(Mono.just(buffer)); } public static class Config { // 配置属性 }}三、服务间认证3.1 Feign客户端配置java@Configurationpublic class FeignConfig { @Bean public RequestInterceptor requestInterceptor() { return requestTemplate -> { // 从安全上下文获取当前用户的令牌 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getCredentials() instanceof String) { String token = (String) authentication.getCredentials(); requestTemplate.header("Authorization", "Bearer " + token); } else if (authentication instanceof JwtAuthenticationToken) { JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) authentication; requestTemplate.header("Authorization", "Bearer " + jwtAuth.getToken().getTokenValue()); } }; }}3.2 服务间JWT传播java@Servicepublic class ServiceClient { @Autowired private RestTemplate restTemplate; @Value("${service-b.url}") private String serviceBUrl; public String callServiceB() { // 获取当前用户的JWT令牌 String token = getCurrentUserToken(); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token); headers.set("X-Service-Request", "true"); HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<String> response = restTemplate.exchange( serviceBUrl + "/api/data", HttpMethod.GET, entity, String.class ); return response.getBody(); } private String getCurrentUserToken() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication instanceof JwtAuthenticationToken) { JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) authentication; return jwtAuth.getToken().getTokenValue(); } else if (authentication instanceof AbstractOAuth2TokenAuthenticationToken) { AbstractOAuth2TokenAuthenticationToken<?> oauth2Auth = (AbstractOAuth2TokenAuthenticationToken<?>) authentication; return oauth2Auth.getToken().getTokenValue(); } throw new IllegalStateException("无法获取当前用户的认证令牌"); }}四、分布式会话管理4.1 Spring Session Redis配置java@Configuration@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)public class SessionConfig { @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } @Bean public HttpSessionIdResolver httpSessionIdResolver() { // 支持多种方式传递Session ID return new HeaderHttpSessionIdResolver("X-Auth-Token"); }}4.2 会话共享过滤器java@Componentpublic class SessionSharingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // 从请求头获取会话ID String sessionId = httpRequest.getHeader("X-Session-Id"); if (sessionId != null) { // 设置当前会话ID RequestContextHolder.currentRequestAttributes().setSessionId(sessionId); } chain.doFilter(request, response); // 将会话ID添加到响应头 if (sessionId == null) { HttpSession session = httpRequest.getSession(false); if (session != null) { httpResponse.setHeader("X-Session-Id", session.getId()); } } }}五、服务注册发现集成5.1 Eureka客户端配置java@SpringBootApplication@EnableDiscoveryClientpublic class AuthServiceApplication { public static void main(String[] args) { SpringApplication.run(AuthServiceApplication.class, args); }}5.2 动态服务发现java@Servicepublic class ServiceDiscoveryClient { @Autowired private DiscoveryClient discoveryClient; @Autowired private LoadBalancerClient loadBalancer; public String getServiceUrl(String serviceId) { List<ServiceInstance> instances = discoveryClient.getInstances(serviceId); if (instances.isEmpty()) { throw new ServiceUnavailableException("服务 " + serviceId + " 不可用"); } ServiceInstance instance = loadBalancer.choose(serviceId); return instance.getUri().toString(); } public List<String> getAllAuthServices() { return discoveryClient.getServices().stream() .filter(service -> service.endsWith("-auth-service")) .collect(Collectors.toList()); }}六、配置中心集成6.1 Spring Cloud Config配置yaml# bootstrap.ymlspring: application: name: sso-service cloud: config: uri: http://config-server:8888 fail-fast: true retry: max-attempts: 6 max-interval: 10000 encrypt: key: ${CONFIG_ENCRYPT_KEY}6.2 动态JWT配置java@Configuration@RefreshScopepublic class DynamicJwtConfig { @Value("${jwt.secret:default-secret}") private String secret; @Value("${jwt.expiration:3600}") private Long expiration; @Bean @RefreshScope public JwtTokenProvider jwtTokenProvider() { return new JwtTokenProvider(secret, expiration); }}七、熔断与降级7.1 Resilience4j配置yamlresilience4j: circuitbreaker: instances: authService: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 5 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 5s failureRateThreshold: 50 retry: instances: authService: maxAttempts: 3 waitDuration: 500ms timelimiter: instances: authService: timeoutDuration: 3s7.2 认证服务熔断器java@Service@Slf4jpublic class AuthServiceWithCircuitBreaker { @Autowired private AuthService authService; @CircuitBreaker(name = "authService", fallbackMethod = "fallbackLogin") @TimeLimiter(name = "authService") @Retry(name = "authService") public CompletableFuture<AuthResponse> loginAsync(LoginRequest request) { return CompletableFuture.supplyAsync(() -> authService.login(request)); } private CompletableFuture<AuthResponse> fallbackLogin( LoginRequest request, Throwable throwable) { log.warn("认证服务降级处理,请求: {}, 异常: {}", request.getUsername(), throwable.getMessage()); // 返回降级响应 AuthResponse response = new AuthResponse(); response.setError("认证服务暂时不可用,请稍后重试"); response.setFallback(true); return CompletableFuture.completedFuture(response); }}八、监控与追踪8.1 Spring Cloud Sleuth集成yamlspring: sleuth: sampler: probability: 1.0 propagation: type: B3 baggage: remote-fields: - X-User-Id - X-Request-ID logging: pattern: level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"8.2 自定义追踪过滤器java@Componentpublic class TraceFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; // 添加追踪信息 String traceId = httpRequest.getHeader("X-B3-TraceId"); if (traceId == null) { traceId = generateTraceId(); } MDC.put("traceId", traceId); MDC.put("spanId", generateSpanId()); try { chain.doFilter(request, response); } finally { MDC.clear(); } } private String generateTraceId() { return UUID.randomUUID().toString().replace("-", ""); } private String generateSpanId() { return Long.toHexString(new Random().nextLong()); }}九、API版本管理9.1 版本化认证端点java@RestController@RequestMapping("/api/v{auth-api-version}/auth")public class VersionedAuthController { @PostMapping("/login") public ResponseEntity<?> loginV1(@PathVariable("auth-api-version") String version, @RequestBody LoginRequest request) { if ("1".equals(version)) { return loginV1(request); } else if ("2".equals(version)) { return loginV2(request); } return ResponseEntity.status(HttpStatus.NOT_FOUND) .body("不支持的API版本"); } private ResponseEntity<?> loginV1(LoginRequest request) { // V1版本实现 return ResponseEntity.ok("V1 response"); } private ResponseEntity<?> loginV2(LoginRequest request) { // V2版本实现,支持多因素认证 return ResponseEntity.ok("V2 response with MFA support"); }}十、微服务SSO最佳实践服务治理:使用API网关统一入口实施服务熔断和降级配置合理的超时和重试安全策略:服务间使用mTLS相互认证实施最小权限原则定期轮换服务凭证可观测性:集成分布式追踪收集认证相关指标设置告警规则部署策略:认证服务独立部署支持蓝绿部署配置自动伸缩数据一致性:使用分布式会话存储实现最终一致性定期清理过期数据
-
一、JWT核心原理1.1 JWT结构详解JWT由三部分组成,以点分隔:textHeader.Payload.SignatureHeader示例:json{ "alg": "HS256", "typ": "JWT"}Payload示例:json{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622, "roles": ["USER", "ADMIN"]}二、JWT工具类实现2.1 完整JWT工具类java@Component@Slf4jpublic class JwtTokenProvider { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.expiration}") private long validityInMilliseconds; @Value("${jwt.refresh-expiration}") private long refreshValidityInMilliseconds; // 生成访问令牌 public String createAccessToken(String username, List<String> roles) { Claims claims = Jwts.claims().setSubject(username); claims.put("roles", roles); claims.put("type", "ACCESS"); Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(validity) .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) .compact(); } // 生成刷新令牌 public String createRefreshToken(String username) { Claims claims = Jwts.claims().setSubject(username); claims.put("type", "REFRESH"); Date now = new Date(); Date validity = new Date(now.getTime() + refreshValidityInMilliseconds); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(validity) .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) .compact(); } // 解析用户名 public String getUsername(String token) { return getAllClaimsFromToken(token).getSubject(); } // 获取用户角色 @SuppressWarnings("unchecked") public List<String> getRoles(String token) { return getAllClaimsFromToken(token).get("roles", List.class); } // 获取过期时间 public Date getExpirationDate(String token) { return getAllClaimsFromToken(token).getExpiration(); } // 验证令牌是否过期 public boolean isTokenExpired(String token) { final Date expiration = getExpirationDate(token); return expiration.before(new Date()); } // 验证令牌有效性 public boolean validateToken(String token) { try { Jwts.parser() .setSigningKey(secretKey.getBytes()) .parseClaimsJws(token); return !isTokenExpired(token); } catch (JwtException | IllegalArgumentException e) { log.warn("无效的JWT令牌: {}", e.getMessage()); return false; } } // 获取令牌中的所有声明 private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(secretKey.getBytes()) .parseClaimsJws(token) .getBody(); } // 从请求头提取令牌 public String resolveToken(HttpServletRequest req) { String bearerToken = req.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; }}三、JWT过滤器配置3.1 JWT认证过滤器java@Component@Slf4jpublic class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider jwtTokenProvider; @Autowired private CustomUserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String token = jwtTokenProvider.resolveToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { String username = jwtTokenProvider.getUsername(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); // 刷新令牌逻辑 refreshTokenIfNeeded(token, response); } } catch (Exception e) { log.error("无法设置用户认证: {}", e.getMessage()); } filterChain.doFilter(request, response); } private void refreshTokenIfNeeded(String token, HttpServletResponse response) { long expirationTime = jwtTokenProvider.getExpirationDate(token).getTime(); long currentTime = System.currentTimeMillis(); long remainingTime = expirationTime - currentTime; // 如果令牌将在30分钟内过期,则刷新 if (remainingTime < 30 * 60 * 1000) { String username = jwtTokenProvider.getUsername(token); List<String> roles = jwtTokenProvider.getRoles(token); String newToken = jwtTokenProvider.createAccessToken(username, roles); response.setHeader("X-New-Access-Token", newToken); } }}四、令牌黑名单实现4.1 黑名单管理java@Servicepublic class TokenBlacklistService { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String BLACKLIST_KEY = "token:blacklist:"; // 添加令牌到黑名单 public void blacklistToken(String token, long ttlInSeconds) { String key = BLACKLIST_KEY + DigestUtils.md5DigestAsHex(token.getBytes()); redisTemplate.opsForValue().set(key, "blacklisted", ttlInSeconds, TimeUnit.SECONDS); } // 检查令牌是否在黑名单中 public boolean isTokenBlacklisted(String token) { String key = BLACKLIST_KEY + DigestUtils.md5DigestAsHex(token.getBytes()); return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } // 从黑名单中移除令牌 public void removeFromBlacklist(String token) { String key = BLACKLIST_KEY + DigestUtils.md5DigestAsHex(token.getBytes()); redisTemplate.delete(key); }}4.2 增强的JWT验证java@Componentpublic class EnhancedJwtTokenProvider extends JwtTokenProvider { @Autowired private TokenBlacklistService blacklistService; @Override public boolean validateToken(String token) { if (blacklistService.isTokenBlacklisted(token)) { return false; } return super.validateToken(token); }}五、配置文件示例5.1 application.ymlyamljwt: secret: "mySuperSecretKeyThatIsAtLeast32BytesLongForHS256" expiration: 3600000 # 1小时(毫秒) refresh-expiration: 86400000 # 24小时(毫秒) issuer: "sso-server" audience: "web-client" redis: host: localhost port: 6379 password: timeout: 2000ms database: 0 jedis: pool: max-active: 8 max-idle: 8 min-idle: 0六、API接口设计6.1 认证控制器java@RestController@RequestMapping("/api/auth")public class AuthController { @Autowired private JwtTokenProvider tokenProvider; @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenBlacklistService blacklistService; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) { try { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); User user = (User) authentication.getPrincipal(); List<String> roles = user.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); // 生成令牌 String accessToken = tokenProvider.createAccessToken(user.getUsername(), roles); String refreshToken = tokenProvider.createRefreshToken(user.getUsername()); AuthResponse response = AuthResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) .tokenType("Bearer") .expiresIn(tokenProvider.getExpirationDate(accessToken).getTime()) .username(user.getUsername()) .roles(roles) .build(); return ResponseEntity.ok(response); } catch (AuthenticationException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(ErrorResponse.of("认证失败", e.getMessage())); } } @PostMapping("/refresh") public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) { try { String refreshToken = request.getRefreshToken(); if (!tokenProvider.validateToken(refreshToken)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(ErrorResponse.of("刷新令牌无效或已过期")); } String username = tokenProvider.getUsername(refreshToken); UserDetails userDetails = userDetailsService.loadUserByUsername(username); List<String> roles = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); String newAccessToken = tokenProvider.createAccessToken(username, roles); AuthResponse response = AuthResponse.builder() .accessToken(newAccessToken) .refreshToken(refreshToken) // 可以重新生成刷新令牌 .tokenType("Bearer") .expiresIn(tokenProvider.getExpirationDate(newAccessToken).getTime()) .username(username) .roles(roles) .build(); return ResponseEntity.ok(response); } catch (Exception e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(ErrorResponse.of("令牌刷新失败", e.getMessage())); } } @PostMapping("/logout") public ResponseEntity<?> logout(HttpServletRequest request) { String token = tokenProvider.resolveToken(request); if (token != null) { // 将令牌加入黑名单,剩余有效期同令牌有效期 long expirationTime = tokenProvider.getExpirationDate(token).getTime(); long currentTime = System.currentTimeMillis(); long ttlInSeconds = (expirationTime - currentTime) / 1000; if (ttlInSeconds > 0) { blacklistService.blacklistToken(token, ttlInSeconds); } // 清除安全上下文 SecurityContextHolder.clearContext(); } return ResponseEntity.ok("登出成功"); } @GetMapping("/validate") public ResponseEntity<?> validateToken(@RequestParam String token) { boolean isValid = tokenProvider.validateToken(token); Map<String, Object> response = new HashMap<>(); response.put("valid", isValid); if (isValid) { response.put("username", tokenProvider.getUsername(token)); response.put("roles", tokenProvider.getRoles(token)); response.put("expiresAt", tokenProvider.getExpirationDate(token)); } return ResponseEntity.ok(response); }}七、安全配置7.1 Spring Security配置java@Configuration@EnableWebSecuritypublic class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Autowired private CustomUserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .antMatchers("/api/public/**").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint()); return http.build(); } @Bean public AuthenticationEntryPoint jwtAuthenticationEntryPoint() { return (request, response, authException) -> { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write( "{\"error\": \"Unauthorized\", \"message\": \"无效或过期的令牌\"}" ); }; } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; }}八、JWT优化实践8.1 令牌压缩(针对长令牌)java@Componentpublic class TokenCompressionUtil { // 压缩令牌(当payload很大时) public String compressToken(String token) { byte[] compressed = LZ4Factory.fastestInstance() .fastCompressor() .compress(token.getBytes(StandardCharsets.UTF_8)); return Base64.getUrlEncoder().encodeToString(compressed); } // 解压令牌 public String decompressToken(String compressedToken) { byte[] decoded = Base64.getUrlDecoder().decode(compressedToken); byte[] decompressed = LZ4Factory.fastestInstance() .fastDecompressor() .decompress(decoded, 1024 * 1024); // 1MB max return new String(decompressed, StandardCharsets.UTF_8); }}8.2 令牌轮换策略java@Servicepublic class TokenRotationService { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String TOKEN_FAMILY_KEY = "token:family:"; // 创建令牌族(用于刷新令牌时轮换) public String createTokenFamily(String userId) { String familyId = UUID.randomUUID().toString(); String key = TOKEN_FAMILY_KEY + userId + ":" + familyId; redisTemplate.opsForValue().set(key, "active", 30, TimeUnit.DAYS); return familyId; } // 验证令牌族 public boolean isValidTokenFamily(String userId, String familyId) { String key = TOKEN_FAMILY_KEY + userId + ":" + familyId; return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } // 撤销令牌族 public void revokeTokenFamily(String userId, String familyId) { String key = TOKEN_FAMILY_KEY + userId + ":" + familyId; redisTemplate.delete(key); }}九、监控和审计9.1 JWT使用监控java@Component@Slf4jpublic class JwtUsageMonitor { private final MeterRegistry meterRegistry; public JwtUsageMonitor(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } // 记录令牌生成 public void recordTokenGeneration(String username) { meterRegistry.counter("jwt.tokens.generated", "username", username).increment(); log.info("为用户 {} 生成了新的JWT令牌", username); } // 记录令牌验证 public void recordTokenValidation(String username, boolean valid) { if (valid) { meterRegistry.counter("jwt.tokens.validated.success", "username", username).increment(); } else { meterRegistry.counter("jwt.tokens.validated.failed", "username", username).increment(); log.warn("用户 {} 的JWT令牌验证失败", username); } } // 记录令牌刷新 public void recordTokenRefresh(String username) { meterRegistry.counter("jwt.tokens.refreshed", "username", username).increment(); log.info("用户 {} 刷新了JWT令牌", username); }}十、最佳实践总结密钥管理:使用至少32字节的密钥定期轮换密钥密钥与代码分离存储令牌设计:访问令牌有效期1-2小时刷新令牌有效期7-30天包含必要的最小信息安全措施:强制HTTPS传输实现令牌黑名单防止重放攻击监控异常使用模式性能优化:合理设置令牌大小使用高效的签名算法缓存验证结果运维建议:记录所有令牌操作设置令牌使用阈值告警定期审计令牌策略
-
一、OAuth2.0核心概念OAuth2.0是一个授权框架,不是认证协议,但常被用于实现SSO。它定义了四种授权模式,其中授权码模式最适合Web应用的SSO场景。1.1 核心角色资源所有者:用户客户端:需要访问资源的应用授权服务器:颁发访问令牌资源服务器:保护资源的服务器1.2 授权流程(授权码模式)text1. 用户访问客户端2. 客户端重定向到授权服务器3. 用户登录并授权4. 授权服务器返回授权码5. 客户端用授权码交换访问令牌6. 客户端用访问令牌访问资源二、Spring Security OAuth2配置2.1 授权服务器配置java@Configuration@EnableAuthorizationServerpublic class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("webapp") .secret(passwordEncoder.encode("websecret")) .authorizedGrantTypes("authorization_code", "refresh_token", "password") .scopes("read", "write") .redirectUris("http://localhost:8081/login/oauth2/code/webapp") .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(86400); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .tokenStore(tokenStore()) .accessTokenConverter(accessTokenConverter()); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("signing-key"); return converter; }}2.2 资源服务器配置java@Configuration@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/api/public/**").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .antMatchers("/api/**").authenticated() .and() .oauth2ResourceServer() .jwt(); }}2.3 安全配置java@Configuration@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/oauth/**", "/login/**").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}三、JWT令牌集成3.1 JWT配置增强java@Configurationpublic class JwtConfig { @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("my-signing-key"); // 自定义JWT声明 DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter(); accessTokenConverter.setUserTokenConverter( new CustomUserAuthenticationConverter()); converter.setAccessTokenConverter(accessTokenConverter); return converter; } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); }}3.2 自定义用户信息转换javapublic class CustomUserAuthenticationConverter implements UserAuthenticationConverter { @Override public Map<String, ?> convertUserAuthentication( Authentication authentication) { Map<String, Object> response = new LinkedHashMap<>(); response.put(USERNAME, authentication.getName()); if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) { response.put(AUTHORITIES, AuthorityUtils.authorityListToSet( authentication.getAuthorities())); } // 添加自定义声明 if (authentication.getDetails() != null) { response.put("details", authentication.getDetails()); } return response; }}四、客户端应用集成4.1 客户端配置yamlspring: security: oauth2: client: registration: sso-server: client-id: webapp client-secret: websecret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/sso-server" scope: read,write provider: sso-server: authorization-uri: http://localhost:8080/oauth/authorize token-uri: http://localhost:8080/oauth/token user-info-uri: http://localhost:8080/userinfo user-name-attribute: name4.2 客户端安全配置java@Configuration@EnableWebSecuritypublic class ClientSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/error", "/webjars/**").permitAll() .anyRequest().authenticated() .and() .oauth2Login() .loginPage("/oauth2/authorization/sso-server") .defaultSuccessUrl("/home", true) .failureUrl("/login?error=true"); }}五、用户信息端点5.1 用户信息服务java@RestControllerpublic class UserInfoController { @GetMapping("/userinfo") public Map<String, Object> getUserInfo(@AuthenticationPrincipal OAuth2User principal) { Map<String, Object> userInfo = new HashMap<>(); userInfo.put("username", principal.getName()); userInfo.put("authorities", principal.getAuthorities()); userInfo.put("attributes", principal.getAttributes()); return userInfo; } @GetMapping("/api/me") public UserProfile getCurrentUser(@AuthenticationPrincipal(expression = "username") String username) { return userService.getUserProfile(username); }}六、令牌管理6.1 令牌增强器java@Componentpublic class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String, Object> additionalInfo = new HashMap<>(); // 添加额外信息到令牌 additionalInfo.put("organization", "Example Corp"); // 如果是密码模式,添加用户详情 if (authentication.getUserAuthentication() != null) { Object principal = authentication.getUserAuthentication().getPrincipal(); if (principal instanceof UserDetails) { UserDetails user = (UserDetails) principal; additionalInfo.put("username", user.getUsername()); additionalInfo.put("authorities", user.getAuthorities()); } } ((DefaultOAuth2AccessToken) accessToken) .setAdditionalInformation(additionalInfo); return accessToken; }}6.2 令牌存储策略java@Configurationpublic class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean @Primary public TokenStore tokenStore() { // 使用Redis存储令牌(支持集群) RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory); tokenStore.setPrefix("oauth:"); return tokenStore; } @Bean @ConditionalOnProperty(name = "oauth2.token.store", havingValue = "jwt") public TokenStore jwtTokenStore() { // 使用JWT令牌(无状态) return new JwtTokenStore(jwtAccessTokenConverter()); }}七、单点登出实现7.1 全局登出端点java@RestControllerpublic class LogoutController { @Autowired private TokenStore tokenStore; @PostMapping("/oauth/revoke-token") public ResponseEntity<?> revokeToken(@RequestParam("token") String tokenValue) { OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue); if (accessToken != null) { tokenStore.removeAccessToken(accessToken); // 如果支持刷新令牌,也一并删除 OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { tokenStore.removeRefreshToken(refreshToken); } } return ResponseEntity.ok().build(); } @PostMapping("/global-logout") public ResponseEntity<?> globalLogout( @RequestParam("access_token") String accessToken, HttpServletRequest request) { // 1. 使当前令牌失效 revokeToken(accessToken); // 2. 获取所有活跃会话并通知登出 List<String> clientIds = getActiveClients(accessToken); notifyClientsLogout(clientIds); // 3. 清除安全上下文 SecurityContextHolder.clearContext(); // 4. 使HTTP会话失效 HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } return ResponseEntity.ok("登出成功"); }}八、安全加固措施8.1 防止CSRF攻击java@Configurationpublic class CsrfConfig { @Bean public CsrfTokenRepository csrfTokenRepository() { CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse(); repository.setCookiePath("/"); repository.setCookieDomain("example.com"); return repository; }}8.2 防止重放攻击java@Componentpublic class ReplayAttackProtection { @Autowired private RedisTemplate<String, String> redisTemplate; public boolean isReplayAttack(String token, String requestId) { String key = "token:nonce:" + token; // 检查请求ID是否已使用过 if (redisTemplate.hasKey(key)) { String existingRequestId = redisTemplate.opsForValue().get(key); return existingRequestId.equals(requestId); } // 存储新的请求ID,有效期5分钟 redisTemplate.opsForValue().set(key, requestId, 5, TimeUnit.MINUTES); return false; }}九、性能优化9.1 令牌缓存java@Configuration@EnableCachingpublic class TokenCacheConfig { @Bean public CacheManager cacheManager() { ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); cacheManager.setCacheNames(Arrays.asList( "accessTokens", "refreshTokens", "authorizationCodes")); return cacheManager; } @Bean public TokenServices tokenServices() { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore()); tokenServices.setSupportRefreshToken(true); tokenServices.setReuseRefreshToken(false); tokenServices.setAccessTokenValiditySeconds(3600); tokenServices.setRefreshTokenValiditySeconds(86400); return tokenServices; }}十、监控与日志10.1 审计日志java@Componentpublic class OAuth2AuditLogger { private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT"); public void logAuthorizationRequest(OAuth2Request request) { auditLogger.info("Authorization request: clientId={}, scope={}, approved={}", request.getClientId(), request.getScope(), request.isApproved()); } public void logTokenIssue(String clientId, String username) { auditLogger.info("Token issued: clientId={}, username={}", clientId, username); }}总结SpringBoot结合OAuth2.0实现SSO提供了企业级的认证授权解决方案。通过合理的配置和扩展,可以满足不同场景的需求。关键点包括:正确选择授权模式:Web应用使用授权码模式安全令牌管理:合理设置有效期,实现令牌刷新完善的登出机制:支持全局登出性能优化:合理使用缓存安全加固:防止CSRF、重放等攻击监控审计:记录关键操作日志
-
一、SSO核心概念与价值单点登录(Single Sign-On,SSO)是一种身份认证方案,允许用户使用单一凭证登录多个相互信任的应用系统。在微服务架构和分布式系统盛行的今天,SSO已成为企业级应用的标准配置。核心价值体现:用户体验提升:减少重复登录,提高操作效率安全管理集中化:统一认证入口,便于监控审计开发维护简化:各应用无需独立实现认证逻辑安全风险降低:统一密码策略和会话管理二、主流SSO协议对比2.1 OAuth 2.0适用场景:第三方应用授权、API访问控制优点:标准化程度高、生态系统完善缺点:协议复杂、学习曲线陡峭SpringBoot支持:spring-security-oauth2-autoconfigure2.2 OpenID Connect适用场景:身份认证标准化解决方案优点:基于OAuth 2.0,专为认证设计缺点:相对较新,部分企业接受度低SpringBoot支持:spring-security-oauth2-client2.3 JWT(JSON Web Token)适用场景:无状态认证、微服务间通信优点:自包含、跨语言、易于传输缺点:令牌无法撤销、需要额外存储机制SpringBoot支持:jjwt库2.4 SAML 2.0适用场景:企业级SSO、跨组织认证优点:成熟稳定、企业特性丰富缺点:XML格式复杂、实现繁琐SpringBoot支持:spring-security-saml2-core三、SpringBoot SSO架构设计3.1 典型架构模式集中式认证中心架构:text用户浏览器 │ ├─▶ 应用A(前端) │ │ │ └─▶ 统一认证中心 │ │ └─▶ 应用B(前端)─┘ │ └─▶ 用户数据存储技术组件划分:认证服务器:处理用户登录、令牌颁发资源服务器:保护API资源,验证令牌客户端应用:集成SSO的前端应用会话存储:Redis/数据库存储会话信息用户目录:LDAP/数据库存储用户数据3.2 关键设计原则安全性原则:最小权限原则:只授予必要权限防御性编程:验证所有输入参数安全传输:强制使用HTTPS定期审计:记录所有认证操作可用性原则:故障隔离:单点故障不影响整体负载均衡:支持水平扩展会话持久化:避免会话丢失优雅降级:部分故障时保持基本功能四、技术栈选择建议4.1 基础依赖配置xml<dependencies> <!-- Spring Security核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- OAuth2客户端 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <!-- JWT支持 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <!-- Redis会话存储 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency></dependencies>4.2 配置建议开发环境:使用内存存储简化配置开启详细日志便于调试使用自签名证书测试HTTPS生产环境:使用专业CA证书配置集群化Redis存储会话启用API网关进行流量控制配置监控告警系统五、安全考虑与最佳实践5.1 令牌安全使用HTTPS传输令牌设置合理的令牌有效期实现令牌刷新机制记录令牌使用日志5.2 会话管理限制并发会话数量实现会话超时机制支持跨域会话共享提供全局登出功能5.3 密码安全使用BCrypt等强哈希算法实施密码复杂度策略支持多因素认证记录登录失败尝试六、性能优化策略6.1 缓存策略java@Configuration@EnableCachingpublic class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(1000)); return cacheManager; }}6.2 数据库优化为认证相关表添加合适索引定期清理过期令牌和会话使用连接池管理数据库连接考虑读写分离策略七、监控与运维7.1 关键监控指标认证成功率/失败率平均认证响应时间活跃会话数量令牌颁发频率系统资源使用率7.2 日志记录策略yamllogging: level: org.springframework.security: DEBUG com.example.sso: INFO file: name: logs/sso-service.log max-size: 10MB max-history: 30八、部署架构建议8.1 开发测试环境单节点部署,简化运维使用内嵌数据库和缓存配置开发人员访问权限8.2 生产环境text负载均衡器 │ ├─▶ 认证节点1 ──┐ │ │ ├─▶ 认证节点2 ──┼─▶ Redis集群 │ │ └─▶ 认证节点3 ──┘ │ └─▶ 数据库集群高可用配置:至少3节点集群部署使用Keepalived实现VIP漂移配置数据库主从复制设置自动化备份策略总结SpringBoot SSO实现需要综合考虑安全性、性能、可扩展性和可维护性。建议从实际业务需求出发,选择合适的协议和架构模式。对于初创项目,可以从简单的JWT方案开始;对于企业级应用,建议采用OAuth 2.0或SAML 2.0。无论选择哪种方案,都要确保遵循安全最佳实践,建立完善的监控和应急响应机制。随着业务发展,SSO系统可能需要支持更复杂的场景,如多因素认证、生物识别认证、风险控制等。因此,在架构设计时应预留扩展点,确保系统能够平滑演进。
-
springboot静态资源映射规则SpringBoot 对静态资源的访问提供了自动配置支持,核心依赖是 WebMvcAutoConfiguration 自动配置类,其底层通过重写 addResourceHandlers 方法,借助 ResourceHandlerRegistry(资源处理器注册器)注册多个 ResourceHandler(资源处理器),分别定义了普通静态资源和 WebJars 资源的存放路径与 URL 访问路径映射关系,同时遵循 Controller 映射优先级高于静态资源映射的规则,确保请求匹配的合理性。springboot静态资源映射规则源码解析首先我们找到 spring-boot-autoconfigure 的jar包,点击 spring.factories 文件,找到 org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,进入 WebMvcAutoConfiguration 核心配置类 SpringBoot 之所以能访问到静态资源,本质是 WebMvcAutoConfiguration 自动配置类中,通过 addResourceHandlers 方法注册了默认的资源处理器 下面具体简述该函数代码第一部分:加载缓存配置 点击进入 ResourceProperties 这是静态资源相关配置的绑定类,可以在 application.yml/properties 静态资源配置中进行设置和静态资源有关的参数,包括缓存时间等第二部分:注册 WebJars 专用资源处理器 Webjars 是以 jar 包的方式引入静态资源,Webjars 的网址为 https://www.webjars.org/由代码可知所有以 /webjars/ 开头的请求都会被该处理器处理,WebJars 组件的 Jar 包中静态资源默认存放在 classpath:META-INF/resources/webjars/ 目录下下面以引入 jQuery 为例进行演示使用方式是导入 org.webjars 的相关依赖 <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.3.1</version> </dependency>访问方式是访问 classpath:META-INF/resources/webjars/ 目录下的内容,例如访问 jquery 的路径为:localhost:8080/webjars/jquery/3.3.1/jquery.js 第三部分是:注册普通静态资源处理器 点击 getStaticPathPattern 函数进入 WebMvcProperties 类中,里面 staticPathPattern 注册 URL 访问路径模式,默认是 /**(所有未被 Controller 处理的请求),可通过配置修改 点击 getStaticLocations 函数进入 ResourceProperties 类中,里面 staticLocations 读取默认 / 自定义的静态资源目录,默认值为四个目录 优先级从高到低(同名文件会优先访问优先级高的目录):静态资源 classpath:/META-INF/resources/,即项目内实际存放路径 src/main/resources/META-INF/resources/静态资源 classpath:/resources/,即项目内实际存放路径 src/main/resources/resources/静态资源 classpath:/static/,即项目内实际存放路径 src/main/resources/static/静态资源 classpath:/public/,即项目内实际存放路径 src/main/resources/public/下面我们来看在 WebMvcAutoConfiguration 自动配置类中 SpringBoot 的默认欢迎页的自动配置核心welcomePageHandlerMapping 函数专门处理欢迎页的 HandlerMapping getWelcomePage() 函数用于查找静态资源目录和模板目录下的 index.html,严格遵循静态资源默认目录优先级进行查找,只要其中任意一个目录下有 index.html,就返回该资源路径。另外,如果通过 spring.web.resources.staticLocations 自定义了静态资源目录,getWelcomePage() 也会在自定义目录中查找 index.html 由代码可知这个方法的本质是向 SpringMVC 容器注册一个 WelcomePageHandlerMapping 实例,它是一个请求处理器映射器(HandlerMapping),专门处理根路径(/)请求,自动查找并返回项目中的 index.html 作为欢迎页。访问 http://localhost:8080/ 自动打开欢迎页的效果————————————————版权声明:本文为CSDN博主「期待のcode」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/2401_84284464/article/details/154792579
-
Spring Boot 异步处理全面详解:从基础到高级应用作者:北辰alk关键词:Spring Boot、异步处理、@Async、线程池、性能优化1. 引言在现代Web应用开发中,高并发和快速响应是系统设计的重要目标。传统的同步处理方式在面对大量耗时操作时,会导致请求线程阻塞,降低系统的吞吐量。Spring Boot提供了强大的异步处理能力,能够有效提升系统性能和用户体验。本文将全面深入地介绍Spring Boot中的异步处理机制,涵盖从基础使用到高级应用的各个方面,包括异步配置、线程池优化、异常处理、事务管理等,并通过实际代码示例和流程图帮助读者彻底掌握这一重要技术。2. 异步处理基础概念2.1 同步 vs 异步同步处理:请求发起后,必须等待任务完成才能继续执行线程处于阻塞状态,资源利用率低编程模型简单,易于理解和调试异步处理:请求发起后,立即返回,任务在后台执行线程不会阻塞,可以继续处理其他请求提高系统吞吐量和资源利用率2.2 异步处理适用场景耗时IO操作:文件上传、邮件发送、短信通知批量数据处理:数据导入导出、报表生成第三方API调用:支付接口、外部服务集成日志记录:操作日志、系统监控数据收集3. Spring Boot异步处理核心注解3.1 启用异步支持在Spring Boot应用中,首先需要启用异步处理功能:@Configuration@EnableAsyncpublic class AsyncConfig { // 异步配置类}或者直接在主应用类上添加注解:@SpringBootApplication@EnableAsyncpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}3.2 @Async注解详解@Async注解用于标记异步执行的方法:@Servicepublic class AsyncService { @Async public void processTask(String taskName) { // 异步执行的任务逻辑 System.out.println("处理任务: " + taskName + ",线程: " + Thread.currentThread().getName()); }}4. 异步方法返回值处理4.1 无返回值异步方法@Servicepublic class NotificationService { @Async public void sendEmail(String to, String subject, String content) { try { // 模拟邮件发送耗时 Thread.sleep(3000); System.out.println("邮件发送成功至: " + to); System.out.println("主题: " + subject); System.out.println("内容: " + content); System.out.println("执行线程: " + Thread.currentThread().getName()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }}4.2 有返回值异步方法@Servicepublic class CalculationService { @Async public CompletableFuture<Integer> calculateSum(int start, int end) { System.out.println("开始计算从 " + start + " 到 " + end + " 的和"); System.out.println("计算线程: " + Thread.currentThread().getName()); int sum = 0; for (int i = start; i <= end; i++) { sum += i; try { Thread.sleep(10); // 模拟计算耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } return CompletableFuture.completedFuture(sum); } @Async public CompletableFuture<String> processData(String data) { return CompletableFuture.supplyAsync(() -> { try { // 模拟数据处理 Thread.sleep(2000); return "处理结果: " + data.toUpperCase(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); }}5. 自定义线程池配置5.1 基础线程池配置@Configuration@EnableAsyncpublic class AsyncConfig { @Bean("taskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程数 executor.setCorePoolSize(10); // 最大线程数 executor.setMaxPoolSize(20); // 队列容量 executor.setQueueCapacity(200); // 线程活跃时间(秒) executor.setKeepAliveSeconds(60); // 线程名前缀 executor.setThreadNamePrefix("async-task-"); // 拒绝策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待所有任务结束后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 等待时间 executor.setAwaitTerminationSeconds(60); executor.initialize(); return executor; }}5.2 多个线程池配置@Configuration@EnableAsyncpublic class MultipleAsyncConfig { // CPU密集型任务线程池 @Bean("cpuIntensiveTaskExecutor") public ThreadPoolTaskExecutor cpuIntensiveTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); int corePoolSize = Runtime.getRuntime().availableProcessors(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(corePoolSize * 2); executor.setQueueCapacity(100); executor.setThreadNamePrefix("cpu-intensive-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); executor.initialize(); return executor; } // IO密集型任务线程池 @Bean("ioIntensiveTaskExecutor") public ThreadPoolTaskExecutor ioIntensiveTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); executor.setMaxPoolSize(50); executor.setQueueCapacity(500); executor.setThreadNamePrefix("io-intensive-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } // 定时任务线程池 @Bean("scheduledTaskExecutor") public ThreadPoolTaskExecutor scheduledTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(50); executor.setThreadNamePrefix("scheduled-task-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); executor.initialize(); return executor; }}5.3 使用指定线程池@Servicepublic class AdvancedAsyncService { // 使用CPU密集型线程池 @Async("cpuIntensiveTaskExecutor") public CompletableFuture<String> cpuIntensiveTask() { System.out.println("CPU密集型任务,线程: " + Thread.currentThread().getName()); // 模拟CPU密集型计算 return CompletableFuture.completedFuture("CPU任务完成"); } // 使用IO密集型线程池 @Async("ioIntensiveTaskExecutor") public CompletableFuture<String> ioIntensiveTask() { System.out.println("IO密集型任务,线程: " + Thread.currentThread().getName()); // 模拟IO操作 try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return CompletableFuture.completedFuture("IO任务完成"); }}6. 异步处理流程详解6.1 异步执行流程图graph TD A[客户端请求] --> B[Controller接收请求] B --> C[调用@Async方法] C --> D[立即返回响应] D --> E[客户端收到响应] C --> F[提交任务到线程池] F --> G{线程池状态检查} G -->|队列未满| H[任务进入队列] G -->|队列已满| I[创建新线程] I --> J{是否超过最大线程数} J -->|是| K[执行拒绝策略] J -->|否| L[创建线程执行任务] H --> M[线程从队列获取任务] M --> N[执行异步任务] N --> O[任务完成] K --> P[任务被拒绝处理]6.2 异步处理时序图ClientControllerAsync ServiceThread PoolWorker Thread发送请求调用异步方法提交任务到线程池立即返回CompletableFuture返回Future对象立即返回响应异步处理开始分配任务给工作线程执行耗时任务任务完成,设置结果客户端可以继续其他操作ClientControllerAsync ServiceThread PoolWorker Thread7. 完整示例代码7.1 控制器类@RestController@RequestMapping("/api/async")public class AsyncController { private final NotificationService notificationService; private final CalculationService calculationService; private final AdvancedAsyncService advancedAsyncService; public AsyncController(NotificationService notificationService, CalculationService calculationService, AdvancedAsyncService advancedAsyncService) { this.notificationService = notificationService; this.calculationService = calculationService; this.advancedAsyncService = advancedAsyncService; } @PostMapping("/send-email") public ResponseEntity<String> sendEmail() { String to = "user@example.com"; String subject = "测试邮件"; String content = "这是一封测试异步处理的邮件"; notificationService.sendEmail(to, subject, content); return ResponseEntity.accepted().body("邮件发送任务已提交"); } @GetMapping("/calculate") public CompletableFuture<ResponseEntity<Map<String, Object>>> calculateSum() { CompletableFuture<Integer> sum1 = calculationService.calculateSum(1, 100); CompletableFuture<Integer> sum2 = calculationService.calculateSum(101, 200); return CompletableFuture.allOf(sum1, sum2) .thenApply(v -> { Map<String, Object> result = new HashMap<>(); try { result.put("sum1", sum1.get()); result.put("sum2", sum2.get()); result.put("total", sum1.get() + sum2.get()); } catch (Exception e) { throw new RuntimeException(e); } return ResponseEntity.ok(result); }); } @GetMapping("/parallel-tasks") public CompletableFuture<ResponseEntity<Map<String, String>>> executeParallelTasks() { CompletableFuture<String> cpuTask = advancedAsyncService.cpuIntensiveTask(); CompletableFuture<String> ioTask = advancedAsyncService.ioIntensiveTask(); return CompletableFuture.allOf(cpuTask, ioTask) .thenApply(v -> { Map<String, String> results = new HashMap<>(); try { results.put("cpuTask", cpuTask.get()); results.put("ioTask", ioTask.get()); } catch (Exception e) { throw new RuntimeException(e); } return ResponseEntity.ok(results); }); }}7.2 服务类@Servicepublic class ComprehensiveAsyncService { private static final Logger logger = LoggerFactory.getLogger(ComprehensiveAsyncService.class); @Async("taskExecutor") public CompletableFuture<String> processWithRetry(String data, int maxRetries) { return CompletableFuture.supplyAsync(() -> { int attempt = 0; while (attempt < maxRetries) { try { logger.info("处理数据尝试: {}, 数据: {}", attempt + 1, data); // 模拟处理逻辑 if (Math.random() > 0.3) { // 70%成功率 return "处理成功: " + data; } else { throw new RuntimeException("处理失败"); } } catch (Exception e) { attempt++; if (attempt >= maxRetries) { logger.error("处理数据失败,已达到最大重试次数"); throw new RuntimeException("处理失败,重试次数耗尽"); } try { Thread.sleep(1000 * attempt); // 指数退避 } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("任务被中断"); } } } return "处理完成"; }); } @Async public CompletableFuture<List<String>> batchProcess(List<String> items) { logger.info("开始批量处理,项目数量: {}", items.size()); List<CompletableFuture<String>> futures = items.stream() .map(item -> processItem(item)) .collect(Collectors.toList()); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> futures.stream() .map(future -> { try { return future.get(); } catch (Exception e) { return "处理失败: " + e.getMessage(); } }) .collect(Collectors.toList())); } @Async private CompletableFuture<String> processItem(String item) { return CompletableFuture.supplyAsync(() -> { try { // 模拟处理时间 Thread.sleep(100 + (long)(Math.random() * 400)); return "处理完成: " + item; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } }); }}8. 异常处理机制8.1 异步方法异常处理@Servicepublic class AsyncExceptionHandlingService { @Async public CompletableFuture<String> processWithExceptionHandling(String input) { return CompletableFuture.supplyAsync(() -> { if (input == null || input.trim().isEmpty()) { throw new IllegalArgumentException("输入不能为空"); } try { // 模拟业务处理 Thread.sleep(1000); return "处理结果: " + input.toUpperCase(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("处理被中断", e); } }).exceptionally(throwable -> { // 异常处理逻辑 logger.error("处理过程中发生异常", throwable); return "错误: " + throwable.getMessage(); }); }}8.2 全局异步异常处理器@Componentpublic class GlobalAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalAsyncExceptionHandler.class); @Override public void handleUncaughtException(Throwable ex, Method method, Object... params) { logger.error("异步方法执行异常 - 方法: {}, 参数: {}", method.getName(), Arrays.toString(params), ex); // 可以在这里添加额外的异常处理逻辑,如发送告警、记录监控等 sendAlert(method, ex); } private void sendAlert(Method method, Throwable ex) { // 模拟发送告警 logger.warn("发送异步任务异常告警 - 方法: {}, 异常: {}", method.getName(), ex.getMessage()); }}8.3 配置异常处理器@Configuration@EnableAsyncpublic class AsyncExceptionConfig implements AsyncConfigurer { private final GlobalAsyncExceptionHandler exceptionHandler; public AsyncExceptionConfig(GlobalAsyncExceptionHandler exceptionHandler) { this.exceptionHandler = exceptionHandler; } @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-exception-"); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return exceptionHandler; }}9. 异步与事务管理9.1 异步事务处理@Servicepublic class AsyncTransactionService { private final UserRepository userRepository; private final AuditLogRepository auditLogRepository; public AsyncTransactionService(UserRepository userRepository, AuditLogRepository auditLogRepository) { this.userRepository = userRepository; this.auditLogRepository = auditLogRepository; } @Async @Transactional(propagation = Propagation.REQUIRES_NEW) public CompletableFuture<Void> asyncOperationWithTransaction(Long userId, String action) { return CompletableFuture.runAsync(() -> { try { // 在独立事务中执行 User user = userRepository.findById(userId).orElseThrow(); AuditLog auditLog = new AuditLog(); auditLog.setUserId(userId); auditLog.setAction(action); auditLog.setTimestamp(LocalDateTime.now()); auditLogRepository.save(auditLog); // 模拟业务处理 Thread.sleep(1000); logger.info("异步事务操作完成 - 用户: {}, 操作: {}", user.getUsername(), action); } catch (Exception e) { logger.error("异步事务操作失败", e); throw new RuntimeException(e); } }); }}10. 监控和调试10.1 线程池监控@Componentpublic class ThreadPoolMonitor { private static final Logger logger = LoggerFactory.getLogger(ThreadPoolMonitor.class); private final ThreadPoolTaskExecutor taskExecutor; public ThreadPoolMonitor(@Qualifier("taskExecutor") ThreadPoolTaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } @Scheduled(fixedRate = 30000) // 每30秒执行一次 public void monitorThreadPool() { ThreadPoolExecutor threadPoolExecutor = taskExecutor.getThreadPoolExecutor(); logger.info("线程池监控信息:"); logger.info("活跃线程数: {}", threadPoolExecutor.getActiveCount()); logger.info("核心线程数: {}", threadPoolExecutor.getCorePoolSize()); logger.info("最大线程数: {}", threadPoolExecutor.getMaximumPoolSize()); logger.info("池中当前线程数: {}", threadPoolExecutor.getPoolSize()); logger.info("任务总数: {}", threadPoolExecutor.getTaskCount()); logger.info("已完成任务数: {}", threadPoolExecutor.getCompletedTaskCount()); logger.info("队列大小: {}", threadPoolExecutor.getQueue().size()); logger.info("队列剩余容量: {}", threadPoolExecutor.getQueue().remainingCapacity()); }}10.2 异步任务跟踪@Aspect@Componentpublic class AsyncExecutionAspect { private static final Logger logger = LoggerFactory.getLogger(AsyncExecutionAspect.class); @Around("@annotation(org.springframework.scheduling.annotation.Async)") public Object logAsyncExecution(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); String className = joinPoint.getTarget().getClass().getSimpleName(); long startTime = System.currentTimeMillis(); logger.info("开始执行异步方法: {}.{}", className, methodName); try { Object result = joinPoint.proceed(); long executionTime = System.currentTimeMillis() - startTime; logger.info("异步方法执行完成: {}.{}, 耗时: {}ms", className, methodName, executionTime); return result; } catch (Exception e) { long executionTime = System.currentTimeMillis() - startTime; logger.error("异步方法执行异常: {}.{}, 耗时: {}ms, 异常: {}", className, methodName, executionTime, e.getMessage()); throw e; } }}11. 性能优化建议11.1 线程池参数调优@Configuration@EnableAsyncpublic class OptimizedAsyncConfig { @Bean("optimizedTaskExecutor") public ThreadPoolTaskExecutor optimizedTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 根据服务器CPU核心数动态设置 int cpuCores = Runtime.getRuntime().availableProcessors(); // CPU密集型任务 executor.setCorePoolSize(cpuCores); executor.setMaxPoolSize(cpuCores * 2); // IO密集型任务(注释掉的配置) // executor.setCorePoolSize(cpuCores * 2); // executor.setMaxPoolSize(cpuCores * 4); executor.setQueueCapacity(1000); executor.setKeepAliveSeconds(300); executor.setThreadNamePrefix("optimized-async-"); // 自定义拒绝策略 executor.setRejectedExecutionHandler((r, executor1) -> { logger.warn("线程池队列已满,任务被拒绝,当前活跃线程: {}", executor1.getActiveCount()); // 可以在这里添加降级逻辑 if (!executor1.isShutdown()) { r.run(); } }); executor.initialize(); return executor; }}11.2 最佳实践总结合理配置线程池参数:根据任务类型(CPU密集型/IO密集型)调整参数使用合适的拒绝策略:根据业务需求选择适当的拒绝策略异常处理:确保所有异步方法都有完善的异常处理机制资源清理:应用关闭时确保线程池正确关闭监控告警:建立线程池监控和告警机制避免过度使用:不是所有场景都适合异步处理12. 总结Spring Boot的异步处理功能为构建高性能、高并发的应用提供了强大的支持。通过合理使用@Async注解、配置线程池、处理异常和监控性能,可以显著提升系统的吞吐量和响应速度。本文详细介绍了Spring Boot异步处理的各个方面,从基础使用到高级特性,提供了完整的代码示例和流程图。在实际项目中,应根据具体业务需求和系统特点,合理设计和配置异步处理方案,以达到最佳的性能效果。附录:完整项目结构src/main/java/├── com/example/async/│ ├── config/│ │ ├── AsyncConfig.java│ │ ├── MultipleAsyncConfig.java│ │ └── AsyncExceptionConfig.java│ ├── controller/│ │ └── AsyncController.java│ ├── service/│ │ ├── AsyncService.java│ │ ├── NotificationService.java│ │ ├── CalculationService.java│ │ ├── AdvancedAsyncService.java│ │ └── AsyncTransactionService.java│ ├── aspect/│ │ └── AsyncExecutionAspect.java│ ├── monitor/│ │ └── ThreadPoolMonitor.java│ └── exception/│ └── GlobalAsyncExceptionHandler.java└── resources/ └── application.yml希望本文能够帮助您全面掌握Spring Boot中的异步处理技术,在实际项目中灵活运用,构建高性能的应用程序。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/qq_16242613/article/details/152948442
-
1. 自动配置概述1.1 什么是自动配置?Spring Boot 自动配置是其最重要的特性之一,它尝试根据添加的 jar 依赖自动配置 Spring 应用程序。简单来说,就是约定优于配置理念的具体实现。 传统 Spring 配置: @Configurationpublic class ManualDataSourceConfig { @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/test"); dataSource.setUsername("root"); dataSource.setPassword("password"); return dataSource; } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }} Spring Boot 自动配置: // 只需在 application.properties 中配置spring.datasource.url=jdbc:mysql://localhost:3306/testspring.datasource.username=rootspring.datasource.password=password// Spring Boot 自动创建 DataSource 和 JdbcTemplate 1.2 自动配置的优势快速启动:减少样板代码配置智能默认值:提供合理的默认配置灵活覆盖:可轻松自定义配置条件化装配:根据条件智能启用配置2. 自动配置核心原理2.1 核心注解 @SpringBootApplication让我们从启动类开始分析: @SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }} @SpringBootApplication 源码分析: @Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration // 关键注解@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication { // ...} 2.2 @EnableAutoConfiguration 注解这是自动配置的入口点: @Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class) // 核心导入public @interface EnableAutoConfiguration { // ...}AI写代码 2.3 AutoConfigurationImportSelector 类这是自动配置的核心处理器: public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } // 获取自动配置入口 AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { // 获取所有配置类 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); configurations = removeDuplicates(configurations); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = getConfigurationClassFilter().filter(configurations); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); } protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 从 META-INF/spring.factories 加载配置 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); return configurations; } protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }} 3. 自动配置加载机制3.1 spring.factories 文件Spring Boot 在 spring-boot-autoconfigure jar 包的 META-INF/spring.factories 文件中定义了大量的自动配置类: # Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration,\org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration,\org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration,\org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration 3.2 自动配置加载流程graph TD A[Spring Boot 启动] --> B[@SpringBootApplication] B --> C[@EnableAutoConfiguration] C --> D[AutoConfigurationImportSelector] D --> E[加载 spring.factories] E --> F[获取自动配置类列表] F --> G[条件注解过滤] G --> H[排除用户配置的类] H --> I[注册有效的配置类] I --> J[创建 Bean 定义] J --> K[完成自动配置] subgraph 条件过滤 G1[@ConditionalOnClass] G2[@ConditionalOnBean] G3[@ConditionalOnProperty] G4[@ConditionalOnMissingBean] end 4. 条件化配置详解4.1 常用条件注解Spring Boot 提供了一系列条件注解来控制配置类的加载: 注解 说明@ConditionalOnClass 类路径下存在指定类时生效@ConditionalOnMissingClass 类路径下不存在指定类时生效@ConditionalOnBean 容器中存在指定 Bean 时生效@ConditionalOnMissingBean 容器中不存在指定 Bean 时生效@ConditionalOnProperty 配置属性满足条件时生效@ConditionalOnResource 资源文件存在时生效@ConditionalOnWebApplication 是 Web 应用时生效@ConditionalOnNotWebApplication 不是 Web 应用时生效@ConditionalOnExpression SpEL 表达式为 true 时生效4.2 DataSource 自动配置示例让我们分析 DataSourceAutoConfiguration 的实现: @Configuration(proxyBeanMethods = false)@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")@EnableConfigurationProperties(DataSourceProperties.class)@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class })public class DataSourceAutoConfiguration { @Configuration(proxyBeanMethods = false) @Conditional(EmbeddedDatabaseCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import(EmbeddedDataSourceConfiguration.class) static class EmbeddedDatabaseConfiguration { } @Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration { } // 内部条件类 static class PooledDataSourceCondition extends SpringBootCondition { // 条件判断逻辑 }} 4.3 自定义条件注解我们也可以创建自定义条件注解: // 自定义条件注解@Target({ ElementType.TYPE, ElementType.METHOD })@Retention(RetentionPolicy.RUNTIME)@Documented@Conditional(OnProductionCondition.class)public @interface ConditionalOnProduction {} // 条件判断逻辑public class OnProductionCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment env = context.getEnvironment(); String profile = env.getProperty("spring.profiles.active"); return "prod".equals(profile); }} // 使用自定义条件注解@Configuration@ConditionalOnProductionpublic class ProductionConfiguration { // 生产环境特有的配置} 5. 自动配置实现实战5.1 创建自定义 Starter让我们创建一个简单的邮件服务自动配置: 项目结构: email-spring-boot-starter/├── src/│ └── main/│ ├── java/│ │ └── com/example/email/│ │ ├── EmailService.java│ │ ├── EmailProperties.java│ │ └── autoconfigure/│ │ └── EmailAutoConfiguration.java│ └── resources/│ └── META-INF/│ └── spring.factories EmailService.java: public class EmailService { private final String host; private final int port; private final String username; private final String password; public EmailService(String host, int port, String username, String password) { this.host = host; this.port = port; this.username = username; this.password = password; } public void sendEmail(String to, String subject, String content) { // 模拟发送邮件 System.out.printf("发送邮件到: %s, 主题: %s, 内容: %s%n", to, subject, content); System.out.printf("使用服务器: %s:%d, 用户: %s%n", host, port, username); }} EmailProperties.java: @ConfigurationProperties(prefix = "email")public class EmailProperties { private String host = "smtp.example.com"; private int port = 25; private String username; private String password; private boolean enabled = true; // getters and setters public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } 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; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; }} EmailAutoConfiguration.java: @Configuration@ConditionalOnClass(EmailService.class)@EnableConfigurationProperties(EmailProperties.class)public class EmailAutoConfiguration { private final EmailProperties properties; public EmailAutoConfiguration(EmailProperties properties) { this.properties = properties; } @Bean @ConditionalOnMissingBean @ConditionalOnProperty(prefix = "email", name = "enabled", havingValue = "true", matchIfMissing = true) public EmailService emailService() { return new EmailService( properties.getHost(), properties.getPort(), properties.getUsername(), properties.getPassword() ); } @Bean @ConditionalOnMissingBean public EmailController emailController(EmailService emailService) { return new EmailController(emailService); }} EmailController.java: public class EmailController { private final EmailService emailService; public EmailController(EmailService emailService) { this.emailService = emailService; } public void sendWelcomeEmail(String email) { emailService.sendEmail(email, "欢迎", "欢迎使用我们的服务!"); }} spring.factories: # Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.example.email.autoconfigure.EmailAutoConfiguration 5.2 使用自定义 Starter在应用项目中引入 starter 依赖: application.yml: email: host: smtp.163.com port: 465 username: your-email@163.com password: your-password enabled: true 使用示例: @RestControllerpublic class UserController { private final EmailController emailController; public UserController(EmailController emailController) { this.emailController = emailController; } @PostMapping("/register") public String register(@RequestParam String email) { // 用户注册逻辑 emailController.sendWelcomeEmail(email); return "注册成功"; }} 6. 自动配置高级特性6.1 配置顺序控制使用 @AutoConfigureAfter 和 @AutoConfigureBefore 控制配置顺序: @Configuration@AutoConfigureAfter(DataSourceAutoConfiguration.class)@AutoConfigureBefore(TransactionAutoConfiguration.class)public class MyCustomAutoConfiguration { // 这个配置会在 DataSource 配置之后,事务配置之前执行} 6.2 条件配置的复杂逻辑@Configuration@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })@ConditionalOnSingleCandidate(DataSource.class)@ConditionalOnProperty(prefix = "app.feature", name = "enabled", havingValue = "true")public class ComplexConditionConfiguration { @Bean @ConditionalOnMissingBean public MyRepository myRepository(JdbcTemplate jdbcTemplate) { return new MyRepository(jdbcTemplate); } static class MyRepository { private final JdbcTemplate jdbcTemplate; public MyRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // 数据访问方法 }} 6.3 使用 @ConditionalOnExpression@Configuration@ConditionalOnExpression( "${app.feature.enabled:false} && " + "T(org.springframework.util.StringUtils).hasText('${app.feature.api-key:}')")public class ExpressionConditionConfiguration { // 复杂的条件判断} 7. 自动配置调试与优化7.1 调试自动配置启用调试日志: # application.properties 查看自动配置报告:启动应用后,控制台会输出自动配置报告: =========================AUTO-CONFIGURATION REPORT========================= Positive matches:----------------- AopAutoConfiguration matched: - @ConditionalOnClass found required classes 'org.springframework.context.annotation.EnableAspectJAutoProxy', ... (OnClassCondition) - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition) DataSourceAutoConfiguration matched: - @ConditionalOnClass found required classes 'javax.sql.DataSource', ... (OnClassCondition) Negative matches:----------------- ActiveMQAutoConfiguration: Did not match: - @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition) Exclusions:----------- None 7.2 排除自动配置方式1:使用注解排除 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, MailSenderAutoConfiguration.class})public class Application { // ...} 方式2:使用配置属性排除 spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfigurationAI写代码properties17.3 性能优化建议合理使用条件注解:避免不必要的条件检查延迟初始化:使用 @Lazy 注解配置扫描路径:精确指定 @ComponentScan 路径排除不必要的自动配置———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/qq_16242613/article/details/152824353
-
什么是JWTJson web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。JWT请求流程1. 用户使用账号和面发出post请求;2. 服务器使用私钥创建一个jwt;3. 服务器返回这个jwt给浏览器;4. 浏览器将该jwt串在请求头中像服务器发送请求;5. 服务器验证该jwt;6. 返回响应的资源给浏览器。JWT的主要应用场景身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。优点1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。4.不需要在服务端保存会话信息,特别适用于分布式微服务。`JWT的结构JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。就像这样:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQJWT包含了三部分:Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)Payload 负载 (类似于飞机上承载的物品)Signature 签名/签证HeaderJWT的头部承载两部分信息:token类型和采用的加密算法。{ "alg": "HS256", "typ": "JWT"} 声明类型:这里是jwt声明加密的算法:通常直接使用 HMAC SHA256加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证Payload载荷就是存放有效信息的地方。有效信息包含三个部分1.标准中注册的声明2.公共的声明3.私有的声明标准中注册的声明 (建议但不强制使用) :iss: jwt签发者sub: 面向的用户(jwt所面向的用户)aud: 接收jwt的一方exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)nbf: 定义在什么时间之前,该jwt都是不可用的.iat: jwt的签发时间jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。公共的声明 :公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.私有的声明 :私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。Signaturejwt的第三部分是一个签证信息,这个签证信息由三部分组成:header (base64后的)payload (base64后的)secret这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。下面来进行SpringBoot和JWT的集成引入JWT依赖,由于是基于Java,所以需要的是java-jwt<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version></dependency>需要自定义两个注解用来跳过验证的PassToken@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface PassToken { boolean required() default true;}需要登录才能进行操作的注解UserLoginToken@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface UserLoginToken { boolean required() default true;}@Target:注解的作用目标@Target(ElementType.TYPE)——接口、类、枚举、注解@Target(ElementType.FIELD)——字段、枚举的常量@Target(ElementType.METHOD)——方法@Target(ElementType.PARAMETER)——方法参数@Target(ElementType.CONSTRUCTOR) ——构造函数@Target(ElementType.LOCAL_VARIABLE)——局部变量@Target(ElementType.ANNOTATION_TYPE)——注解@Target(ElementType.PACKAGE)——包@Retention:注解的保留位置RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。@Document:说明该注解将被包含在javadoc中@Inherited:说明子类可以继承父类中的该注解简单自定义一个实体类User,使用lombok简化实体类的编写@Data@AllArgsConstructor@NoArgsConstructorpublic class User { String Id; String username; String password;}需要写token的生成方法public String getToken(User user) { String token=""; token= JWT.create().withAudience(user.getId()) .sign(Algorithm.HMAC256(user.getPassword())); return token; }Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。withAudience()存入需要保存在token的信息,这里我把用户ID存入token中接下来需要写一个拦截器去获取token并验证tokenpublic class AuthenticationInterceptor implements HandlerInterceptor { @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token // 如果不是映射到方法直接通过 if(!(object instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)object; Method method=handlerMethod.getMethod(); //检查是否有passtoken注释,有则跳过认证 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(UserLoginToken.class)) { UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class); if (userLoginToken.required()) { // 执行认证 if (token == null) { throw new RuntimeException("无token,请重新登录"); } // 获取 token 中的 user id String userId; try { userId = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { throw new RuntimeException("401"); } User user = userService.findUserById(userId); if (user == null) { throw new RuntimeException("用户不存在,请重新登录"); } // 验证 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { throw new RuntimeException("401"); } return true; } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { }实现一个拦截器就需要实现HandlerInterceptor接口HandlerInterceptor接口主要定义了三个方法1.boolean preHandle ():预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。2.void postHandle():后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。3.void afterCompletion():整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中主要流程:1.从 http 请求头中取出 token,2.判断是否映射到方法3.检查是否有passtoken注释,有则跳过认证4.检查有没有需要用户登录的注解,有则需要取出并验证5.认证通过则可以访问,不通过会报相关错误信息配置拦截器在配置类上添加了注解@Configuration,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内@Configurationpublic class InterceptorConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录 } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); }}WebMvcConfigurerAdapter该抽象类其实里面没有任何的方法实现,只是空实现了接口WebMvcConfigurer内的全部方法,并没有给出任何的业务逻辑处理,这一点设计恰到好处的让我们不必去实现那些我们不用的方法,都交由WebMvcConfigurerAdapter抽象类空实现,如果我们需要针对具体的某一个方法做出逻辑处理,仅仅需要在WebMvcConfigurerAdapter子类中@Override对应方法就可以了。注:在SpringBoot2.0及Spring 5.0中WebMvcConfigurerAdapter已被废弃网上有说改为继承WebMvcConfigurationSupport(),不过试了下,还是过期的解决方法:直接实现WebMvcConfigurer (官方推荐)@Configurationpublic class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); }}InterceptorRegistry内的addInterceptor需要一个实现HandlerInterceptor接口的拦截器实例,addPathPatterns方法用于设置拦截器的过滤路径规则。这里我拦截所有请求,通过判断是否有@LoginRequired注解 决定是否需要登录在数据访问接口中加入登录操作注解@RestController@RequestMapping("api")public class UserApi { @Autowired UserService userService; @Autowired TokenService tokenService; //登录 @PostMapping("/login") public Object login(@RequestBody User user){ JSONObject jsonObject=new JSONObject(); User userForBase=userService.findByUsername(user); if(userForBase==null){ jsonObject.put("message","登录失败,用户不存在"); return jsonObject; }else { if (!userForBase.getPassword().equals(user.getPassword())){ jsonObject.put("message","登录失败,密码错误"); return jsonObject; }else { String token = tokenService.getToken(userForBase); jsonObject.put("token", token); jsonObject.put("user", userForBase); return jsonObject; } } } @UserLoginToken @GetMapping("/getMessage") public String getMessage(){ return "你已通过验证"; }}不加注解的话默认不验证,登录接口一般是不验证的。在getMessage()中我加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问下面进行测试,启动项目,使用postman测试接口在没token的情况下访问api/getMessage接口我这里使用了统一异常处理,所以只看到错误message 下面进行登录,从而获取token登录操作我没加验证注解,所以可以直接访问 把token加在请求头中,再次访问api/getMessage接口注意:这里的key一定不能错,因为在拦截器中是取关键字token的值String token = httpServletRequest.getHeader("token");加上token之后就可以顺利通过验证和进行接口访问了
-
MQTT配置 1. 前言公司的IOT平台主要采用MQTT(消息队列遥测传输)对底层的驱动做命令下发和数据采集。也用到了redis、zeroMQ、nats等消息中间件。今天先整理SpringBoot集成MQTT笔记和工作中遇到的问题。2. MQTT介绍MQTT is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. It was designed as an extremely lightweight publish/subscribe messaging transport. It is useful for connections with remote locations where a small code footprint is required and/or network bandwidth is at a premium.官网地址:http://mqtt.org/ 、 https://www.mqtt.com/MQTT除了具备大部分消息中间件拥有的功能外,其最大的特点就是小型传输。以减少开销,减低网络流量的方式去满足低带宽、不稳定的网络远程传输。MQTT服务器有很多,比如Apache-Apollo和EMQX,ITDragon龙 目前使用的时EMQX作为MQTT的服务器。使用也很简单,下载解压后,进入bin目录执行emqx console 启动服务。MQTT调试工具可以用MQTTBox3. SpringBoot 集成MQTT3.1 导入mqtt库第一步:导入面向企业应用集成库和对应mqtt集成库compile('org.springframework.boot:spring-boot-starter-integration')compile('org.springframework.integration:spring-integration-mqtt')这里要注意spring-integration-mqtt的版本。因为会存在org.eclipse.paho.client.mqttv3修复了一些bug,并迭代了新版本。但spring-integration-mqtt并没有及时更新的情况。修改方法如下compile("org.springframework.integration:spring-integration-mqtt") { exclude group: "org.eclipse.paho" , module: "org.eclipse.paho.client.mqttv3"}compile("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.2")第二步:MQTT连接配置文件# MQTT Configmqtt.server=tcp://x.x.x.x:1883mqtt.username=xxxmqtt.password=xxxmqtt.client-id=clientIDmqtt.cache-number=100mqtt.message.topic=itDragon/tags/cov3.2 配置MQTT订阅者Inbound 入站消息适配器第一步:配置MQTT客户端工厂类DefaultMqttPahoClientFactory第二步:配置MQTT入站消息适配器MqttPahoMessageDrivenChannelAdapter第三步:定义MQTT入站消息通道MessageChannel第四步:声明MQTT入站消息处理器MessageHandler以下有些配置是冲突或者重复的,主要是体现一些重要配置。package com.itdragon.server.configimport com.itdragon.server.message.ITDragonMQTTMessageHandlerimport org.eclipse.paho.client.mqttv3.MqttConnectOptionsimport org.springframework.beans.factory.annotation.Valueimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.integration.annotation.ServiceActivatorimport org.springframework.integration.channel.DirectChannelimport org.springframework.integration.core.MessageProducerimport org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactoryimport org.springframework.integration.mqtt.core.MqttPahoClientFactoryimport org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapterimport org.springframework.integration.mqtt.support.DefaultPahoMessageConverterimport org.springframework.messaging.MessageChannelimport org.springframework.messaging.MessageHandlerimport java.time.Instant@Configurationclass MQTTConfig { @Value("\${mqtt.server}") lateinit var mqttServer: String @Value("\${mqtt.user-name}") lateinit var mqttUserName: String @Value("\${mqtt.password}") lateinit var mqttUserPassword: String @Value("\${mqtt.client-id}") lateinit var clientID: String @Value("\${mqtt.cache-number}") lateinit var maxMessageInFlight: String @Value("\${mqtt.message.topic}") lateinit var messageTopic: String /** * 配置DefaultMqttPahoClientFactory * 1. 配置基本的链接信息 * 2. 配置maxInflight,在mqtt消息量比较大的情况下将值设大 */ fun mqttClientFactory(): MqttPahoClientFactory { val mqttConnectOptions = MqttConnectOptions() // 配置mqtt服务端地址,登录账号和密码 mqttConnectOptions.serverURIs = arrayOf(mqttServer) mqttConnectOptions.userName = mqttUserName mqttConnectOptions.password = mqttUserPassword.toCharArray() // 配置最大不确定接收消息数量,默认值10,qos!=0 时生效 mqttConnectOptions.maxInflight = maxMessageInFlight.toInt() val factory = DefaultMqttPahoClientFactory() factory.connectionOptions = mqttConnectOptions return factory } /** * 配置Inbound入站,消费者基本连接配置 * 1. 通过DefaultMqttPahoClientFactory 初始化入站通道适配器 * 2. 配置超时时长,默认30000毫秒 * 3. 配置Paho消息转换器 * 4. 配置发送数据的服务质量 0~2 * 5. 配置订阅通道 */ @Bean fun itDragonMqttInbound(): MessageProducer { // 初始化入站通道适配器,使用的是Eclipse Paho MQTT客户端库 val adapter = MqttPahoMessageDrivenChannelAdapter(clientID + Instant.now().toEpochMilli(), mqttClientFactory(), messageTopic) // 设置连接超时时长(默认30000毫秒) adapter.setCompletionTimeout(30000) // 配置默认Paho消息转换器(qos=0, retain=false, charset=UTF-8) adapter.setConverter(DefaultPahoMessageConverter()) // 设置服务质量 // 0 最多一次,数据可能丢失; // 1 至少一次,数据可能重复; // 2 只有一次,有且只有一次;最耗性能 adapter.setQos(0) // 设置订阅通道 adapter.outputChannel = itDragonMqttInputChannel() return adapter } /** * 配置Inbound入站,消费者订阅的消息通道 */ @Bean fun itDragonMqttInputChannel(): MessageChannel { return DirectChannel() } /** * 配置Inbound入站,消费者的消息处理器 * 1. 使用@ServiceActivator注解,表明所修饰的方法用于消息处理 * 2. 使用inputChannel值,表明从指定通道中取值 * 3. 利用函数式编程的思路,解耦MessageHandler的业务逻辑 */ @Bean @ServiceActivator(inputChannel = "itDragonMqttInputChannel") fun commandDataHandler(): MessageHandler { /*return MessageHandler { message -> println(message.payload) }*/ return ITDragonMQTTMessageHandler() }}注意:1)MQTT的客户端ID要唯一。2)MQTT在消息量大的情况下会出现消息丢失的情况。3)MessageHandler注意解耦问题。3.3 配置MQTT发布者Outbound 出站消息适配器第一步:配置Outbound出站,出站通道适配器第二步:配置Outbound出站,发布者发送的消息通道第三步:对外提供推送消息的接口在原有的MQTTConfig配置类的集成上补充以下内容 /** * 配置Outbound出站,出站通道适配器 * 1. 通过MqttPahoMessageHandler 初始化出站通道适配器 * 2. 配置异步发送 * 3. 配置默认的服务质量 */ @Bean @ServiceActivator(inputChannel = "itDragonMqttOutputChannel") fun itDragonMqttOutbound(): MqttPahoMessageHandler { // 初始化出站通道适配器,使用的是Eclipse Paho MQTT客户端库 val messageHandler = MqttPahoMessageHandler(clientID + Instant.now().toEpochMilli() + "_set", mqttClientFactory()) // 设置异步发送,默认是false(发送时阻塞) messageHandler.setAsync(true) // 设置默认的服务质量 messageHandler.setDefaultQos(0) return messageHandler } /** * 配置Outbound出站,发布者发送的消息通道 */ @Bean fun itDragonMqttOutputChannel(): MessageChannel { return DirectChannel() } /** * 对外提供推送消息的接口 * 1. 使用@MessagingGateway注解,配置MQTTMessageGateway消息推送接口 * 2. 使用defaultRequestChannel值,调用时将向其发送消息的默认通道 * 3. 配置灵活的topic主题 */ @MessagingGateway(defaultRequestChannel = "itDragonMqttOutputChannel") interface MQTTMessageGateway { fun sendToMqtt(data: String, @Header(MqttHeaders.TOPIC) topic: String) fun sendToMqtt(data: String, @Header(MqttHeaders.QOS) qos: Int, @Header(MqttHeaders.TOPIC) topic: String) } 注意:1)发布者和订阅者的客户端ID不能相同。2)消息的推送建议采用异步的方式。3)消息的推送方法可以只传payload消息体,但需要配置setDefaultTopic。3.4 MQTT消息处理和发送3.4.1 消息处理为了让消息处理函数和MQTT配置解耦,这里提供MessageHandler 注册类,将消息处理的业务逻辑以函数式编程的思维注册到Handler中。package com.itdragon.server.messageimport org.springframework.messaging.Messageimport org.springframework.messaging.MessageHandlerclass ITDragonMQTTMessageHandler : MessageHandler { private var handler: ((String) -> Unit)? = null fun registerHandler(handler: (String) -> Unit) { this.handler = handler } override fun handleMessage(message: Message<*>) { handler?.run { this.invoke(message.payload.toString()) } }}注册MessageHandlerpackage com.itdragon.server.messageimport org.slf4j.LoggerFactoryimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.stereotype.Serviceimport javax.annotation.PostConstruct@Serviceclass ITDragonMessageDispatcher { private val logger = LoggerFactory.getLogger(ITDragonMessageDispatcher::class.java) @Autowired lateinit var itDragonMQTTMessageHandler: ITDragonMQTTMessageHandler @PostConstruct fun init() { itDragonMQTTMessageHandler.registerHandler { itDragonMsgHandler(it) } } fun itDragonMsgHandler(message: String) { logger.info("itdragon mqtt receive message: $message") try { // todo }catch (ex: Exception) { ex.printStackTrace() } }}3.4.1 消息发送注入MQTT的MessageGateway,然后推送消息。@Autowiredlateinit var mqttGateway: MQTTConfig.MQTTMessageGateway@Scheduled(fixedDelay = 10*1000)fun sendMessage() { mqttGateway.sendToMqtt("Hello ITDragon ${Instant.now()}", "itDragon/tags/cov/set")}4. 开发常见问题4.1 MQTT每次重连失败都会增长线程数项目上线一段时间后,客户的服务器严重卡顿。原因是客户服务断网后,MQTT在每次尝试重连的过程中一直在创建新的线程,导致一个Java服务创建了上万个线程。解决方案是更新了org.eclipse.paho.client.mqttv3的版本,也是 "3.1 导入mqtt库" 中提到的。后续就没有出现这个问题了。4.2 MQTT消息量大存在消息丢失的情况MQTT的消息量大的情况下,既要保障数据的完整,又要保障性能的稳定。光从MQTT本身上来说,很难做到鱼和熊掌不可兼得。 先要理清需求:1)数据的完整性,主要用于能耗的统计、报警的分析2)性能的稳定性,服务器不挂🤣🤣🤣🤣在消息量大的情况下, 可以将服务质量设置成0(最多一次)以减少消息确认的开销,用来保证系统的稳定性。将消息的服务质量设置成0后,会让消息的丢失可能性变得更大,如何保证数据的完整性?其实可以在往MQTT通道推送消息之前,先将底层驱动采集的数据先异步保存到Inflxudb数据库中。还有就是每次发送消息量不能太大,太大也会导致消息丢失。最直接的就是后端报错,比如:java.io.EOFException 和 too large message: xxx bytes 。但是有的场景后端没有报错,前端订阅的mqtt也没收到消息。最麻烦的是mqttbox工具因为数据量太大直接卡死。一时间真不知道把锅甩给谁。其实我们 可以将消息拆包一批批发送。可以缓解这个问题🤣🤣🤣🤣。其实采集的数据消息,若在这一批推送过程中丢失。也会在下一批推送过程中补上。命令下发也是一样,如果下发失败,再重写下发一次。毕竟消息的丢失并不是必现的情况。也是小概率事件,系统的稳定性才是最重要的。转载自https://www.cnblogs.com/itdragon/p/12463050.html
-
前言在当今数字化时代,随着互联网应用的飞速发展,数据传输的效率和性能成为了至关重要的问题。GeoJSON 是一种基于 JSON 格式的地理空间数据交换格式,它广泛应用于地理信息系统(GIS)领域,用于描述地理空间数据的几何形状、属性等信息。GeoJSON 数据通常包含大量的地理坐标点、几何形状等信息,数据量往往较大。在 Web 地理信息系统应用中,如地图展示、地理数据可视化等场景,GeoJSON 数据的传输效率直接关系到地图加载的速度和用户体验。因此,对 GeoJSON 数据进行有效的压缩,以减少数据传输的体积,显得尤为重要。对于基于SpringBoot 框架构建的 WebGIS 应用来说,如何高效地传输数据、减少网络带宽的占用,是提升用户体验和系统性能的关键所在。而 Gzip 压缩技术,作为一种被广泛采用的解决方案,无疑为这一问题的解决提供了强大的助力。图片以 GeoJSON 数据为例,通过在 SpringBoot 应用中开启 Gzip 压缩,对 GeoJSON 数据进行瘦身,不仅可以显著减少数据传输的体积,提高地图加载的速度,还可以提升用户的交互体验。在实际的 Web 地理信息系统开发中,这种优化手段是非常实用和有效的。通过对 GeoJSON 数据的压缩处理,我们可以更好地满足用户对于地图快速加载和流畅交互的需求,同时也为整个应用的性能优化提供了有力的支持。在接下来的内容中,我们将详细介绍在 SpringBoot 中开启 Gzip 压缩的两种方式的具体实现步骤,并通过实际的 GeoJSON 数据压缩案例,展示这两种方式的应用效果和优缺点。希望通过本文的介绍,能够帮助读者更好地理解和掌握在 SpringBoot 应用中使用 Gzip 压缩技术的方法,从而提升自己开发的 WebGIS 应用的性能和用户体验。一、GZIP压缩知识简介GZIP 是一种数据压缩格式,只能用于压缩单个文件。它可用于网络文件传输时的压缩,例如 nginx 中的 ngx_http_gzip_module,启用压缩功能后可以节约带宽;也可用于本地文件存储时的压缩。本节将重点对Gzip进行一个简单的介绍,让大家对Gzip的相关知识有一个简单的了解。1、什么是GzipGzip 的压缩算法基于 LZ77 算法 和 Huffman 编码 的结合。具体过程如下:• LZ77 算法: LZ77 算法通过查找和替换重复的字节序列来压缩数据。它维护一个滑动窗口,在窗口内查找匹配的字符串,然后使用指针来替代这些重复的字符串。例如,对于字符串 "http://jiurl.yeah.nethttp://jiurl.nease.net",LZ77 算法会将其压缩为 "http://jiurl.yeah.net(22,13)nease(23,4)",其中 (22,13) 表示距离当前位置 22 个字符处的 13 个字符与当前位置的字符相同。• Huffman 编码: Huffman 编码是一种基于字符频率的编码方法。它为出现频率高的字符分配较短的编码,为出现频率低的字符分配较长的编码,从而达到压缩的目的。在 Gzip 中,LZ77 算法的输出结果会进一步通过 Huffman 编码进行压缩。• Gzip 文件结构: Gzip 文件包含文件头、压缩数据块和文件尾。文件头存储文件的元数据,如压缩方法、时间戳等;压缩数据块是使用 DEFLATE 算法压缩后的数据;文件尾存储校验和(CRC32)和原始文件大小,以确保文件的完整性2、Gzip特点• 无损压缩: Gzip 是一种无损压缩算法,数据在解压缩后可以完全还原,没有任何损失。• 高效的压缩率: 对于文本文件(如 HTML、JSON、XML),Gzip 的压缩率通常在 50%-90% 之间。它通过查找重复的字符串并用较短的指针替代,以及根据字符频率进行编码,从而实现高效压缩。• 广泛支持: Gzip 被几乎所有现代浏览器、服务器和编程语言支持。在 HTTP 传输中,服务器可以根据浏览器的请求头(如 Accept-Encoding: gzip)来判断是否使用 Gzip 压缩响应内容。• 压缩和解压速度较快: Gzip 的压缩和解压速度相对较快,尤其是解压过程,因为解压时只需根据指针和编码还原数据,计算量相对较小。• 适用于特定文件类型: Gzip 对文本文件的压缩效果较好,但对于已经压缩过的文件(如图片、音乐、视频)效果不明显,甚至可能导致文件变大3、Gzip在GIS方面的应用• 地理数据传输: 在 GIS 应用中,地理数据(如 GeoJSON 文件)通常包含大量的坐标点和几何形状信息,数据量较大。使用 Gzip 压缩可以显著减少数据传输的体积,加快地图加载速度,提升用户体验。• 服务器端优化: 在服务器端,可以配置 Web 服务器(如 Nginx)开启 Gzip 压缩功能。当客户端请求地理数据时,服务器会自动对响应内容进行压缩,减少网络带宽的占用。• 前端性能优化: 在前端开发中,可以通过工具(如 Webpack 的 Compression-webpack-plugin 插件)在构建过程中对地理数据文件进行 Gzip 压缩,然后在服务器上直接提供压缩后的文件,减少服务器的实时压缩负载。• 数据存储优化: 对于存储在服务器上的地理数据文件,使用 Gzip 压缩可以节省存储空间。在需要读取数据时,再进行解压处理。总之,Gzip 压缩技术在 GIS 领域的应用,不仅可以提高数据传输效率,还可以优化服务器性能和存储空间,是提升 GIS 应用性能的重要手段之一。二、SpringBoot中开启Gzip的方式SpringBoot 是一个非常流行的 Java 基于 Spring 框架的快速开发框架,它极大地简化了 Spring 应用的开发过程。在 SpringBoot 应用中,通过合理的配置和编程,可以很方便地集成 Gzip 压缩功能,从而实现对响应数据的自动压缩。这不仅可以提高数据传输的效率,还可以减轻服务器的负载,提升整个应用的性能。本节将重点介绍在SpringBoot中关于Gzip的相关知识以及在SpringBoot中GeoJSON的一些实践案例。1、在SpringBoot中开启Gzip的知识简介在 SpringBoot 中,开启 Gzip 压缩主要有两种方式:一种是通过配置文件进行全局配置,另一种是通过编程的方式在特定的控制器或方法上进行局部配置。这两种方式各有优缺点,适用于不同的应用场景。• 全局配置方式 : 通过在 SpringBoot 的配置文件(如 application.yml 或 application.properties)中添加相关的 Gzip 压缩配置,可以实现对整个应用的 HTTP 响应进行统一的压缩处理。这种方式简单方便,适用于大多数需要压缩的场景,但缺乏对特定数据类型的针对性处理。• 局部配置方式 : 通过在控制器或方法上添加自定义的注解或逻辑,可以实现对特定数据类型的压缩处理。这种方式更加灵活,可以根据不同的数据类型和业务需求,定制不同的压缩策略,但相对来说实现起来较为复杂,需要更多的编程工作。这里首先简单介绍了两种在SpringBoot中开启Gzip压缩的方式,为下文全面讲解这两种方式做准备,先让大家了解相关知识。2、SpringBoot中GeoJSON的实例GeoJSON在WebGIS中的用处很多,很多矢量数据的边界,范围点等数据,我们都是直接以GeoJSON的格式返回给前端,并直接进行展示的。比如之前很多的行政区划展示,省市县等不同的行政区划范围展示等,我们的实现过程都是在后台的Controller层中返回一个包含GeoJSON的对象,方法如下:package com.yelang.project.meteorology.domain;import java.io.Serializable;import com.baomidou.mybatisplus.annotation.TableField;import lombok.Data;import lombok.EqualsAndHashCode;import lombok.ToString;@Data@ToString(callSuper=true)//callSuper=true表示输出父类属性@EqualsAndHashCode(callSuper=false)public class AreaWeatherVO extends WeatherNow implements Serializable{ private static final long serialVersionUID = -7559774548761847068L; @TableField(exist = false,value= "province_code") private String provinceCode; @TableField(exist = false,value= "province_name") private String provinceName; @TableField(exist = false,value= "city_code") private String cityCode; @TableField(exist = false,value= "city_name") private String cityName; @TableField(exist = false,value= "area_name") private String areaName; @TableField(exist = false) private String geomJson; private String lat; private String lon;}上面是一个视图对象的具体代码,在Controller的方法中我们调用如下:@RequiresPermissions("met:province:weather:list")@GetMapping("/list/{pcode}")@ResponseBodypublic AjaxResult ewsnProvinceList(@PathVariable("pcode") String pcode){ String day = "2025-08-17"; List<AreaWeatherVO> dataList = weatherNowService.getWeatherByProvinceAndday(pcode,day); return AjaxResult.success().put("data", dataList); }经过以上的代码输出接口中就包含GeoJOSN数据,如下图所示:图片其具体的geoJSON值如下图:图片在网络窗口中可以看到整个接口返回的数据大小大约为5MB,如果遇到更大范围的行政区划,返回的数据肯定会大,比如西藏的行政区划大约有14MB,如图所示:图片那么如何通过开启Gzip来减少这些数据的输出呢?下面两个部分来重点讲解。三、全局开启Gzip实现本节将详细介绍如何在SpringBoot中开启Gzip压缩的配置。1、实现原理在 application.yml 或 application.properties 中添加以下配置:示例:application.ymlserver: compression: enabled: true mime-types: application/json min-response-size: 1KB # 小于1KB的响应不压缩或者application.properties中:server.compression.enabled=trueserver.compression.mime-types=application/jsonserver.compression.min-response-size=1024请注意:该配置会对所有返回 application/json 的接口启用 GZIP 压缩。2、实现效果在我们的工程中配置文件是以yml的形式配置的,按照上面的步骤进行设置后,重新启动应用程序后来看一下同样的接口,其返回的数据量大小是多少:图片通过以上图片可以直观的看到,开启全局压缩后,我们的接口返回大小,从14.4M下降了5MB,几乎是原来的1/3,这个压缩比例还是可以的。四、局部约定配置上面的这种实现方式全局的开启,也就是所有的接口都会开启,虽然可以设置mime-types来进行一定的过滤,但是依然会有很大的覆盖面。如果只想对某个接口生效或者指定一些接口生效又应该怎么实现呢?本节来讲讲针对这种情况的实现。1、实现原理基于局部约定配置的方式的实现原理其实是通过自定义 Filter 来精确控制哪些接口启用压缩,因此通过过滤器就可以将我们需要设定的请求路径进行针对性过滤,从而开启针对这些接口的Gzip过滤压缩。下面我们来看看在SpringBoot中如何实现呢?2、具体代码实现首先在SpringBoot中直接创建一个过滤器Filter,关键代码如下:package com.yelang.framework.interceptor.gzip;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import java.io.IOException;import java.util.Arrays;import java.util.List;import java.util.zip.GZIPOutputStream;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.ServletOutputStream;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.WriteListener;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpServletResponseWrapper;@Componentpublic class SelectiveGzipFilter implements Filter { private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final List<String> gzipPatterns = Arrays.asList( "/eq/province/geojson/**", "/eq/province/detourcoefficient/list/**", "/eq/info/home/earthinfo", "/eq/province/abbreviations/list" ); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String requestUri = req.getRequestURI(); String contextPath = req.getContextPath(); boolean match = gzipPatterns.stream() .anyMatch(pattern -> pathMatcher.match(pattern, requestUri)); if (match) { System.out.println("成功匹配"); // 启用 GZIP 压缩逻辑(同上) HttpServletResponse res = (HttpServletResponse) response; res.setHeader("Content-Encoding", "gzip"); res.setHeader("Content-Type", "application/json"); GZIPResponseWrapper gzipResponse = new GZIPResponseWrapper(res); chain.doFilter(request, gzipResponse); gzipResponse.finish(); } else { System.out.println("未匹配上..."); chain.doFilter(request, response); } } static class GZIPResponseWrapper extends HttpServletResponseWrapper { private final GZIPOutputStream gzipOutputStream; public GZIPResponseWrapper(HttpServletResponse response) throws IOException { super(response); gzipOutputStream = new GZIPOutputStream(response.getOutputStream()); } @Override public ServletOutputStream getOutputStream() { return new ServletOutputStream() { @Override public void write(int b) throws IOException { gzipOutputStream.write(b); } @Override public boolean isReady() { returntrue; } @Override public void setWriteListener(WriteListener writeListener) { throw new UnsupportedOperationException(); } }; } public void finish() throws IOException { gzipOutputStream.finish(); } }}这里演示了如何设置多个目标URL地址的配置方式,使用AntPathMatcher 来进行匹配实现。需要注意的是,这里我们没有区分请求的头地址,以若依为例,可能会在匹配时无法正确对应,导致无法正常的开启Gzip压缩,因此为了保证正确的启用,我们在进行地址匹配时,需要自动过滤项目的服务名称,详细代码如下:String contextPath = req.getContextPath();// 去掉 context-path 部分,只匹配相对路径String relativePath = requestUri.substring(contextPath.length());System.out.println("relativePath==>" + relativePath);/*boolean match = gzipPatterns.stream() .anyMatch(pattern -> pathMatcher.match(pattern, requestUri));*/boolean match = gzipPatterns.stream() .anyMatch(pattern -> pathMatcher.match(pattern, relativePath));其实这里的contenxPath对应的就是配置文件中定义的参数:# 开发环境配置server: # 服务器的HTTP端口,默认为80 port: 8080 servlet: # 应用的访问路径 context-path: /earthqadminGeoJSON 数据通常体积较大,压缩后可显著减少传输时间。以下是实战建议:图片只有加上以上代码后才能实现正确匹配,在访问地址被请求是输出如下信息:图片五、总结以上就是本文的主要内容,我们将详细介绍在 SpringBoot 中开启 Gzip 压缩的两种方式的具体实现步骤,并通过实际的 GeoJSON 数据压缩案例,展示这两种方式的应用效果和优缺点。希望通过本文的介绍,能够帮助读者更好地理解和掌握在 SpringBoot 应用中使用 Gzip 压缩技术的方法,从而提升自己开发的 WebGIS 应用的性能和用户体验。博文首先简单介绍了Gzip的相关知识,然后介绍了在SpringBoot中开启Gzip的方式,最后以代码加案例的形式详细的介绍全局开启Gzip和局部开启Gzip的两种不同模式实现原理及具体代码实现。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。
-
无需重启服务,实时更新配置! 本文将深入探索Spring Boot中@RefreshScope的神奇力量,让你的应用配置在运行时动态刷新,彻底告别服务重启的烦恼。 一、为什么需要动态刷新配置?在传统Java应用中,修改配置文件后必须重启服务才能生效,这会导致:• 服务中断: 重启期间服务不可用• 状态丢失: 内存中的临时数据被清空• 运维复杂: 需要复杂的发布流程Spring Boot的@RefreshScope完美解决了这些问题,实现配置热更新,让应用像乐高积木一样灵活重组! 二、@RefreshScope核心原理 1. 工作原理图解graph TD A[修改配置文件] --> B[发送POST刷新请求] B --> C[/actuator/refresh 端点] C --> D[RefreshScope 刷新机制] D --> E[销毁旧Bean并创建新Bean] E --> F[新配置立即生效] 2. 关键技术解析• 作用域代理: 为Bean创建动态代理,拦截方法调用• 配置绑定: 当配置更新时,重新绑定@Value注解的值• Bean生命周期管理: 销毁并重新初始化被@RefreshScope标记的Bean 三、完整实现步骤 步骤1:添加必要依赖<!-- pom.xml --><dependencies> <!-- Spring Boot基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 配置刷新核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- 配置中心支持 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> <version>3.1.3</version> </dependency></dependencies> 步骤2:启用刷新机制// 主应用类@SpringBootApplication@EnableRefreshScope // 关键注解:开启配置刷新能力publicclassDynamicConfigApp { publicstaticvoidmain(String[] args) { SpringApplication.run(DynamicConfigApp.class, args); }}步骤3:配置application.yml# 应用基础配置app:feature: enabled:true timeout:5000 retry-count:3 welcome-msg:"Hello, Dynamic Config!"# 暴露刷新端点(关键!)management:endpoints: web: exposure: include:refresh,health,info 步骤4:创建动态配置Bean@Service@RefreshScope// 标记此Bean支持动态刷新publicclassFeatureService { // 注入可刷新的配置项 @Value("${app.feature.enabled}") privateboolean featureEnabled; @Value("${app.feature.timeout}") privateint timeout; @Value("${app.feature.retry-count}") privateint retryCount; @Value("${app.feature.welcome-msg}") private String welcomeMessage; public String getFeatureConfig() { return String.format(""" Feature Enabled: %s Timeout: %d ms Retry Count: %d Message: %s """, featureEnabled, timeout, retryCount, welcomeMessage); }} 步骤5:创建测试控制器@RestController@RequestMapping("/config")publicclassConfigController { privatefinal FeatureService featureService; // 构造函数注入 publicConfigController(FeatureService featureService) { this.featureService = featureService; } @GetMapping public String getConfig() { return featureService.getFeatureConfig(); }} 步骤6:触发配置刷新修改application.yml后,发送刷新请求:curl -X POST http://localhost:8080/actuator/refresh响应示例(返回被修改的配置项):["app.feature.timeout", "app.feature.welcome-msg"] 四、深入理解@RefreshScope 1. 作用域代理原理// 伪代码:Spring如何实现动态刷新publicclassRefreshScopeProxyimplementsApplicationContextAware { private Object targetBean; @Override public Object invoke(Method method) { if (configChanged) { // 1. 销毁旧Bean context.destroyBean(targetBean); // 2. 重新创建Bean targetBean = context.getBean(beanName); } return method.invoke(targetBean, args); }} 2. 刷新范围控制技巧场景1:只刷新特定Bean的部分属性@Component@RefreshScopepublicclassPaymentService { // 只有带@Value的属性会刷新 @Value("${payment.timeout}") privateint timeout; // 不会被刷新的属性 privatefinalStringapiVersion="v1.0"; }场景2:组合配置类刷新@Configuration@RefreshScope// 整个配置类可刷新publicclassAppConfig { @Bean @RefreshScope public FeatureService featureService() { returnnewFeatureService(); } @Value("${app.theme}") private String theme;} 五、生产环境最佳实践 1. 安全加固配置management: endpoint: refresh: enabled:trueendpoints: web: exposure: include:refresh base-path:/internal# 修改默认路径 path-mapping: refresh:secure-refresh# 端点重命名# 添加安全认证spring:security: user: name:admin password:$2a$10$NVM0n8ElaRgg7zWO1CxUdei7vWoQP91oGycgVNCY8GQEx.TGx.AaC 2. 自动刷新方案方案1:Git Webhook自动刷新 方案2:配置中心联动(Nacos示例)//bootstrap.ymlspring: cloud: nacos: config: server-addr:localhost:8848 auto-refresh:true # 开启自动刷新 六、常见问题排查 问题1:刷新后配置未生效解决方案:• 检查是否添加@RefreshScope• 确认刷新端点返回了修改的配置项• 查看日志:logging.level.org.springframework.cloud=DEBUG 问题2:多实例刷新不同步解决方案:# 使用Spring Cloud Bus同步刷新curl-XPOSThttp://host:port/actuator/bus-refresh 问题3:配置更新导致内存泄漏预防措施:@PreDestroypublicvoidcleanUp() { // 清理资源} 七、扩展应用场景动态功能开关:实时开启/关闭功能模块# 修改后立即生效feature.new-checkout.enabled=true运行时日志级别调整@RefreshScopepublicclassLogConfig { @Value("${logging.level.root}") private String logLevel; // 动态应用新日志级别}数据库连接池调优# 动态修改连接池配置spring.datasource.hikari.maximum-pool-size=20 结语:拥抱动态配置新时代通过@RefreshScope,我们实现了:• ✅ 零停机配置更新• ✅ 即时生效的应用参数• ✅ 更灵活的运维体验• ✅ 资源利用最大化最佳实践建议:• 敏感配置(如密码)避免使用动态刷新• 配合配置中心(Nacos/Config Server)使用• 生产环境务必保护刷新端点
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签