token升级(考虑在分布式环境中布置token,结合session保证请求调用过程中token不会过期。)

发布于:2025-03-18 ⋅ 阅读:(20) ⋅ 点赞:(0)

思路: 首先,用户的需求是确保使用同一个Token的外部调用都在一个Session中处理。 需要考虑Token与Session绑定、安全措施、Session管理、分布式处理等。

  1. 使用Redis作为Session存储, 在Java中 通过Spring Data Redis或Lettuce库实现。

2.生成Token部分,Java中常用JWT,可以用jjwt库来生成和解析。需要将Session ID嵌入到JWT的claims中,这样每次请求都能获取到对应的Session。

接下来是Token验证的流程。在Spring中,可以通过拦截器或过滤器来验证请求中的Token,并从Redis中获取对应的Session信息。这里需要处理Token的解析、验证以及Session的检索。

Session的生命周期管理部分,需要设置自动过期时间,并且在每次访问时更新Session的过期时间。可能需要使用Redis的过期机制,每次访问时延长TTL。

分布式系统的处理,使用Redis Cluster来保证不同服务实例都能访问到同一个Session存储。此外,需要确保Token的生成和验证逻辑在分布式环境中一致。

安全措施方面,需要确保JWT使用强加密算法,比如HS256或RS256。Token的时效要短,并且通过HTTPS传输。此外,可以检查IP地址的一致性,但需要考虑用户可能处于动态IP的情况,这可能作为可选的安全措施。

客户端规范部分,需要确保客户端在每次请求携带Token,并在Token过期时使用Refresh Token来更新。这部分可能需要前端配合,但后端需要提供刷新Token的接口。

监控和审计方面,可以在拦截器中记录请求的日志,存储到数据库或日志系统中。例如,使用Spring AOP来记录Session的活动。

现在需要将这些思路转化为具体的Java代码示例。例如,生成Token的方法,使用Jwts.builder()来创建JWT,添加claims,包括sessionId。存储到Redis时,使用RedisTemplate来操作键值对。

验证Token的过滤器需要继承OncePerRequestFilter,在doFilterInternal方法中提取Token,解析出sessionId,然后从Redis获取Session数据。如果Session不存在或无效,返回401错误。

还要考虑异常处理,比如Token过期、无效签名等情况,返回相应的HTTP状态码。

在代码结构上,可能需要配置Spring Security的过滤器链,将自定义的JWT过滤器添加到链中。同时,配置Redis连接信息,确保正确连接到Redis服务器。

最后,需要测试各个部分,确保Token生成、存储、验证和Session管理的流程正确,并且在高并发和分布式环境下稳定工作。

以下是基于Java的完整解决方案,使用Spring Boot框架配合Redis实现Token-Session绑定机制:

1. 核心依赖配置 (pom.xml)

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- JJWT for JWT处理 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

2. Session管理核心实现

Redis配置类
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}
Session服务类
@Service
public class SessionService {

    private static final String TOKEN_PREFIX = "token:";
    private static final String SESSION_PREFIX = "session:";
    private static final Duration TOKEN_TTL = Duration.ofMinutes(30);
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 创建Session并生成Token
    public String createSession(User user) {
        String sessionId = UUID.randomUUID().toString();
        String token = generateJwtToken(sessionId, user.getId());
        
        // 存储Token-Session映射
        redisTemplate.opsForValue().set(
            TOKEN_PREFIX + token, 
            sessionId, 
            TOKEN_TTL
        );

        // 存储Session数据
        HashOperations<String, Object, Object> ops = redisTemplate.opsForHash();
        ops.put(SESSION_PREFIX + sessionId, "user", user);
        ops.put(SESSION_PREFIX + sessionId, "creationTime", Instant.now());
        redisTemplate.expire(SESSION_PREFIX + sessionId, TOKEN_TTL);
        
        return token;
    }

    // 验证Token并获取Session
    public SessionContext validateToken(String token) {
        String sessionId = (String) redisTemplate.opsForValue().get(TOKEN_PREFIX + token);
        if (sessionId == null) {
            throw new InvalidTokenException("Invalid or expired token");
        }

        Map<Object, Object> entries = redisTemplate.opsForHash()
                .entries(SESSION_PREFIX + sessionId);
        
        if (entries.isEmpty()) {
            throw new SessionExpiredException("Session expired");
        }

        // 更新Session有效期
        redisTemplate.expire(TOKEN_PREFIX + token, TOKEN_TTL);
        redisTemplate.expire(SESSION_PREFIX + sessionId, TOKEN_TTL);

        return new SessionContext(
            sessionId,
            (User) entries.get("user"),
            (Instant) entries.get("creationTime")
        );
    }

    private String generateJwtToken(String sessionId, String userId) {
        return Jwts.builder()
                .setSubject(userId)
                .claim("sessionId", sessionId)
                .setIssuedAt(new Date())
                .setExpiration(Date.from(Instant.now().plus(TOKEN_TTL)))
                .signWith(SignatureAlgorithm.HS256, "your-256-bit-secret")
                .compact();
    }
}

3. 安全拦截器实现

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Autowired
    private SessionService sessionService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain)
            throws ServletException, IOException {
        
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            sendError(response, "Missing authorization header");
            return;
        }

        try {
            String token = authHeader.substring(7);
            SessionContext context = sessionService.validateToken(token);
            
            // 将会话上下文存入请求
            request.setAttribute("sessionContext", context);
            
            // 验证IP一致性(可选)
            validateIpConsistency(request, context);
            
        } catch (InvalidTokenException | SessionExpiredException e) {
            sendError(response, e.getMessage());
            return;
        }

        filterChain.doFilter(request, response);
    }

    private void validateIpConsistency(HttpServletRequest request, SessionContext context) {
        String currentIp = request.getRemoteAddr();
        // 首次访问记录IP,后续验证一致性
        if (context.getClientIp() == null) {
            context.setClientIp(currentIp);
        } else if (!context.getClientIp().equals(currentIp)) {
            throw new SecurityException("IP address changed during session");
        }
    }

    private void sendError(HttpServletResponse response, String message) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write("{\"error\": \"" + message + "\"}");
    }
}

4. 安全配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthFilter jwtAuthFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
                .antMatchers("/api/auth/login").permitAll()
                .anyRequest().authenticated();
    }
}

5. 会话上下文管理

public class SessionContext {
    private final String sessionId;
    private final User user;
    private final Instant creationTime;
    private String clientIp;

    // 构造函数、getters、setters
}

@RestController
@RequestMapping("/api")
public class ApiController {

    @GetMapping("/protected-data")
    public ResponseEntity<?> getProtectedData(@RequestAttribute SessionContext context) {
        return ResponseEntity.ok(Map.of(
            "user", context.getUser(),
            "sessionId", context.getSessionId(),
            "data", "Sensitive information"
        ));
    }
}

6. 异常处理增强

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler({InvalidTokenException.class, SessionExpiredException.class})
    public ResponseEntity<?> handleAuthExceptions(RuntimeException ex) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", ex.getMessage()));
    }

    @ExceptionHandler(SecurityException.class)
    public ResponseEntity<?> handleSecurityExceptions(SecurityException ex) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(Map.of("error", ex.getMessage()));
    }
}

架构流程说明:

  1. Token生成

    • 用户登录成功后生成包含sessionId的JWT
    • Redis存储两个关键记录:
      • token:<jwt> → sessionId (TTL 30分钟)
      • session:<sessionId> → 完整会话数据 (TTL 30分钟)
  2. 请求处理

    Client Filter SessionService Redis Controller 携带Bearer Token 验证Token 获取sessionId 返回sessionId 获取会话数据 返回会话数据 更新TTL 传递会话上下文 返回业务数据 Client Filter SessionService Redis Controller
  3. 关键特性

    • 自动续期:每次有效访问自动延长Token和Session有效期
    • IP绑定:可选的安全增强功能
    • 分布式支持:通过Redis实现跨服务节点的会话共享
    • 安全防护:JWT签名验证 + Redis存储校验双保险

最佳实践建议:

  1. 密钥管理:使用安全密钥管理系统存储JWT签名密钥
  2. 监控报警:监控以下指标:
    // 示例监控点
    @Aspect
    @Component
    public class SessionMonitor {
        @Around("execution(* com.example.service.SessionService.validateToken(..))")
        public Object monitorValidation(ProceedingJoinPoint pjp) throws Throwable {
            long start = System.currentTimeMillis();
            try {
                return pjp.proceed();
            } finally {
                long duration = System.currentTimeMillis() - start;
                Metrics.timer("session.validation.time").record(duration);
            }
        }
    }
    
  3. 压力测试:针对Session服务进行专项压测,确保Redis集群性能
  4. 安全审计:记录关键会话事件:
    public class SessionAuditListener {
        @Async
        @EventListener
        public void handleSessionEvent(SessionEvent event) {
            auditRepository.save(new AuditLog(
                event.getType(),
                event.getSessionId(),
                event.getUserId(),
                Instant.now()
            ));
        }
    }
    

该方案通过严格的Token-Session绑定机制,结合Spring Security的过滤器和Redis的高性能存储,确保了外部调用在统一会话上下文中完成。所有会话状态集中存储在Redis中,天然支持分布式部署,同时通过JWT的标准验证机制保障了传输安全性。