企业级前后端认证方式

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 微服务认证

微服务认证挑战

  1. 服务间调用需要认证
  1. 用户Token需要在服务间传递
  1. 需要统一的认证中心

微服务认证方案

方案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):使用多种认证方式提高安全性。

认证因素:

  1. 知识因素:密码、PIN码
  1. 拥有因素:手机、硬件Token
  1. 生物因素:指纹、人脸识别

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的挑战

  1. 多服务器需要共享Session
  1. Session数据一致性
  1. 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 总结

企业级认证架构要点

  1. 多因素认证:提高安全性
  1. 分布式Session:支持水平扩展
  1. API网关认证:统一入口
  1. 权限缓存:提升性能
  1. 审计日志:追踪操作
  1. 安全监控:及时发现异常

最佳实践

  1. 使用HTTPS传输
  1. Token设置合理过期时间
  1. 实现Token刷新机制
  1. 使用密码加密存储
  1. 实现登录失败锁定
  1. 定期更新密钥
  1. 监控异常行为
  1. 记录操作日志
相关推荐
林小帅20 小时前
【笔记】OpenClaw 架构浅析
前端·agent
林小帅21 小时前
【笔记】OpenClaw 生态系统的多语言实现对比分析
前端·agent
程序猿的程21 小时前
开源一个 React 股票 K 线图组件,传个股票代码就能画图
前端·javascript
不爱说话郭德纲21 小时前
告别漫长的HbuilderX云打包排队!uni-app x 安卓本地打包保姆级教程(附白屏、包体积过大排坑指南)
android·前端·uni-app
唐叔在学习1 天前
[前端特效] 左滑显示按钮的实现介绍
前端·javascript
用户5282290301801 天前
【学习笔记】ECMAScript 词法环境全解析
前端
青青家的小灰灰1 天前
React 架构进阶:自定义 Hooks 的高级设计模式与最佳实践
前端·react.js·前端框架
Angelial1 天前
Vite 性能瓶颈排查标准流程
前端
不要秃头啊1 天前
别再谈提效了:AI 时代的开发范式本质变了
前端·后端·程序员
青青家的小灰灰1 天前
深入理解事件循环:异步编程的基石
前端·javascript·面试