Skip to content
第 230 / 250 章架构⏱ 12 分钟阅读

第 230 章:微服务安全

学习目标

  • 理解 OAuth2 与 JWT
  • 掌握 Spring Security 集成
  • 学会网关统一鉴权
  • 零信任安全模型

一、安全威胁全景

威胁说明
SQL 注入拼接 SQL,执行恶意代码
XSS在页面注入 JS
CSRF利用用户身份发起请求
越权访问A 访问 B 的资源
敏感信息泄露日志/响应包含密码/token
DDoS大量请求打挂服务

二、认证 vs 授权

概念含义解决
认证(Authentication)你是谁?账号 + 密码
授权(Authorization)你能做什么?RBAC / ACL

三、JWT 简介

JWT 是基于 JSON 的轻量级令牌。

3.1 结构

eyJhbGciOiJIUzI1NiJ9.                  # Header (base64)
eyJzdWIiOiIxMjMiLCJuYW1lIjoiVG9tIn0.   # Payload (base64)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  # Signature

3.2 Payload 示例

json
{
  "sub": "userId:123",
  "name": "Tom",
  "roles": ["admin", "user"],
  "exp": 1691896200,
  "iat": 1691809800,
  "iss": "auth-service"
}

3.3 优势

  • 无状态(无需 session)
  • 自包含(信息在 Token 中)
  • 跨语言
  • 适合分布式

3.4 注意事项

  • 不可撤销:签发后无法作废 → 通过短期 + Refresh Token
  • 不存敏感信息:Payload 只是 base64,不算加密

四、OAuth2 流程

4.1 角色

角色职责
Resource Owner用户
Client应用
Authorization Server授权服务器(签发 token)
Resource Server资源服务器(业务 API)

4.2 流程(授权码模式)

4.3 模式对比

模式适用
授权码第三方 Web 应用
密码自己的应用(已过时)
客户端凭证服务间调用
隐式SPA / 移动

五、Spring Security 集成

5.1 依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>

5.2 Spring Boot 3.x

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }
}

5.3 JWT 工具类

java
@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expire:86400000}")
    private long expireMs;

    public String generateToken(Long userId, String username, List<String> roles) {
        return Jwts.builder()
            .setSubject(String.valueOf(userId))
            .claim("username", username)
            .claim("roles", roles)
            .setExpiration(new Date(System.currentTimeMillis() + expireMs))
            .setIssuedAt(new Date())
            .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
            .compact();
    }

    public Claims parse(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(secret.getBytes())
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
}

5.4 JWT 过滤器

java
@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain)
            throws ServletException, IOException {
        String token = extract(req);
        if (token != null) {
            try {
                Claims claims = jwtUtil.parse(token);
                String userId = claims.getSubject();
                String username = (String) claims.get("username");
                List<String> roles = (List<String>) claims.get("roles");

                List<SimpleGrantedAuthority> authorities = roles.stream()
                    .map(r -> new SimpleGrantedAuthority("ROLE_" + r))
                    .collect(Collectors.toList());

                // 存储到 SecurityContext
                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(userId, username, authorities);
                SecurityContextHolder.getContext().setAuthentication(auth);

                // 透传给下游
                UserContextHolder.setUserId(userId);
            } catch (JwtException e) {
                resp.setStatus(HttpStatus.UNAUTHORIZED.value());
                resp.getWriter().write("Invalid token");
                return;
            }
        }
        chain.doFilter(req, resp);
    }

    private String extract(HttpServletRequest req) {
        String header = req.getHeader("Authorization");
        return (header != null && header.startsWith("Bearer ")) ? header.substring(7) : null;
    }
}

5.5 登录接口

java
@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private UserService userService;
    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public Result<TokenDTO> login(@RequestBody LoginRequest req) {
        User user = userService.findByUsername(req.getUsername());
        if (user == null || !BCrypt.checkpw(req.getPassword(), user.getPassword())) {
            return Result.error(401, "用户名或密码错误");
        }

        String token = jwtUtil.generateToken(
            user.getId(),
            user.getUsername(),
            user.getRoles()
        );

        return Result.ok(new TokenDTO(token, 86400000));
    }

    @PostMapping("/refresh")
    public Result<TokenDTO> refresh(@RequestHeader("Authorization") String authHeader) {
        // 验证旧 token,签发新 token
        // ...
    }
}

六、网关统一鉴权

6.1 Gateway Filter

java
@Component
public class AuthGatewayFilter implements GlobalFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest req = exchange.getRequest();
        String path = req.getURI().getPath();

        // 白名单
        if (isWhiteList(path)) {
            return chain.filter(exchange);
        }

        String token = req.getHeaders().getFirst("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            return unauthorized(exchange, "Missing token");
        }

        try {
            Claims claims = jwtUtil.parse(token.substring(7));
            String userId = claims.getSubject();

            // 透传用户信息
            ServerHttpRequest mutated = req.mutate()
                .header("X-User-Id", userId)
                .header("X-User-Roles", String.join(",", (List<String>) claims.get("roles")))
                .build();

            return chain.filter(exchange.mutate().request(mutated).build());
        } catch (JwtException e) {
            return unauthorized(exchange, "Invalid token");
        }
    }

    private boolean isWhiteList(String path) {
        return path.startsWith("/api/auth/")
            || path.startsWith("/api/public/");
    }
}

6.2 下游服务获取用户头

java
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/list")
    public List<Order> list(@RequestHeader("X-User-Id") String userId) {
        return orderService.findByUser(userId);
    }
}

七、服务间鉴权

7.1 Client Credentials 模式

服务 A 调用服务 B:

java
@FeignClient(name = "inventory-service")
public interface InventoryClient {

    // 自动加 token
    @GetMapping("/api/inventory/{skuId}")
    Result<InventoryDTO> getStock(@PathVariable String skuId);
}

拦截器自动添加:

java
@Component
public class FeignAuthInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 透传用户 token
        ServletRequestAttributes attrs =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attrs != null) {
            String token = attrs.getRequest().getHeader("Authorization");
            if (token != null) {
                template.header("Authorization", token);
            }
        }
    }
}

7.2 mTLS(双向认证)

yaml
server:
  ssl:
    enabled: true
    key-store: classpath:client.p12
    key-store-password: changeit
    keyStoreType: PKCS12
    keyAlias: client
    trust-store: classpath:trust.jks
    trust-store-password: changeit

零信任:每跳独立校验。

八、RBAC 权限模型

8.1 数据库设计

sql
CREATE TABLE user (id, username, password);

CREATE TABLE role (id, name);

CREATE TABLE user_role (user_id, role_id);

CREATE TABLE permission (id, name, code);

CREATE TABLE role_permission (role_id, perm_id);

8.2 Shiro + JWT 注解

java
@RequiresRoles("admin")
@RequiresPermissions("order:delete")
public void deleteOrder(Long id) { ... }

九、API 安全防护

9.1 限流(防 DDOS)

见上一章 Sentinel。

9.2 防 SQL 注入

java
// ❌ 拼接
@Query("SELECT u FROM User u WHERE u.name = '" + name + "'")

// ✅ 参数化
@Query("SELECT u FROM User u WHERE u.name = :name")
@Param("name")

9.3 防 XSS

html
<!-- Vue 模板默认转义 -->
<div>{{ userInput }}</div>   ✅ 自动转义
<div v-html="userInput"></div> ❌ 危险
java
// Spring HTML 过滤工具
String clean = HtmlUtils.htmlEscape(dirty);

9.4 防 CSRF

java
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 纯 API 禁用 CSRF
        http.csrf(csrf -> csrf.disable());
    }
}

或用 token 校验替代 Cookie。

9.5 密钥管理

yaml
# ❌ 不写在配置
jwt.secret: mysecretkey

# ✅ 通过环境变量或 Vault
jwt.secret: ${JWT_SECRET}

十、零信任架构

10.1 原则

永不信任,永远验证

10.2 关键能力

能力实现
强身份认证MFA、WebAuthn
动态授权ABAC(基于属性)
最小权限Just-in-time 授权
持续验证token 频繁签发
加密传输mTLS
审计所有访问有日志

10.3 Spring Security 6 + OAuth2 Resource Server

java
@Configuration
@EnableWebSecurity
public class ZeroTrustConfig {

    @Bean
    public SecurityFilterChain filter(HttpSecurity http) throws Exception {
        // 多条件授权
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                .requestMatchers("/api/**").hasAuthority("SCOPE_user")
                .anyRequest().denyAll()    // 默认拒绝
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .build();
    }
}

十一、安全审计

11.1 AOP 记录操作日志

java
@Aspect
@Component
public class AuditLogAspect {

    @Autowired
    private AuditLogRepository repo;

    @After("@annotation(Audit)")
    public void audit(JoinPoint jp) {
        MethodSignature sig = (MethodSignature) jp.getSignature();
        Audit annotation = sig.getMethod().getAnnotation(Audit.class);

        AuditLog log = new AuditLog();
        log.setAction(annotation.action());
        log.setUserId(UserContextHolder.getUserId());
        log.setIp(ServletUtil.getClientIP());
        log.setParams(JSON.toJSONString(jp.getArgs()));
        repo.save(log);
    }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit {
    String action();
}

@Audit(action = "DELETE_ORDER")
public void delete(Long id) { ... }

11.2 SIEM 集成

将审计日志接 ELK / Splunk / 阿里云日志服务,做异常检测。

十二、本章小结

主题要点
认证JWT、OAuth2
授权RBAC、ABAC
网关统一鉴权GlobalFilter
服务间JWT 透传 / mTLS
防护SQL 注入、XSS、CSRF
零信任永不信任

动手练习

  1. 用 Spring Security + JWT 实现登录接口
  2. 在 Gateway 实现统一鉴权 Filter
  3. 用 BCrypt 加密密码入库
  4. 用 Feign 拦截器透传 token
  5. 加 RBAC 注解,做接口权限控制

推荐阅读


下一章:第 231 章:综合实战(电商系统设计)

本站基于 VitePress 构建 · 由 Codebook 团队维护