企业级前后端认证方式

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. 记录操作日志
相关推荐
2501_948120153 小时前
基于Vue 3的可视化大屏系统设计
前端·javascript·vue.js
cws2004013 小时前
MFA双因素用户使用手册
运维·windows·网络安全·github·邮件·邮箱
Jinuss3 小时前
源码分析之React中createFiberRoot方法创建Fiber根节点
前端·javascript·react.js
Jinuss4 小时前
源码分析之React中ReactDOMRoot实现
前端·javascript·react.js
web守墓人4 小时前
【前端】vue3的指令
前端
billy_gisboy4 小时前
01-Windows+DockerDesktop部署ClickHouse
windows·clickhouse
想起你的日子5 小时前
EFCore之Code First
前端·.netcore
2501_944424125 小时前
Flutter for OpenHarmony游戏集合App实战之黑白棋落子翻转
android·开发语言·windows·flutter·游戏·harmonyos
框架图5 小时前
Html语法
前端·html