安全是微服务架构的基石。Spring Security 提供了一套可扩展的认证(Authentication)与授权(Authorization)框架,结合 OAuth2 协议与 JWT 令牌,可实现分布式系统的无状态安全体系。本文从基础配置到高阶场景,构建完整的 Java 安全方案。
1. Spring Security 核心架构
1.1 过滤器链
请求 → SecurityContextPersistenceFilter → LogoutFilter
→ UsernamePasswordAuthenticationFilter → ...
→ FilterSecurityInterceptor → 目标资源
核心概念:
- Authentication:封装用户身份信息( principal + credentials + authorities )
- SecurityContext:线程级安全上下文,存储当前 Authentication
- UserDetailsService:加载用户数据的接口,需对接实际用户系统
- AccessDecisionManager:投票决定访问是否通过
1.2 最小安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 无状态 API 关闭 CSRF
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers("/api/v1/orders/**").authenticated()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
2. JWT 无状态认证
2.1 JWT 结构与签名
Header.Payload.Signature
Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "123", "roles": ["USER"], "iat": 1690000000, "exp": 1690003600 }
Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Payload 声明:
| 声明 | 含义 |
|---|---|
sub | 主题(用户 ID) |
iss | 签发者 |
aud | 接收者 |
iat | 签发时间 |
exp | 过期时间 |
jti | 唯一标识(用于令牌黑名单) |
2.2 JWT 工具类
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration:86400000}") // 默认 24 小时
private long jwtExpirationMs;
private SecretKey key;
@PostConstruct
public void init() {
this.key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList());
return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(key)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().verifyWith(key).build().parseSignedClaims(token);
return !isTokenBlacklisted(token);
} catch (JwtException | IllegalArgumentException e) {
log.warn("Invalid JWT: {}", e.getMessage());
return false;
}
}
public String getUsernameFromToken(String token) {
return Jwts.parser().verifyWith(key).build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
private boolean isTokenBlacklisted(String token) {
// 登出时加入 Redis Set,TTL 设为 token 剩余有效期
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember("jwt:blacklist", token));
}
}
2.3 JWT 认证过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
log.error("Cannot set user authentication", e);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
2.4 刷新令牌机制
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refreshToken(
@RequestBody RefreshTokenRequest request) {
String refreshToken = request.getRefreshToken();
if (!refreshTokenProvider.validate(refreshToken)) {
throw new InvalidTokenException("Refresh token expired");
}
String username = refreshTokenProvider.getUsername(refreshToken);
UserDetails user = userDetailsService.loadUserByUsername(username);
// 刷新令牌使用一次即删除(轮换机制)
refreshTokenProvider.delete(refreshToken);
String newAccess = jwtTokenProvider.generateToken(user);
String newRefresh = refreshTokenProvider.createToken(user);
return ResponseEntity.ok(new TokenResponse(newAccess, newRefresh));
}
}
| 令牌类型 | 有效期 | 用途 | 存储 |
|---|---|---|---|
| Access Token | 15-60 分钟 | API 鉴权 | 内存/Header |
| Refresh Token | 7-30 天 | 换取新 Access Token | HttpOnly Cookie |
3. OAuth2 授权服务器
3.1 四种授权模式
| 模式 | 场景 | 安全性 |
|---|---|---|
| 授权码模式 | Web 应用、SPA 授权 | 最高 |
| 简化模式 | 纯前端应用(已废弃,推荐 PKCE) | 低 |
| 密码凭证 | 第一方应用(官方 App) | 中 |
| 客户端凭证 | 服务端到服务端 | 中 |
3.2 Spring Authorization Server(OAuth2 新标准)
Spring Security OAuth 项目已停止维护,新项目使用 Spring Authorization Server。
@Configuration
public class AuthorizationServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("web-client")
.clientSecret("{bcrypt}$2a$10$...")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://localhost:8080/login/oauth2/code/web-client")
.scope(OidcScopes.OPENID)
.scope("read")
.scope("write")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true) // 显式授权确认页
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30))
.refreshTokenTimeToLive(Duration.ofDays(7))
.build())
.build();
return new InMemoryRegisteredClientRepository(client);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey((RSAPrivateKey) keyPair.getPrivate())
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, context) -> jwkSelector.select(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
}
3.3 OAuth2 资源服务器
@Configuration
@EnableMethodSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain resourceFilterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter =
new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthoritiesClaimName("roles");
authoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}
3.4 社交登录(Google/GitHub)
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid,profile,email
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: read:user,user:email
4. RBAC 权限模型
4.1 数据模型
-- 五表结构:用户-角色-权限 + 用户角色关联 + 角色权限关联
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
enabled BOOLEAN DEFAULT true
);
CREATE TABLE roles (
id BIGINT PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL, -- ROLE_ADMIN, ROLE_USER
description VARCHAR(255)
);
CREATE TABLE permissions (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL, -- user:create, order:delete
resource VARCHAR(50) NOT NULL,
action VARCHAR(20) NOT NULL -- CREATE, READ, UPDATE, DELETE
);
CREATE TABLE user_roles (
user_id BIGINT REFERENCES users(id),
role_id BIGINT REFERENCES roles(id),
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE role_permissions (
role_id BIGINT REFERENCES roles(id),
permission_id BIGINT REFERENCES permissions(id),
PRIMARY KEY (role_id, permission_id)
);
4.2 方法级安全注解
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN') or hasRole('ORDER_MANAGER')")
public List<OrderDTO> listAllOrders() { ... }
@PreAuthorize("hasPermission(#orderId, 'Order', 'READ')")
public OrderDTO getOrder(Long orderId) { ... }
@PreAuthorize("@orderSecurity.isOwner(#orderId, authentication.name)")
public void cancelOrder(Long orderId) { ... }
@PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
public OrderDTO getOrderSecure(Long orderId) { ... }
@Secured({"ROLE_ADMIN"}) // 旧式注解
public void deleteUser(Long userId) { ... }
}
4.3 基于 SpEL 的自定义权限评估
@Component("orderSecurity")
public class OrderSecurity {
@Autowired
private OrderRepository orderRepository;
public boolean isOwner(Long orderId, String username) {
return orderRepository.findById(orderId)
.map(order -> order.getBuyerName().equals(username))
.orElse(false);
}
}
启用方法安全:
@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // @PreAuthorize / @PostAuthorize
securedEnabled = true, // @Secured
jsr250Enabled = true // @RolesAllowed
)
public class MethodSecurityConfig { }
5. 安全加固清单
5.1 HTTPS 强制
@Configuration
public class HttpsConfig {
@Bean
public SecurityFilterChain httpsFilterChain(HttpSecurity http) throws Exception {
http
.requiresChannel(channel -> channel
.anyRequest().requiresSecure()) // 强制 HTTPS
.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
)
);
return http.build();
}
}
5.2 安全头部
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny()) // 防止点击劫持
.xssProtection(xss -> xss.disable()) // 现代浏览器用 CSP 替代
.contentTypeOptions(cto -> cto.disable()) // X-Content-Type-Options: nosniff
.referrerPolicy(referrer ->
referrer.policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
);
5.3 密码安全
// 密码强度校验
public class PasswordValidator {
private static final Pattern PATTERN = Pattern.compile(
"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{12,}$"
);
public boolean isValid(String password) {
return PATTERN.matcher(password).matches();
}
}
// 密码哈希迭代(Spring Security 5+ 默认使用 DelegatingPasswordEncoder)
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
// 自动识别前缀:{bcrypt}, {pbkdf2}, {scrypt}, {argon2}
}
5.4 登录安全
@Component
public class LoginAttemptService {
private final LoadingCache<String, Integer> attempts;
public LoginAttemptService() {
attempts = CacheBuilder.newBuilder()
.expireAfterWrite(15, TimeUnit.MINUTES)
.build(CacheLoader.from(() -> 0));
}
public void loginSucceeded(String key) {
attempts.invalidate(key);
}
public void loginFailed(String key) {
int attempts = this.attempts.getUnchecked(key);
this.attempts.put(key, attempts + 1);
}
public boolean isBlocked(String key) {
return attempts.getUnchecked(key) >= 5;
}
}
6. 多因素认证(MFA)
@Service
public class MfaService {
// TOTP 基于时间的一次性密码(Google Authenticator 兼容)
public String generateSecret() {
return new DefaultSecretGenerator().generate();
}
public String generateQrUrl(String username, String secret, String issuer) {
return new QrGenerator().generate(
username, secret, issuer,
BarcodeImageType.PNG, 200, 200);
}
public boolean verifyCode(String secret, String code) {
return new TimeProvider().getTime() / 30 ==
Integer.parseInt(code) / 100000; // 简化示意,实际使用 Totp 验证器
}
}
7. API 安全审计与日志
@Component
public class SecurityAuditListener {
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
log.info("[AUTH_SUCCESS] user={}, source={}, time={}",
event.getAuthentication().getName(),
getClientIp(),
LocalDateTime.now());
}
@EventListener
public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
log.warn("[AUTH_FAILURE] user={}, reason={}, ip={}",
event.getAuthentication().getName(),
event.getException().getMessage(),
getClientIp());
}
@EventListener
public void onAuthorizationDenied(AuthorizationDeniedEvent event) {
log.error("[ACCESS_DENIED] user={}, resource={}, decision={}",
event.getAuthentication().get().getName(),
event.getAuthorizationDecision(),
getRequestUri());
}
}
8. 会话管理与单点登录 (SSO)
8.1 Redis 集中式会话
微服务体系中,无状态 JWT 虽好但无法主动吊销。结合 Redis 实现集中式会话管理,兼顾无状态优势与会话可控性:
@Component
public class RedisSessionManager {
@Autowired private StringRedisTemplate redisTemplate;
private static final String SESSION_PREFIX = "session:";
private static final Duration SESSION_TTL = Duration.ofHours(2);
public void createSession(String sessionId, UserPrincipal user) {
String key = SESSION_PREFIX + sessionId;
Map<String, String> sessionData = Map.of(
"userId", user.getId().toString(),
"username", user.getUsername(),
"roles", String.join(",", user.getRoles()),
"ip", user.getClientIp(),
"loginAt", Instant.now().toString()
);
redisTemplate.opsForHash().putAll(key, sessionData);
redisTemplate.expire(key, SESSION_TTL);
}
public Optional<UserPrincipal> getSession(String sessionId) {
String key = SESSION_PREFIX + sessionId;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
if (entries.isEmpty()) return Optional.empty();
// 刷新 TTL(滑动过期)
redisTemplate.expire(key, SESSION_TTL);
return Optional.of(mapToUser(entries));
}
public void invalidateSession(String sessionId) {
redisTemplate.delete(SESSION_PREFIX + sessionId);
}
public void invalidateAllSessions(Long userId) {
// 用户登出所有设备场景中,维护 userId -> sessionIds 反向索引
Set<String> sessions = redisTemplate.opsForSet()
.members("user_sessions:" + userId);
if (sessions != null) {
redisTemplate.delete(sessions.stream()
.map(s -> SESSION_PREFIX + s)
.collect(Collectors.toList()));
}
}
}
8.2 SAML 与 OIDC 单点登录
企业级 SSO 常用协议对比:
| 协议 | 底层技术 | 适用场景 | 复杂度 |
|---|---|---|---|
| SAML 2.0 | XML + SOAP | 传统企业、政府 | 高 |
| OpenID Connect | OAuth2 + JWT | 互联网应用、移动端 | 中 |
| CAS | 独立协议 | 高校、内部系统 | 中 |
// Spring Security 配置 OIDC 登录 (OAuth2 Client)
@Configuration
public class OidcConfig {
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(
ClientRegistration.withRegistrationId("okta")
.clientId("{okta-client-id}")
.clientSecret("{okta-secret}")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://localhost:8080/login/oauth2/code/okta")
.scope("openid", "profile", "email")
.authorizationUri("https://dev-xxx.okta.com/oauth2/default/v1/authorize")
.tokenUri("https://dev-xxx.okta.com/oauth2/default/v1/token")
.userInfoUri("https://dev-xxx.okta.com/oauth2/default/v1/userinfo")
.userNameAttributeName("sub")
.clientName("Okta")
.build()
);
}
}
9. API 网关安全集成
9.1 Spring Cloud Gateway 安全过滤
网关作为流量入口,承担认证鉴权、限流、日志记录等职责:
@Component
@Order(-1) // 最早执行
public class GatewayAuthFilter implements GlobalFilter {
@Autowired private JwtTokenProvider tokenProvider;
@Autowired private RedisSessionManager sessionManager;
private final List<String> whitelist = List.of(
"/api/auth/login", "/api/auth/refresh", "/actuator/health"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 白名单放行
if (whitelist.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
// JWT 验证
String token = extractToken(exchange.getRequest());
if (token == null || !tokenProvider.validateToken(token)) {
return unauthorized(exchange);
}
// 可选:在网关层解析 JWT,透传用户信息到下游
String username = tokenProvider.getUsernameFromToken(token);
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-User-Id", username)
.header("X-User-Roles", String.join(",", getRoles(token)))
.build();
return chain.filter(exchange.mutate().request(mutated).build());
}
private Mono<Void> unauthorized(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = "{\"error\":\"Unauthorized\"}";
DataBuffer buffer = exchange.getResponse()
.bufferFactory().wrap(body.getBytes());
return exchange.getResponse().writeWith(Mono.just(buffer));
}
}
9.2 网关层限流与防刷
@Component
public class RateLimitFilter implements GlobalFilter {
@Autowired private ReactiveRedisTemplate<String, String> redisTemplate;
private final int MAX_REQUESTS = 100; // 每分钟
private final Duration WINDOW = Duration.ofMinutes(1);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String clientId = getClientIdentifier(exchange); // IP + User
String key = "ratelimit:" + clientId;
return redisTemplate.opsForValue()
.increment(key)
.flatMap(count -> {
if (count == 1) {
return redisTemplate.expire(key, WINDOW)
.thenReturn(count);
}
return Mono.just(count);
})
.flatMap(count -> {
if (count > MAX_REQUESTS) {
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
exchange.getResponse().getHeaders()
.add("X-RateLimit-Remaining", "0");
exchange.getResponse().setComplete();
}
return chain.filter(exchange);
});
}
}
10. 安全事件响应
| 事件类型 | 检测手段 | 响应动作 |
|---|---|---|
| 暴力破解 | 登录失败频率 > 5 次/分钟 | 锁定账号 + 发送告警 |
| Token 泄露 | 异地登录 + 设备指纹变化 | 吊销所有 Token + 强制重登 |
| 权限提升 | 高频访问 admin 接口 | 二次认证 + 审计日志 |
| 数据爬取 | 单 IP 请求量突增 | 验证码 + 限流降级 |
| 异常时间访问 | 非工作时间访问敏感接口 | 告警 + 人工审核 |
@Component
public class SecurityEventHandler {
@EventListener
public void onSuspiciousLogin(SuspiciousLoginEvent event) {
// 1. 记录安全审计日志
securityAuditLog.warn("SUSPICIOUS_LOGIN: user={}, ip={}, reason={}",
event.getUsername(), event.getIp(), event.getReason());
// 2. 发送告警通知
alertService.sendAlert(AlertLevel.HIGH,
"账号 " + event.getUsername() + " 存在异常登录行为");
// 3. 可选:临时锁定
if (event.getRiskScore() > 80) {
loginAttemptService.lockAccount(event.getUsername(), Duration.ofHours(1));
}
}
}
总结
| 层次 | 方案 | 要点 |
|---|---|---|
| 传输层 | TLS 1.3 + HSTS | 强制 HTTPS,防范中间人攻击 |
| 认证层 | JWT + OAuth2 | 无状态令牌,刷新令牌轮换 |
| 授权层 | RBAC + 方法级注解 | 最小权限原则,SpEL 表达式 |
| 应用层 | Spring Security 过滤器链 | CSRF 关闭(API)、CORS 白名单 |
| 数据层 | BCrypt/Argon2 + 密码策略 | 加盐哈希,防彩虹表 |
| 审计层 | 安全事件日志 | 登录失败、访问拒绝监控 |
现代 Java 安全体系的核心是:OAuth2 负责认证流转,JWT 负责无状态传输,Spring Security 负责权限控制,RBAC 负责授权管理。配合密码策略、登录保护、MFA 与审计日志,可构建企业级安全防线。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。