第 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 # Signature3.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 |
| 零信任 | 永不信任 |
动手练习
- 用 Spring Security + JWT 实现登录接口
- 在 Gateway 实现统一鉴权 Filter
- 用 BCrypt 加密密码入库
- 用 Feign 拦截器透传 token
- 加 RBAC 注解,做接口权限控制