3.5 API网关认证
API网关的作用
API网关是微服务架构中的统一入口,负责:
- 路由转发
- 认证授权
- 限流熔断
- 日志监控
网关认证实现(Spring Cloud Gateway)
1. 网关配置
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/api/user/**")
.filters(f -> f.filter(authFilter()))
.uri("http://user-service"))
.route("order-service", r -> r.path("/api/order/**")
.filters(f -> f.filter(authFilter()))
.uri("http://order-service"))
.build();
}
@Bean
public GatewayFilter authFilter() {
return new AuthGatewayFilter();
}
}
2. 认证过滤器
@Component
public class AuthGatewayFilter implements GatewayFilter {
@Autowired
private JWTUtil jwtUtil;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 1. 获取Token
String token = getTokenFromRequest(request);
if (token == null) {
return unauthorized(exchange);
}
// 2. 验证Token
if (!jwtUtil.validateToken(token)) {
return unauthorized(exchange);
}
// 3. 检查Token是否在黑名单中
if (isTokenBlacklisted(token)) {
return unauthorized(exchange);
}
// 4. 从Token中提取用户信息
String userId = jwtUtil.getUserIdFromToken(token);
String username = jwtUtil.getUsernameFromToken(token);
// 5. 将用户信息添加到请求头,传递给下游服务
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", userId)
.header("X-Username", username)
.build();
ServerWebExchange modifiedExchange = exchange.mutate()
.request(modifiedRequest)
.build();
return chain.filter(modifiedExchange);
}
private String getTokenFromRequest(ServerHttpRequest request) {
// 从Header中获取
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);
}
}
// 从Cookie中获取
List<HttpCookie> cookies = request.getCookies().get("token");
if (cookies != null && !cookies.isEmpty()) {
return cookies.get(0).getValue();
}
return null;
}
private boolean isTokenBlacklisted(String token) {
return redisTemplate.hasKey("blacklist:" + token);
}
private Mono<Void> unauthorized(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json");
String body = "{\"error\":\"未授权\"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes());
return response.writeWith(Mono.just(buffer));
}
}
3. 全局异常处理
@Configuration
public class GatewayExceptionHandler {
@Bean
public ErrorWebExceptionHandler errorWebExceptionHandler() {
return new GlobalExceptionHandler();
}
}
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (ex instanceof TokenExpiredException) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return writeResponse(response, "{\"error\":\"Token已过期\"}");
}
if (ex instanceof InvalidTokenException) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return writeResponse(response, "{\"error\":\"Token无效\"}");
}
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
return writeResponse(response, "{\"error\":\"服务器内部错误\"}");
}
private Mono<Void> writeResponse(ServerHttpResponse response, String body) {
response.getHeaders().add("Content-Type", "application/json");
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes());
return response.writeWith(Mono.just(buffer));
}
}
3.6 微服务认证
微服务认证挑战
- 服务间调用需要认证
- 用户Token需要在服务间传递
- 需要统一的认证中心
微服务认证方案
方案1:Token传递
@Service
public class ServiceAClient {
@Autowired
private RestTemplate restTemplate;
/**
* 调用服务B,传递用户Token
*/
public String callServiceB(String userToken, String apiPath) {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + userToken);
headers.add("X-User-Id", getUserIdFromToken(userToken));
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
"http://service-b" + apiPath,
HttpMethod.GET,
entity,
String.class
);
return response.getBody();
}
}
方案2:服务间Token(Service Token)
@Service
public class ServiceTokenService {
@Autowired
private JWTUtil jwtUtil;
@Value("${service.token.secret}")
private String serviceSecret;
/**
* 生成服务间Token
*/
public String generateServiceToken(String serviceName, String targetService) {
Map<String, Object> claims = new HashMap<>();
claims.put("serviceName", serviceName);
claims.put("targetService", targetService);
claims.put("type", "service");
return jwtUtil.generateToken(claims, serviceSecret, 3600); // 1小时
}
/**
* 验证服务间Token
*/
public boolean validateServiceToken(String token) {
try {
Claims claims = jwtUtil.parseToken(token, serviceSecret);
return "service".equals(claims.get("type"));
} catch (Exception e) {
return false;
}
}
}
方案3:OAuth 2.0 Client Credentials
@Service
public class OAuth2ClientCredentialsService {
@Autowired
private RestTemplate restTemplate;
@Value("${oauth2.client-id}")
private String clientId;
@Value("${oauth2.client-secret}")
private String clientSecret;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 获取服务间Access Token
*/
public String getServiceAccessToken() {
// 1. 检查缓存中是否有Token
String cachedToken = redisTemplate.opsForValue().get("service_token");
if (cachedToken != null) {
return cachedToken;
}
// 2. 请求Token
String tokenUrl = "http://auth-server/oauth2/token";
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "client_credentials");
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);
ResponseEntity<Map> response = restTemplate.exchange(
tokenUrl,
HttpMethod.POST,
entity,
Map.class
);
// 3. 提取Token
Map<String, Object> body = response.getBody();
String accessToken = (String) body.get("access_token");
Integer expiresIn = (Integer) body.get("expires_in");
// 4. 缓存Token
redisTemplate.opsForValue().set(
"service_token",
accessToken,
expiresIn - 60,
TimeUnit.SECONDS
);
return accessToken;
}
}
3.7 多因素认证(MFA)
MFA的概念
多因素认证(Multi-Factor Authentication):使用多种认证方式提高安全性。
认证因素:
- 知识因素:密码、PIN码
- 拥有因素:手机、硬件Token
- 生物因素:指纹、人脸识别
MFA实现
1. 短信验证码
@Service
public class SMSCodeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private SMSSender smsSender;
private static final int CODE_LENGTH = 6;
private static final long CODE_EXPIRE = 5 * 60; // 5分钟
/**
* 发送验证码
*/
public void sendCode(String phoneNumber) {
// 1. 生成验证码
String code = generateCode();
// 2. 存储到Redis
String key = "sms_code:" + phoneNumber;
redisTemplate.opsForValue().set(key, code, CODE_EXPIRE, TimeUnit.SECONDS);
// 3. 发送短信
smsSender.send(phoneNumber, "您的验证码是:" + code + ",5分钟内有效");
}
/**
* 验证验证码
*/
public boolean verifyCode(String phoneNumber, String code) {
String key = "sms_code:" + phoneNumber;
String storedCode = redisTemplate.opsForValue().get(key);
if (storedCode == null) {
return false;
}
if (!storedCode.equals(code)) {
return false;
}
// 验证成功后删除验证码
redisTemplate.delete(key);
return true;
}
private String generateCode() {
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
code.append(random.nextInt(10));
}
return code.toString();
}
}
2. TOTP(时间-based一次性密码)
@Service
public class TOTPService {
/**
* 生成TOTP密钥
*/
public String generateSecret() {
byte[] secret = new byte[20];
new SecureRandom().nextBytes(secret);
return Base32.encode(secret);
}
/**
* 生成TOTP二维码URL
*/
public String generateQRCodeUrl(String username, String secret, String issuer) {
String otpAuthUrl = String.format(
"otpauth://totp/%s:%s?secret=%s&issuer=%s",
issuer, username, secret, issuer
);
return "https://api.qrserver.com/v1/create-qr-code/?size=200x200\&data=" +
URLEncoder.encode(otpAuthUrl, "UTF-8");
}
/**
* 验证TOTP
*/
public boolean verifyTOTP(String secret, String code) {
long timeStep = System.currentTimeMillis() / 1000 / 30; // 30秒一个时间窗口
// 验证当前时间窗口
if (generateTOTP(secret, timeStep).equals(code)) {
return true;
}
// 验证前一个时间窗口(允许时间偏差)
if (generateTOTP(secret, timeStep - 1).equals(code)) {
return true;
}
// 验证后一个时间窗口
if (generateTOTP(secret, timeStep + 1).equals(code)) {
return true;
}
return false;
}
private String generateTOTP(String secret, long timeStep) {
try {
byte[] key = Base32.decode(secret);
byte[] time = new byte[8];
for (int i = 7; i >= 0; i--) {
time[i] = (byte) (timeStep & 0xFF);
timeStep >>= 8;
}
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA1");
mac.init(keySpec);
byte[] hash = mac.doFinal(time);
int offset = hash[hash.length - 1] & 0x0F;
int binary = ((hash[offset] & 0x7F) << 24) |
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);
int otp = binary % 1000000;
return String.format("%06d", otp);
} catch (Exception e) {
throw new RuntimeException("生成TOTP失败", e);
}
}
}
3. MFA登录流程
@RestController
@RequestMapping("/api/auth")
public class MFAAuthController {
@Autowired
private UserService userService;
@Autowired
private SMSCodeService smsCodeService;
@Autowired
private TOTPService totpService;
@Autowired
private JWTUtil jwtUtil;
/**
* 第一步:用户名密码登录
*/
@PostMapping("/login/step1")
public ResponseEntity<LoginStep1Response> loginStep1(@RequestBody LoginRequest request) {
// 1. 验证用户名密码
User user = userService.validateUser(request.getUsername(), request.getPassword());
if (user == null) {
return ResponseEntity.status(401).build();
}
// 2. 检查用户是否启用了MFA
if (!user.isMfaEnabled()) {
// 未启用MFA,直接返回Token
String token = jwtUtil.generateToken(user);
LoginStep1Response response = new LoginStep1Response();
response.setToken(token);
response.setMfaRequired(false);
return ResponseEntity.ok(response);
}
// 3. 生成临时Token(用于第二步验证)
String tempToken = jwtUtil.generateTempToken(user.getId());
// 4. 根据MFA类型发送验证码
LoginStep1Response response = new LoginStep1Response();
response.setTempToken(tempToken);
response.setMfaRequired(true);
response.setMfaType(user.getMfaType());
if ("SMS".equals(user.getMfaType())) {
smsCodeService.sendCode(user.getPhoneNumber());
response.setMessage("验证码已发送到手机");
} else if ("TOTP".equals(user.getMfaType())) {
response.setMessage("请输入TOTP验证码");
}
return ResponseEntity.ok(response);
}
/**
* 第二步:MFA验证
*/
@PostMapping("/login/step2")
public ResponseEntity<LoginStep2Response> loginStep2(@RequestBody MFAVerifyRequest request) {
// 1. 验证临时Token
String userId = jwtUtil.getUserIdFromTempToken(request.getTempToken());
if (userId == null) {
return ResponseEntity.status(401).build();
}
// 2. 获取用户信息
User user = userService.getUserById(userId);
// 3. 验证MFA代码
boolean verified = false;
if ("SMS".equals(user.getMfaType())) {
verified = smsCodeService.verifyCode(user.getPhoneNumber(), request.getCode());
} else if ("TOTP".equals(user.getMfaType())) {
verified = totpService.verifyTOTP(user.getTotpSecret(), request.getCode());
}
if (!verified) {
return ResponseEntity.status(401).build();
}
// 4. 生成正式Token
String token = jwtUtil.generateToken(user);
LoginStep2Response response = new LoginStep2Response();
response.setToken(token);
response.setMessage("登录成功");
return ResponseEntity.ok(response);
}
}
3.8 分布式Session管理
分布式Session的挑战
- 多服务器需要共享Session
- Session数据一致性
- Session失效管理
Redis实现分布式Session
1. Spring Session配置
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisSessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
LettuceConnectionFactory factory = new LettuceConnectionFactory();
factory.setHostName("localhost");
factory.setPort(6379);
factory.setPassword("password");
factory.setDatabase(0);
return factory;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
2. Session共享过滤器
@Component
public class SessionShareFilter implements Filter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 1. 获取SessionID
String sessionId = getSessionIdFromRequest(httpRequest);
if (sessionId != null) {
// 2. 从Redis中加载Session
Map<String, Object> sessionData = loadSessionFromRedis(sessionId);
if (sessionData != null) {
// 3. 创建Session包装器
HttpSession session = new RedisHttpSession(httpRequest, sessionId, sessionData);
httpRequest = new SessionRequestWrapper(httpRequest, session);
}
}
chain.doFilter(httpRequest, httpResponse);
}
private String getSessionIdFromRequest(HttpServletRequest request) {
// 从Cookie中获取
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("JSESSIONID".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
private Map<String, Object> loadSessionFromRedis(String sessionId) {
String key = "spring:session:sessions:" + sessionId;
return (Map<String, Object>) redisTemplate.opsForHash().entries(key);
}
}
3. Session同步服务
@Service
public class SessionSyncService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 保存Session到Redis
*/
public void saveSession(String sessionId, Map<String, Object> sessionData, int maxInactiveInterval) {
String key = "spring:session:sessions:" + sessionId;
redisTemplate.opsForHash().putAll(key, sessionData);
redisTemplate.expire(key, maxInactiveInterval, TimeUnit.SECONDS);
}
/**
* 从Redis加载Session
*/
public Map<String, Object> loadSession(String sessionId) {
String key = "spring:session:sessions:" + sessionId;
return redisTemplate.opsForHash().entries(key);
}
/**
* 删除Session
*/
public void deleteSession(String sessionId) {
String key = "spring:session:sessions:" + sessionId;
redisTemplate.delete(key);
}
/**
* 更新Session过期时间
*/
public void refreshSession(String sessionId, int maxInactiveInterval) {
String key = "spring:session:sessions:" + sessionId;
redisTemplate.expire(key, maxInactiveInterval, TimeUnit.SECONDS);
}
}
3.9 权限缓存优化
权限缓存策略
1. 多级缓存
@Service
public class PermissionCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private PermissionService permissionService;
private static final String CACHE_PREFIX = "permission:";
private static final long CACHE_EXPIRE = 3600; // 1小时
/**
* 获取用户权限(带缓存)
*/
public List<String> getUserPermissions(String userId) {
// 1. 先查本地缓存(Caffeine)
List<String> permissions = getFromLocalCache(userId);
if (permissions != null) {
return permissions;
}
// 2. 再查Redis缓存
permissions = getFromRedisCache(userId);
if (permissions != null) {
// 写入本地缓存
putToLocalCache(userId, permissions);
return permissions;
}
// 3. 查数据库
permissions = permissionService.getUserPermissions(userId);
// 4. 写入缓存
putToRedisCache(userId, permissions);
putToLocalCache(userId, permissions);
return permissions;
}
private List<String> getFromLocalCache(String userId) {
// 使用Caffeine本地缓存
return localCache.getIfPresent(userId);
}
private List<String> getFromRedisCache(String userId) {
String key = CACHE_PREFIX + userId;
return (List<String>) redisTemplate.opsForValue().get(key);
}
private void putToLocalCache(String userId, List<String> permissions) {
localCache.put(userId, permissions);
}
private void putToRedisCache(String userId, List<String> permissions) {
String key = CACHE_PREFIX + userId;
redisTemplate.opsForValue().set(key, permissions, CACHE_EXPIRE, TimeUnit.SECONDS);
}
/**
* 清除权限缓存
*/
public void clearPermissionCache(String userId) {
// 清除本地缓存
localCache.invalidate(userId);
// 清除Redis缓存
String key = CACHE_PREFIX + userId;
redisTemplate.delete(key);
}
}
2. 权限变更通知
@Service
public class PermissionChangeNotifier {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 通知权限变更
*/
public void notifyPermissionChange(String userId) {
// 发布消息到Redis频道
redisTemplate.convertAndSend("permission:change", userId);
}
/**
* 监听权限变更
*/
@EventListener
public void onPermissionChange(String userId) {
// 清除本地缓存
permissionCacheService.clearPermissionCache(userId);
}
}
3.10 审计日志
操作审计
@Aspect
@Component
public class AuditLogAspect {
@Autowired
private AuditLogService auditLogService;
@Around("@annotation(auditLog)")
public Object audit(ProceedingJoinPoint joinPoint, AuditLog auditLog) throws Throwable {
// 1. 获取操作信息
String userId = getCurrentUserId();
String operation = auditLog.operation();
String resource = auditLog.resource();
// 2. 记录操作前
AuditLogEntity logEntity = new AuditLogEntity();
logEntity.setUserId(userId);
logEntity.setOperation(operation);
logEntity.setResource(resource);
logEntity.setRequestTime(new Date());
logEntity.setRequestParams(getRequestParams(joinPoint));
try {
// 3. 执行操作
Object result = joinPoint.proceed();
// 4. 记录操作成功
logEntity.setStatus("SUCCESS");
logEntity.setResponseTime(new Date());
logEntity.setResponseData(getResponseData(result));
auditLogService.save(logEntity);
return result;
} catch (Exception e) {
// 5. 记录操作失败
logEntity.setStatus("FAILED");
logEntity.setErrorMsg(e.getMessage());
logEntity.setResponseTime(new Date());
auditLogService.save(logEntity);
throw e;
}
}
}
3.11 安全监控与告警
异常登录检测
@Service
public class SecurityMonitorService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private AlertService alertService;
/**
* 检测异常登录
*/
public void detectAbnormalLogin(String userId, String ip, String userAgent) {
String key = "login_history:" + userId;
// 1. 获取历史登录记录
List<LoginRecord> history = getLoginHistory(userId);
// 2. 检测异常IP
if (isAbnormalIP(history, ip)) {
alertService.sendAlert("异常IP登录", userId, ip);
}
// 3. 检测异常时间
if (isAbnormalTime(history)) {
alertService.sendAlert("异常时间登录", userId, new Date().toString());
}
// 4. 检测异常设备
if (isAbnormalDevice(history, userAgent)) {
alertService.sendAlert("异常设备登录", userId, userAgent);
}
// 5. 记录本次登录
recordLogin(userId, ip, userAgent);
}
private boolean isAbnormalIP(List<LoginRecord> history, String currentIP) {
if (history.isEmpty()) {
return false;
}
// 检查最近10次登录是否都是同一个IP
Set<String> recentIPs = history.stream()
.limit(10)
.map(LoginRecord::getIp)
.collect(Collectors.toSet());
return !recentIPs.contains(currentIP);
}
}
3.12 总结
企业级认证架构要点
- 多因素认证:提高安全性
- 分布式Session:支持水平扩展
- API网关认证:统一入口
- 权限缓存:提升性能
- 审计日志:追踪操作
- 安全监控:及时发现异常
最佳实践
- 使用HTTPS传输
- Token设置合理过期时间
- 实现Token刷新机制
- 使用密码加密存储
- 实现登录失败锁定
- 定期更新密钥
- 监控异常行为
- 记录操作日志