1. 微服务安全痛点:单点登录、权限隔离、接口防护

一、三大核心痛点深度解析

1. 单点登录(SSO)在微服务中的挑战

痛点 :

  • 令牌传播与验证 :用户登录后,身份令牌(如JWT)需要在多个服务间安全、高效传递。
  • 会话状态管理 :无状态 vs 有状态会话的权衡。
  • 跨域/跨应用登录 :多个前端应用(Web、移动端)共享登录状态。
  • 注销与令牌失效 :立即注销困难,令牌有效期管理复杂。

解决方案 :

  • 集中式认证服务 :独立认证服务(如Keycloak、OAuth2服务器)统一处理登录。
  • JWT作为无状态令牌 :包含用户身份和权限,减少会话存储依赖。
  • API网关作为认证入口 :在网关层统一验证令牌,减轻内部服务压力。
  • 短期访问令牌+长期刷新令牌 :平衡安全性与用户体验。
2. 权限隔离(细粒度授权)

痛点 :

  • 服务间权限差异 :不同服务需不同的数据权限和操作权限。
  • 动态权限管理 :用户角色、权限实时变更如何同步。
  • 数据级权限 :超越功能权限,控制到具体数据行/字段。
  • 权限边界模糊 :服务边界与权限边界不一致。

解决方案 :

  • RBAC/ABAC模型结合 :
  • RBAC(角色访问控制)管理基础功能权限
  • ABAC(属性访问控制)实现细粒度、上下文相关的权限
  • 策略决策点(PDP)与执行点(PEP)分离 :
  • 集中策略服务(如Open Policy Agent)统一决策
  • 各服务本地执行授权检查
  • 权限声明式配置 :在API定义中声明所需权限(如OpenAPI扩展)。
3. 接口防护

痛点 :

  • 服务间通信安全 :内部API暴露风险。
  • 过度数据暴露 :服务返回不必要的数据字段。
  • 暴力破解与滥用 :缺乏速率限制和攻击检测。
  • 依赖链安全 :下游服务漏洞影响上游。

解决方案 :

  • 零信任网络架构 :
  • mTLS双向认证所有服务间通信
  • 服务网格(如Istio)自动管理TLS证书
  • 深度防御策略 :
  • API网关层防护 :WAF、速率限制、基础校验
  • 服务层防护 :输入验证、输出过滤、查询限制
  • 数据层防护 :加密存储、脱敏处理
  • 全面的API安全策略 :
  • 严格的API契约(OpenAPI/Swagger)
  • 自动化的API安全测试(SAST/DAST)

二、一体化安全架构设计

推荐架构模式:安全即代码(Security as Code)
┌─────────────────────────────────────────────────────────────┐
│                   客户端 (Web/App/第三方)                    │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│                    API网关 (安全边界)                         │
│  ├─ TLS终止             ├─ 身份验证          ├─ 速率限制      │
│  └─ WAF防护             └─ 请求路由          └─ API计量       │
└───────────────────────────┬─────────────────────────────────┘
                            │
         ┌──────────────────┼──────────────────┐
         │                  │                  │
┌────────▼──────┐   ┌──────▼──────┐   ┌───────▼────────┐
│  认证服务      │   │ 策略决策服务 │   │  业务服务A     │
│ (OAuth2/OpenID)│   │  (OPA等)    │   │ ┌──────────┐  │
└────────────────┘   └─────────────┘   │ │本地PEP   │  │
         │                  │           │ └──────────┘  │
         └──────────────────┼───────────┘               │
                            │                           │
                    ┌───────▼────────────┐              │
                    │  服务网格          │              │
                    │  (mTLS/可观测性)   │◄─────────────┘
                    └────────────────────┘

三、常见陷阱与规避

陷阱

风险

规避策略

JWT令牌过大

增加网络开销,可能超出Cookie限制

仅存储必要声明,敏感数据通过引用ID获取

过度信任内部网络

内部攻击难以发现

实施零信任,所有通信需认证授权

权限配置错误

权限提升或过度授权

定期权限审计,最小权限原则

安全配置分散

各服务安全实现不一致

安全组件SDK化,统一维护


总结

微服务安全是一个系统工程,需要分层防御、纵深防护 。关键成功因素包括:

  1. 统一的安全控制平面 :通过API网关和服务网格集中管理
  2. 身份与权限解耦 :认证服务与业务服务分离
  3. 自动化安全策略 :策略即代码,便于审计和版本控制
  4. 持续的安全文化 :开发团队安全培训,左移安全测试

最终目标 不是追求绝对安全,而是在安全、用户体验和开发效率 之间找到适合业务的最佳平衡点。

2. OAuth2.0+JWT认证流程、令牌签发与校验

一、OAuth 2.0 授权码流程

1.1 流程概述
客户端 → 授权服务器 → 资源服务器
1. 授权请求 → 2. 用户认证 → 3. 授权码 → 4. 令牌请求 → 5. JWT令牌

二、JWT令牌结构

2.1 JWT组成
// Header.Payload.Signature
// 示例:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

三、Java实现代码

3.1 Maven依赖
<dependencies>
    <!-- Spring Security OAuth2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
    
    <!-- JWT支持 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>
3.2 JWT工具类
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtTokenProvider {
    
    @Value("${jwt.secret:your-secret-key-here-at-least-256-bits}")
    private String jwtSecret;
    
    @Value("${jwt.expiration:3600000}")
    private int jwtExpirationMs;
    
    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(jwtSecret.getBytes());
    }
    
    /**
     * 生成JWT令牌
     */
    public String generateToken(String username, Map<String, Object> claims) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(username)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }
    
    /**
     * 从令牌中获取用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }
    
    /**
     * 验证JWT令牌
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
    
    /**
     * 获取令牌中的声明信息
     */
    public Claims getClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}
3.3 OAuth2授权服务器配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;

import java.util.UUID;

@Configuration
public class AuthorizationServerConfig {
    
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.formLogin().and().build();
    }
    
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("client-id")
                .clientSecret("{noop}client-secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/client-id")
                .scope(OidcScopes.OPENID)
                .scope("read")
                .scope("write")
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();
        
        return new InMemoryRegisteredClientRepository(registeredClient);
    }
    
    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder()
                .issuer("http://auth-server:9000")
                .build();
    }
}
3.4 资源服务器配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.decoder(jwtDecoder()))
            );
        
        return http.build();
    }
    
    @Bean
    public JwtDecoder jwtDecoder() {
        String secret = "your-256-bit-secret-your-256-bit-secret-your-256-bit-secret";
        SecretKey key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
        return NimbusJwtDecoder.withSecretKey(key).build();
    }
}
3.5 自定义JWT令牌增强器
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class CustomJwtTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
    
    @Override
    public void customize(JwtEncodingContext context) {
        if (context.getTokenType().getValue().equals("access_token")) {
            Map<String, Object> customClaims = new HashMap<>();
            customClaims.put("organization", "MyOrg");
            customClaims.put("roles", context.getPrincipal().getAuthorities());
            
            context.getClaims().claims(claims -> 
                claims.putAll(customClaims)
            );
        }
    }
}
3.6 令牌校验端点
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;

@RestController
@RequestMapping("/api/auth")
public class TokenController {
    
    private final JwtTokenProvider jwtTokenProvider;
    
    public TokenController(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }
    
    /**
     * 验证令牌
     */
    @PostMapping("/validate")
    public ResponseEntity<?> validateToken(@RequestBody TokenValidationRequest request) {
        try {
            boolean isValid = jwtTokenProvider.validateToken(request.getToken());
            
            if (isValid) {
                String username = jwtTokenProvider.getUsernameFromToken(request.getToken());
                Map<String, Object> response = new HashMap<>();
                response.put("valid", true);
                response.put("username", username);
                response.put("claims", jwtTokenProvider.getClaimsFromToken(request.getToken()));
                return ResponseEntity.ok(response);
            } else {
                return ResponseEntity.badRequest().body("Invalid token");
            }
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("Token validation failed: " + e.getMessage());
        }
    }
    
    /**
     * 刷新令牌
     */
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
        // 验证刷新令牌逻辑
        // 生成新的访问令牌
        return ResponseEntity.ok().build();
    }
    
    // DTO类
    public static class TokenValidationRequest {
        private String token;
        
        // getters and setters
    }
    
    public static class RefreshTokenRequest {
        private String refreshToken;
        
        // getters and setters
    }
}
3.7 客户端实现示例
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.Base64;

public class OAuth2ClientExample {
    
    public String getAccessToken() {
        RestTemplate restTemplate = new RestTemplate();
        
        // 1. 获取授权码(浏览器重定向)
        // String authUrl = "http://auth-server:9000/oauth2/authorize?" +
        //     "response_type=code&" +
        //     "client_id=client-id&" +
        //     "redirect_uri=http://127.0.0.1:8080/callback&" +
        //     "scope=read write";
        
        // 2. 使用授权码获取访问令牌
        String tokenUrl = "http://auth-server:9000/oauth2/token";
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        
        // 基本认证头
        String auth = "client-id:client-secret";
        String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
        headers.set("Authorization", "Basic " + encodedAuth);
        
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("code", "授权码从回调URL获取");
        body.add("redirect_uri", "http://127.0.0.1:8080/callback");
        
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
        
        ResponseEntity<TokenResponse> response = restTemplate.postForEntity(
            tokenUrl, request, TokenResponse.class);
        
        return response.getBody().getAccessToken();
    }
    
    public String callProtectedResource(String accessToken) {
        RestTemplate restTemplate = new RestTemplate();
        
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        
        HttpEntity<String> entity = new HttpEntity<>(headers);
        
        ResponseEntity<String> response = restTemplate.exchange(
            "http://resource-server:8080/api/protected",
            HttpMethod.GET,
            entity,
            String.class
        );
        
        return response.getBody();
    }
    
    // DTO类
    public static class TokenResponse {
        private String access_token;
        private String token_type;
        private long expires_in;
        private String refresh_token;
        private String scope;
        
        // getters and setters
    }
}

四、令牌校验流程

4.1 校验步骤
// 1. 解析JWT
// 2. 验证签名
// 3. 检查过期时间
// 4. 验证颁发者
// 5. 验证受众
// 6. 检查自定义声明
4.2 全局异常处理
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<?> handleAuthenticationException(AuthenticationException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "Authentication failed", "message", e.getMessage()));
    }
    
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<?> handleAccessDeniedException(AccessDeniedException e) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(Map.of("error", "Access denied", "message", e.getMessage()));
    }
    
    @ExceptionHandler(JwtException.class)
    public ResponseEntity<?> handleJwtException(JwtException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "Invalid token", "message", e.getMessage()));
    }
}

五、安全最佳实践

5.1 配置建议
# application.yml
jwt:
  secret: ${JWT_SECRET:your-256-bit-secret-key-change-in-production}
  expiration: 3600000  # 1小时
  refresh-expiration: 2592000000  # 30天
  
security:
  oauth2:
    client:
      registration:
        my-client:
          client-id: client-id
          client-secret: client-secret
          scope: read,write
          authorization-grant-type: authorization_code
          redirect-uri: http://localhost:8080/login/oauth2/code/my-client
5.2 安全注意事项
  1. 密钥管理 :使用环境变量或密钥管理服务存储JWT密钥
  2. 令牌过期 :设置合理的过期时间(访问令牌1小时,刷新令牌30天)
  3. HTTPS :生产环境必须使用HTTPS
  4. 刷新令牌 :安全存储刷新令牌,使用一次后失效
  5. 范围限制 :为不同客户端分配最小必要权限

六、测试示例

6.1 单元测试
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
public class JwtTokenProviderTest {
    
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    
    @Test
    public void testGenerateAndValidateToken() {
        String username = "testuser";
        Map<String, Object> claims = new HashMap<>();
        claims.put("role", "USER");
        
        String token = jwtTokenProvider.generateToken(username, claims);
        
        assertNotNull(token);
        assertTrue(jwtTokenProvider.validateToken(token));
        assertEquals(username, jwtTokenProvider.getUsernameFromToken(token));
    }
}

这个实现提供了完整的OAuth 2.0 + JWT认证流程,包括令牌签发、校验和资源保护。可以根据具体需求进行调整和扩展。

3. 网关统一鉴权、接口权限拦截、角色权限控制

Spring Cloud Gateway 统一鉴权与权限控制方案

一、整体架构设计

客户端 → Spring Cloud Gateway → 鉴权过滤器 → 微服务
         ↓
   权限中心服务
         ↓
    用户/角色/权限数据

二、核心实现方案

1. 网关统一鉴权过滤器
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private PermissionService permissionService;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        
        // 1. 白名单校验
        if (isWhiteList(path)) {
            return chain.filter(exchange);
        }
        
        // 2. 获取token
        String token = getToken(request);
        if (StringUtils.isEmpty(token)) {
            return unauthorized(exchange, "未携带token");
        }
        
        // 3. 验证token有效性
        UserInfo userInfo = verifyToken(token);
        if (userInfo == null) {
            return unauthorized(exchange, "token无效或已过期");
        }
        
        // 4. 权限校验
        if (!hasPermission(userInfo, path, request.getMethod())) {
            return forbidden(exchange, "权限不足");
        }
        
        // 5. 传递用户信息到下游服务
        ServerHttpRequest newRequest = request.mutate()
                .header("X-User-Id", userInfo.getUserId())
                .header("X-User-Name", userInfo.getUsername())
                .header("X-Roles", String.join(",", userInfo.getRoles()))
                .build();
        
        return chain.filter(exchange.mutate().request(newRequest).build());
    }
    
    private boolean hasPermission(UserInfo userInfo, String path, HttpMethod method) {
        // 方案1: 从Redis缓存获取权限
        String key = "user:perms:" + userInfo.getUserId();
        Set<String> permissions = redisTemplate.opsForSet().members(key);
        
        if (permissions == null || permissions.isEmpty()) {
            // 方案2: 从数据库查询并缓存
            permissions = permissionService.getUserPermissions(userInfo.getUserId());
            redisTemplate.opsForSet().add(key, permissions.toArray(new String[0]));
            redisTemplate.expire(key, 30, TimeUnit.MINUTES);
        }
        
        // 权限标识格式: GET:/api/users/**
        String requiredPerm = method.name() + ":" + path;
        return permissions.contains(requiredPerm) || 
               checkPatternPermission(permissions, requiredPerm);
    }
    
    @Override
    public int getOrder() {
        return -100;
    }
}
2. 权限数据模型设计
-- 用户表
CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50) UNIQUE,
    password VARCHAR(100),
    status TINYINT DEFAULT 1
);

-- 角色表
CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY,
    code VARCHAR(50) UNIQUE,
    name VARCHAR(50)
);

-- 权限表(接口级别)
CREATE TABLE sys_permission (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    code VARCHAR(100) UNIQUE, -- 格式: METHOD:PATH
    path_pattern VARCHAR(200), -- 路径模式匹配
    method VARCHAR(10)
);

-- 用户角色关联
CREATE TABLE sys_user_role (
    user_id BIGINT,
    role_id BIGINT
);

-- 角色权限关联
CREATE TABLE sys_role_permission (
    role_id BIGINT,
    permission_id BIGINT
);
3. 动态权限加载服务
@Service
@Slf4j
public class PermissionServiceImpl implements PermissionService {
    
    @Autowired
    private PermissionMapper permissionMapper;
    
    /**
     * 获取用户所有权限(角色权限 + 特殊权限)
     */
    @Override
    public Set<String> getUserPermissions(Long userId) {
        Set<String> permissions = new HashSet<>();
        
        // 1. 获取用户角色
        List<Role> roles = permissionMapper.getUserRoles(userId);
        
        // 2. 获取角色权限
        for (Role role : roles) {
            List<Permission> rolePerms = permissionMapper.getRolePermissions(role.getId());
            rolePerms.forEach(p -> permissions.add(p.getCode()));
        }
        
        // 3. 获取用户特殊权限(如有单独授权)
        List<Permission> userPerms = permissionMapper.getUserPermissions(userId);
        userPerms.forEach(p -> permissions.add(p.getCode()));
        
        return permissions;
    }
    
    /**
     * 刷新权限缓存
     */
    @Override
    public void refreshUserPermissionCache(Long userId) {
        Set<String> permissions = getUserPermissions(userId);
        String key = "user:perms:" + userId;
        
        redisTemplate.delete(key);
        if (!permissions.isEmpty()) {
            redisTemplate.opsForSet().add(key, permissions.toArray(new String[0]));
            redisTemplate.expire(key, 30, TimeUnit.MINUTES);
        }
    }
}
4. RBAC角色权限控制
@Component
public class RoleBasedAccessControl {
    
    /**
     * 基于角色的访问控制
     */
    public boolean checkRoleAccess(UserInfo userInfo, String requiredRole) {
        Set<String> userRoles = userInfo.getRoles();
        
        // 1. 直接角色匹配
        if (userRoles.contains(requiredRole)) {
            return true;
        }
        
        // 2. 角色继承/层级检查
        return checkRoleHierarchy(userRoles, requiredRole);
    }
    
    /**
     * 检查角色层级(如:admin > manager > user)
     */
    private boolean checkRoleHierarchy(Set<String> userRoles, String requiredRole) {
        Map<String, Integer> roleHierarchy = new HashMap<>();
        roleHierarchy.put("super_admin", 100);
        roleHierarchy.put("admin", 90);
        roleHierarchy.put("manager", 80);
        roleHierarchy.put("user", 70);
        roleHierarchy.put("guest", 60);
        
        Integer requiredLevel = roleHierarchy.get(requiredRole);
        if (requiredLevel == null) return false;
        
        // 用户拥有的最高角色等级
        Integer userMaxLevel = userRoles.stream()
                .map(roleHierarchy::get)
                .filter(Objects::nonNull)
                .max(Integer::compare)
                .orElse(0);
        
        return userMaxLevel >= requiredLevel;
    }
}
5. 注解式权限控制(微服务内部)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize {
    String value(); // 权限表达式,如:hasRole('admin') 或 hasPermission('user:delete')
}

@Aspect
@Component
public class PermissionAspect {
    
    @Autowired
    private HttpServletRequest request;
    
    @Around("@annotation(preAuthorize)")
    public Object checkPermission(ProceedingJoinPoint joinPoint, 
                                  PreAuthorize preAuthorize) throws Throwable {
        String expression = preAuthorize.value();
        UserInfo userInfo = getUserInfoFromRequest();
        
        if (!evaluateExpression(expression, userInfo)) {
            throw new AccessDeniedException("权限不足");
        }
        
        return joinPoint.proceed();
    }
    
    private boolean evaluateExpression(String expression, UserInfo userInfo) {
        // 解析表达式,如:hasRole('admin') && hasPermission('user:delete')
        // 实现表达式解析逻辑
        return true;
    }
}

// 使用示例
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/{id}")
    @PreAuthorize("hasRole('user')")
    public User getUser(@PathVariable Long id) {
        // ...
    }
    
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('admin') && hasPermission('user:delete')")
    public void deleteUser(@PathVariable Long id) {
        // ...
    }
}
6. 配置类
# application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - name: AuthGlobalFilter
            - StripPrefix=1
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials

security:
  auth:
    white-list:
      - /api/auth/login
      - /api/auth/register
      - /swagger-ui/**
      - /v3/api-docs/**
    token:
      header: Authorization
      prefix: Bearer
      expire-time: 7200 # 2小时
7. 权限管理端点
@RestController
@RequestMapping("/admin/permissions")
@PreAuthorize("hasRole('admin')")
public class PermissionAdminController {
    
    @Autowired
    private PermissionService permissionService;
    
    /**
     * 为用户分配角色
     */
    @PostMapping("/user/{userId}/roles")
    public Result assignRolesToUser(@PathVariable Long userId, 
                                    @RequestBody List<Long> roleIds) {
        permissionService.assignRolesToUser(userId, roleIds);
        permissionService.refreshUserPermissionCache(userId);
        return Result.success();
    }
    
    /**
     * 刷新用户权限缓存
     */
    @PostMapping("/cache/refresh/{userId}")
    public Result refreshCache(@PathVariable Long userId) {
        permissionService.refreshUserPermissionCache(userId);
        return Result.success();
    }
    
    /**
     * 同步接口权限(从注解扫描)
     */
    @PostMapping("/sync")
    public Result syncPermissions() {
        List<Permission> permissions = scanAnnotationsForPermissions();
        permissionService.syncPermissions(permissions);
        return Result.success();
    }
}

三、扩展功能

1. 接口权限自动扫描
@Component
public class PermissionScanner {
    
    public List<Permission> scanControllerPermissions() {
        List<Permission> permissions = new ArrayList<>();
        
        // 扫描所有@RestController注解的类
        Map<String, Object> controllers = applicationContext
                .getBeansWithAnnotation(RestController.class);
        
        for (Object controller : controllers.values()) {
            Class<?> clazz = controller.getClass();
            RequestMapping classMapping = clazz.getAnnotation(RequestMapping.class);
            String basePath = classMapping != null ? classMapping.value()[0] : "";
            
            // 扫描方法
            for (Method method : clazz.getDeclaredMethods()) {
                PreAuthorize preAuth = method.getAnnotation(PreAuthorize.class);
                if (preAuth != null) {
                    // 获取请求映射
                    GetMapping getMapping = method.getAnnotation(GetMapping.class);
                    PostMapping postMapping = method.getAnnotation(PostMapping.class);
                    // ... 其他HTTP方法
                    
                    // 构建权限信息
                    Permission permission = new Permission();
                    permission.setCode(buildPermissionCode(method, basePath));
                    permission.setName(extractPermissionName(preAuth));
                    permissions.add(permission);
                }
            }
        }
        
        return permissions;
    }
}
2. 分布式权限缓存同步
@Component
public class PermissionCacheSync {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 使用Redis Pub/Sub同步权限变更
     */
    @EventListener
    public void onPermissionChange(PermissionChangeEvent event) {
        String channel = "permission:change";
        redisTemplate.convertAndSend(channel, event.getUserId());
    }
    
    @RedisListener(channel = "permission:change")
    public void handlePermissionChange(Long userId) {
        // 刷新本地缓存或通知网关
        permissionService.refreshUserPermissionCache(userId);
    }
}

四、最佳实践建议

  1. 性能优化
  1. 使用Redis缓存权限数据,设置合理过期时间
  2. 网关层权限校验使用布隆过滤器快速判断无权限
  3. 权限路径使用Ant风格匹配,支持通配符
  1. 安全性建议
  1. 权限变更立即生效(清除缓存)
  2. 敏感操作增加二次验证
  3. 定期审计权限分配情况
  1. 监控告警
  1. 记录权限验证失败日志
  2. 监控接口访问频率
  3. 设置异常访问告警

这个方案提供了从网关统一鉴权到微服务内部细粒度控制的完整解决方案,支持动态权限管理和RBAC模型,可根据实际需求进行调整和扩展。

4. 令牌刷新、过期处理、黑名单管控

整体架构设计

1.1 技术栈选择

# 建议的技术栈
- Spring Security + OAuth2
- JWT (JSON Web Token)
- Redis (存储黑名单和令牌信息)
- Spring Cloud Gateway (网关层拦截)

1.2 令牌生命周期管理

用户登录 → 颁发AccessToken + RefreshToken
    ↓
AccessToken过期 (30分钟)
    ↓
使用RefreshToken刷新 → 颁发新的Token对
    ↓
用户登出 → Token加入黑名单

JWT令牌刷新实现

2.1 自定义Token服务

@Component
public class JwtTokenService {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.access-token-expire}")
    private Long accessTokenExpire;
    
    @Value("${jwt.refresh-token-expire}")
    private Long refreshTokenExpire;
    
    /**
     * 生成访问令牌
     */
    public String generateAccessToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", userDetails.getUsername());
        claims.put("roles", userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()));
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpire))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
    
    /**
     * 生成刷新令牌
     */
    public String generateRefreshToken(UserDetails userDetails) {
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpire))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
    
    /**
     * 刷新令牌
     */
    public TokenResponse refreshToken(String refreshToken) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(refreshToken)
                    .getBody();
            
            String username = claims.getSubject();
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            // 验证刷新令牌是否有效(可加入黑名单检查)
            if (isTokenBlacklisted(refreshToken)) {
                throw new TokenRefreshException("刷新令牌已失效");
            }
            
            return TokenResponse.builder()
                    .accessToken(generateAccessToken(userDetails))
                    .refreshToken(generateRefreshToken(userDetails))
                    .expiresIn(accessTokenExpire / 1000)
                    .build();
        } catch (Exception e) {
            throw new TokenRefreshException("刷新令牌无效");
        }
    }
}

2.2 刷新令牌端点

@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Autowired
    private JwtTokenService tokenService;
    
    @Autowired
    private RedisTokenBlacklistService blacklistService;
    
    /**
     * 刷新令牌接口
     */
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) {
        String refreshToken = request.getRefreshToken();
        
        // 验证刷新令牌
        TokenResponse tokenResponse = tokenService.refreshToken(refreshToken);
        
        // 将旧的刷新令牌加入黑名单
        blacklistService.addToBlacklist(refreshToken, 
            tokenService.getExpirationFromToken(refreshToken));
        
        return ResponseEntity.ok(tokenResponse);
    }
    
    /**
     * 登出接口
     */
    @PostMapping("/logout")
    public ResponseEntity<?> logout(HttpServletRequest request) {
        String token = extractToken(request);
        if (token != null) {
            // 将令牌加入黑名单
            Date expiration = tokenService.getExpirationFromToken(token);
            blacklistService.addToBlacklist(token, expiration);
        }
        return ResponseEntity.ok("登出成功");
    }
    
    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

令牌过期处理

3.1 自定义认证过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenService tokenService;
    
    @Autowired
    private RedisTokenBlacklistService blacklistService;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) throws ServletException, IOException {
        
        try {
            String token = extractToken(request);
            
            if (token != null) {
                // 1. 检查令牌是否在黑名单中
                if (blacklistService.isTokenBlacklisted(token)) {
                    throw new TokenBlacklistedException("令牌已失效");
                }
                
                // 2. 验证令牌有效性
                if (tokenService.validateToken(token)) {
                    String username = tokenService.getUsernameFromToken(token);
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                    
                    UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails, 
                            null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource()
                        .buildDetails(request));
                    
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        } catch (ExpiredJwtException e) {
            // 令牌过期处理
            handleTokenExpired(request, response, e);
            return;
        } catch (TokenBlacklistedException e) {
            // 黑名单令牌处理
            handleBlacklistedToken(response, e);
            return;
        } catch (Exception e) {
            // 其他异常处理
            logger.error("无法验证用户身份", e);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private void handleTokenExpired(HttpServletRequest request, 
                                   HttpServletResponse response,
                                   ExpiredJwtException e) throws IOException {
        
        // 检查是否为刷新令牌请求
        if (request.getRequestURI().equals("/auth/refresh")) {
            filterChain.doFilter(request, response);
            return;
        }
        
        // 返回特定的过期错误码
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        
        Map<String, Object> errorDetails = new HashMap<>();
        errorDetails.put("code", "TOKEN_EXPIRED");
        errorDetails.put("message", "访问令牌已过期");
        errorDetails.put("timestamp", System.currentTimeMillis());
        
        response.getWriter().write(new ObjectMapper().writeValueAsString(errorDetails));
    }
}

3.2 全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ExpiredJwtException.class)
    public ResponseEntity<ErrorResponse> handleExpiredJwtException(ExpiredJwtException e) {
        ErrorResponse error = ErrorResponse.builder()
                .code("TOKEN_EXPIRED")
                .message("令牌已过期,请重新登录或刷新令牌")
                .timestamp(new Date())
                .build();
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
    }
    
    @ExceptionHandler(TokenBlacklistedException.class)
    public ResponseEntity<ErrorResponse> handleTokenBlacklistedException(
            TokenBlacklistedException e) {
        ErrorResponse error = ErrorResponse.builder()
                .code("TOKEN_BLACKLISTED")
                .message("令牌已失效,请重新登录")
                .timestamp(new Date())
                .build();
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
    }
}

黑名单管控实现

4.1 Redis黑名单服务

@Service
public class RedisTokenBlacklistService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String BLACKLIST_KEY_PREFIX = "blacklist:token:";
    
    /**
     * 添加令牌到黑名单
     */
    public void addToBlacklist(String token, Date expiration) {
        String key = BLACKLIST_KEY_PREFIX + token;
        long ttl = expiration.getTime() - System.currentTimeMillis();
        
        if (ttl > 0) {
            redisTemplate.opsForValue().set(key, "blacklisted", ttl, TimeUnit.MILLISECONDS);
        }
    }
    
    /**
     * 检查令牌是否在黑名单中
     */
    public boolean isTokenBlacklisted(String token) {
        String key = BLACKLIST_KEY_PREFIX + token;
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
    
    /**
     * 批量添加令牌到黑名单(用于批量登出)
     */
    public void batchAddToBlacklist(List<String> tokens, Date expiration) {
        long ttl = expiration.getTime() - System.currentTimeMillis();
        
        if (ttl > 0) {
            tokens.forEach(token -> {
                String key = BLACKLIST_KEY_PREFIX + token;
                redisTemplate.opsForValue().set(key, "blacklisted", ttl, TimeUnit.MILLISECONDS);
            });
        }
    }
    
    /**
     * 清理过期的黑名单条目
     */
    public void cleanupExpiredBlacklist() {
        // Redis会自动清理过期的key
    }
}

4.2 增强版黑名单管理

@Service
public class EnhancedTokenBlacklistService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private JwtTokenService jwtTokenService;
    
    /**
     * 添加令牌到黑名单,并记录原因
     */
    public void addToBlacklistWithReason(String token, String reason, String operator) {
        try {
            Claims claims = jwtTokenService.getClaimsFromToken(token);
            String username = claims.getSubject();
            Date expiration = claims.getExpiration();
            
            BlacklistEntry entry = BlacklistEntry.builder()
                    .token(token)
                    .username(username)
                    .reason(reason)
                    .operator(operator)
                    .blacklistedAt(new Date())
                    .expiresAt(expiration)
                    .build();
            
            String key = "blacklist:token:" + token;
            String userKey = "blacklist:user:" + username;
            
            // 存储令牌黑名单信息
            redisTemplate.opsForValue().set(key, entry, 
                expiration.getTime() - System.currentTimeMillis(), 
                TimeUnit.MILLISECONDS);
            
            // 用户维度的黑名单记录
            redisTemplate.opsForSet().add(userKey, token);
            redisTemplate.expire(userKey, 7, TimeUnit.DAYS);
            
            // 发布黑名单事件
            publishBlacklistEvent(entry);
            
        } catch (Exception e) {
            throw new BlacklistOperationException("添加黑名单失败", e);
        }
    }
    
    /**
     * 获取用户的所有黑名单令牌
     */
    public List<BlacklistEntry> getUserBlacklistedTokens(String username) {
        String userKey = "blacklist:user:" + username;
        Set<Object> tokens = redisTemplate.opsForSet().members(userKey);
        
        return tokens.stream()
                .map(token -> (BlacklistEntry) redisTemplate.opsForValue()
                    .get("blacklist:token:" + token))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
    
    /**
     * 强制用户所有令牌失效
     */
    public void invalidateAllUserTokens(String username, String reason) {
        // 获取用户当前所有有效令牌(需要业务系统记录)
        List<String> userTokens = getUserActiveTokens(username);
        
        userTokens.forEach(token -> {
            try {
                Date expiration = jwtTokenService.getExpirationFromToken(token);
                addToBlacklistWithReason(token, reason, "system");
            } catch (Exception e) {
                logger.warn("令牌失效失败: {}", token);
            }
        });
    }
    
    private void publishBlacklistEvent(BlacklistEntry entry) {
        // 发布事件,其他服务可以监听并处理
        applicationEventPublisher.publishEvent(
            new TokenBlacklistedEvent(this, entry));
    }
}

Spring Cloud Gateway集成

5.1 网关全局过滤器

@Component
public class TokenValidationFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private RedisTokenBlacklistService blacklistService;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();
        
        // 跳过认证的路径
        if (isSkipAuthPath(path)) {
            return chain.filter(exchange);
        }
        
        // 获取令牌
        String token = extractToken(request);
        
        if (token == null) {
            return unauthorizedResponse(exchange, "缺少访问令牌");
        }
        
        // 检查黑名单
        if (blacklistService.isTokenBlacklisted(token)) {
            return unauthorizedResponse(exchange, "令牌已失效");
        }
        
        // 验证令牌有效性(可调用认证服务)
        if (!validateToken(token)) {
            return unauthorizedResponse(exchange, "令牌无效");
        }
        
        // 添加用户信息到请求头
        ServerHttpRequest mutatedRequest = request.mutate()
                .header("X-User-Id", extractUserId(token))
                .header("X-User-Roles", extractUserRoles(token))
                .build();
        
        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }
    
    private boolean isSkipAuthPath(String path) {
        return path.startsWith("/auth/login") 
                || path.startsWith("/auth/refresh")
                || path.startsWith("/public/");
    }
    
    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String message) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        
        Map<String, Object> error = new HashMap<>();
        error.put("code", "UNAUTHORIZED");
        error.put("message", message);
        error.put("timestamp", System.currentTimeMillis());
        
        DataBuffer buffer = response.bufferFactory()
                .wrap(JSON.toJSONBytes(error));
        return response.writeWith(Mono.just(buffer));
    }
    
    @Override
    public int getOrder() {
        return -100;
    }
}

配置示例

6.1 application.yml配置

# JWT配置
jwt:
  secret: ${JWT_SECRET:your-256-bit-secret-key-here}
  access-token-expire: 1800000  # 30分钟
  refresh-token-expire: 2592000000  # 30天
  issuer: your-app-name

# Redis配置
spring:
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
    database: 0
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

# 安全配置
security:
  ignored:
    - /auth/login
    - /auth/refresh
    - /public/**
  blacklist:
    cleanup-cron: "0 0 2 * * ?"  # 每天凌晨2点清理

高级特性

7.1 分布式会话管理

@Service
public class DistributedSessionService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 存储用户会话信息
     */
    public void storeUserSession(String userId, UserSession session) {
        String key = "session:user:" + userId;
        String tokenKey = "session:token:" + session.getAccessToken();
        
        // 存储用户维度会话
        redisTemplate.opsForHash().put(key, session.getAccessToken(), session);
        redisTemplate.expire(key, 7, TimeUnit.DAYS);
        
        // 存储令牌维度会话
        redisTemplate.opsForValue().set(tokenKey, session, 
            session.getExpiresIn(), TimeUnit.SECONDS);
    }
    
    /**
     * 获取用户所有活跃会话
     */
    public List<UserSession> getUserActiveSessions(String userId) {
        String key = "session:user:" + userId;
        Map<Object, Object> sessions = redisTemplate.opsForHash().entries(key);
        
        return sessions.values().stream()
                .map(obj -> (UserSession) obj)
                .filter(session -> session.getExpiresAt().after(new Date()))
                .collect(Collectors.toList());
    }
    
    /**
     * 踢出用户其他设备
     */
    public void kickOtherDevices(String userId, String currentToken) {
        List<UserSession> sessions = getUserActiveSessions(userId);
        
        sessions.stream()
                .filter(session -> !session.getAccessToken().equals(currentToken))
                .forEach(session -> {
                    // 将令牌加入黑名单
                    blacklistService.addToBlacklist(session.getAccessToken(), 
                        session.getExpiresAt());
                    // 从会话中移除
                    removeSession(userId, session.getAccessToken());
                });
    }
}

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐