RedisとJWTによるSpring Bootアプリケーションの単一セッション制御

依存関係の設定

プロジェクトに必要なライブラリを追加します。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
</dependencies>

Redis接続設定

application.propertiesでRedis接続を定義します。

spring.redis.host=localhost
spring.redis.port=6379

JWT処理ユーティリティ

トークンの生成・検証ロジックを実装します。

import io.jsonwebtoken.*;
import java.util.Date;
import java.util.Map;

public class TokenProcessor {
    private final String secretKey;

    public TokenProcessor(String secret) {
        this.secretKey = secret;
    }

    public String generateToken(Map<String, Object> payload) {
        return Jwts.builder()
                .setClaims(payload)
                .setExpiration(new Date(System.currentTimeMillis() + 3600000))
                .signWith(SignatureAlgorithm.HS512, secretKey)
                .compact();
    }

    public boolean verifyToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    public Claims extractClaims(String token) {
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody();
    }
}

セキュリティ設定

Spring Securityの設定クラスを作成します。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private TokenProcessor tokenProcessor;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/login", "/register").permitAll()
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(
                new JwtAuthFilter(tokenProcessor), 
                UsernamePasswordAuthenticationFilter.class
            );
    }
}

認証フィルター実装

リクエストごとのJWT検証処理を実装します。

public class JwtAuthFilter extends OncePerRequestFilter {
    private final TokenProcessor tokenProcessor;

    public JwtAuthFilter(TokenProcessor processor) {
        this.tokenProcessor = processor;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) 
        throws ServletException, IOException {
        
        String token = extractToken(req);
        if (token != null && tokenProcessor.verifyToken(token)) {
            Claims claims = tokenProcessor.extractClaims(token);
            String userId = claims.getSubject();
            
            Authentication auth = new UsernamePasswordAuthenticationToken(
                userId, null, Collections.emptyList()
            );
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(req, res);
    }

    private String extractToken(HttpServletRequest req) {
        String header = req.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

セッション管理サービス

Redisを使用したセッション状態管理を実装します。

@Service
public class SessionService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    public void registerSession(String userId, String token) {
        redisTemplate.opsForValue().set(
            "user_session:" + userId, 
            token,
            1, TimeUnit.HOURS
        );
    }

    public boolean isActiveSession(String userId, String token) {
        String activeToken = redisTemplate.opsForValue().get("user_session:" + userId);
        return token.equals(activeToken);
    }

    public void removeSession(String userId) {
        redisTemplate.delete("user_session:" + userId);
    }
}

認証フィルターの拡張

セッション状態チェックを追加します。

public class JwtAuthFilter extends OncePerRequestFilter {
    // 既存のフィールドとメソッド
    
    @Autowired
    private SessionService sessionService;

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) 
        throws ServletException, IOException {
        
        String token = extractToken(req);
        if (token != null && tokenProcessor.verifyToken(token)) {
            Claims claims = tokenProcessor.extractClaims(token);
            String userId = claims.getSubject();
            
            if (!sessionService.isActiveSession(userId, token)) {
                res.setStatus(HttpServletResponse.SC_CONFLICT);
                res.getWriter().write("別のセッションがアクティブです");
                return;
            }
            
            // 認証情報設定...
        }
        chain.doFilter(req, res);
    }
}

ログアウト処理

セッション終了時のクリーンアップを実装します。

@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Autowired
    private SessionService sessionService;
    
    @PostMapping("/logout")
    public ResponseEntity<String> logout(@RequestHeader("Authorization") String authHeader) {
        String token = authHeader.substring(7);
        Claims claims = tokenProcessor.extractClaims(token);
        String userId = claims.getSubject();
        
        sessionService.removeSession(userId);
        return ResponseEntity.ok("ログアウトしました");
    }
}

設計の主要ポイント

  • JWT発行時にユーザー識別子をクレームに設定
  • RedisにユーザーIDとトークンのペアを保存
  • リクエストごとにRedisのアクティブセッションを検証
  • セッション競合時はHTTP 409 Conflictを返却
  • トークン有効期限とRedisのTTLを同期

タグ: Spring Security redis JWT セッション管理 Spring Boot

7月13日 00:06 投稿