[JAVA] JWT 기반 인증/인가 구현하기 (Spring Boot 실전 가이드)

JWT(Json Web Token)은 RESTful API에서 세션 없이 인증과 인가를 처리할 수 있는 토큰 기반의 인증 방식입니다.
Spring Boot에서 JWT 기반 인증/인가를 구현하는 흐름을 알려드리겠습니다.

 

 


JWT란 무엇인가?

JWT는 Header, Payload, Signature 3가지로 구성된 JSON 기반 토큰입니다.
일반적으로 인증이 완료된 사용자의 정보를 Payload에 담아 클라이언트에 전달하며, 이 토큰을 재사용해 API 요청 시 인증 정보를 대신합니다.


HEADER:    { "alg": "HS256", "typ": "JWT" }
PAYLOAD:   { "sub": "userId", "role": "ROLE_USER", "exp": 1700000000 }
SIGNATURE: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

JWT는 자체적으로 정보를 포함하므로 DB 세션 조회 없이 Stateless 인증이 가능합니다.

 

 

Spring Boot에서 JWT 인증 흐름

  1. 사용자가 로그인 요청 (ID/PW)
  2. 서버에서 인증 성공 시 JWT 발급
  3. 클라이언트는 이후 요청 시 Authorization: Bearer {token} 헤더에 포함
  4. 서버는 JWT를 검증 후 사용자 정보 추출
  5. 인가(권한 체크) 처리 후 요청 수행

이 과정은 Spring Security 필터 체인에서 대부분 처리됩니다.

 

 

JWT 의존성 추가 (Gradle)

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

jjwt는 JWT 생성을 위한 가장 널리 사용되는 라이브러리입니다.

 

 

JWT 생성/검증 유틸 클래스

@Component
public class JwtUtil {

    private final String SECRET_KEY = "your-secret-key";

    public String generateToken(String username, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(username);
        claims.put("roles", roles);

        return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 3600_000)) // 1시간
            .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256)
            .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY.getBytes())
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }

    public String getUsernameFromToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(SECRET_KEY.getBytes())
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
}

 

 

JWT 인증 필터 구현

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        String token = resolveToken(request);

        if (token != null && jwtUtil.validateToken(token)) {
            String username = jwtUtil.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authToken =
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authToken);
        }

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

 

 

Security 설정에 필터 적용

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

 

 

로그인 API 구현

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authManager;
    private final JwtUtil jwtUtil;

    public AuthController(AuthenticationManager authManager, JwtUtil jwtUtil) {
        this.authManager = authManager;
        this.jwtUtil = jwtUtil;
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        Authentication auth = authManager.authenticate(
            new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
        );

        List<String> roles = auth.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .toList();

        String token = jwtUtil.generateToken(request.getUsername(), roles);

        return ResponseEntity.ok(Map.of("token", token));
    }
}

 

 

실무 팁

  • 토큰 만료 시간은 너무 길게 설정하지 마세요 (보통 15분 ~ 1시간)
  • Refresh Token을 도입하면 자동 로그인 유지가 가능합니다
  • 쿠키 대신 Authorization: Bearer {token} 방식 권장
  • JWT는 위조 위험이 있으므로 SECRET_KEY는 노출되면 안 됩니다

 


 

 

JWT는 REST API에서 세션 없이 인증을 처리할 수 있는 강력한 도구입니다.
Spring Boot에서 JWT 인증을 구현하면, 마이크로서비스 구조프론트-백엔드 분리 아키텍처에서 유연하게 인증 로직을 처리할 수 있습니다.
단, 토큰 저장 위치, 만료 정책, 리프레시 토큰 처리 등은 보안상 충분한 고민이 필요합니다.