상세 컨텐츠

본문 제목

Java 19 + Spring Boot 3.1.2 JWT 인증 시스템 구축 가이드

Developer/Backend

by 웰크 2025. 4. 25. 20:44

본문

이 글은 Java 19, Spring Boot 3.1.2, Spring Security 6, Gradle 빌드 시스템 환경에서 JWT(JSON Web Token)를 기반으로 인증 시스템을 구축하는 과정을 정리한 중급 개발자용 실전 가이드입니다.


✅ JWT의 장점과 단점

🔷 장점

  • 서버가 상태를 저장하지 않아 확장성과 분산 아키텍처에 유리함
  • 사용자 정보와 권한을 클라이언트가 포함하여 요청할 수 있음
  • 인증 서버와 리소스 서버의 분리 용이

🔶 단점

  • 토큰 무효화 어려움 (서버 세션 없음)
  • 페이로드 인코딩만 되어 있어 민감 정보 포함 불가
  • 탈취 시 위험 → HTTPS 및 토큰 저장 방식에 주의 필요

⚙️ JWT 사용 시 고려사항

  • HMAC 또는 RSA 서명 알고리즘 사용 (HS256, RS256 등)
  • Access Token은 짧게, Refresh Token은 길게 설정
  • HTTPS 전송 필수 / 클라이언트는 HTTP-Only 쿠키 사용 권장
  • 로그아웃 또는 재발급 시 토큰 무효화 전략 필요 (블랙리스트 등)

🛠 Gradle 환경 설정 및 의존성 추가

build.gradle.kts:

dependencies {
    implementation("io.jsonwebtoken:jjwt-api:0.11.5")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
}

🔐 JWT 인증 구성하기 (JwtUtil + 필터 + SecurityConfig)

✅ JwtUtil.java

@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String secretKey;

    private final long ACCESS_EXPIRATION = 1000 * 60 * 15;
    private final long REFRESH_EXPIRATION = 1000 * 60 * 60 * 24 * 7;

    public String generateAccessToken(String username) {
        return generateToken(username, ACCESS_EXPIRATION);
    }

    public String generateRefreshToken(String username) {
        return generateToken(username, REFRESH_EXPIRATION);
    }

    private String generateToken(String username, long expiration) {
        return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
    }

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

    public boolean isTokenValid(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

✅ JwtAuthenticationFilter.java

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            String username = jwtUtil.getUsernameFromToken(token);

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (jwtUtil.isTokenValid(token)) {
                    UsernamePasswordAuthenticationToken authToken =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

👤 컨트롤러에서 사용자 정보 활용

@GetMapping("/api/user/me")
public ResponseEntity<?> getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
    return ResponseEntity.ok(userDetails.getUsername());
}

@GetMapping("/api/user/context")
public ResponseEntity<?> getContextUser() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    return ResponseEntity.ok(authentication.getName());
}

❓ JWT 관련 자주 묻는 질문 Top 5

1. JWT는 세션 기반 인증과 어떻게 다른가요?

세션 기반 인증은 서버가 사용자 세션 정보를 저장하고 관리하는 방식입니다. 반면, JWT는 클라이언트가 토큰을 보유하고 서버는 이를 검증만 하는 무상태(stateless) 인증 방식입니다.

  • 세션: 상태 유지 필요, 서버 메모리 또는 DB 사용
  • JWT: 확장성 우수, 마이크로서비스/모바일에 적합

2. JWT를 어디에 저장하는 것이 가장 안전한가요?

  • LocalStorage: XSS에 취약
  • SessionStorage: 탭 닫히면 삭제되지만 여전히 XSS 취약
  • HTTP-Only 쿠키: JS 접근 불가, 가장 안전

✅ 권장: Secure, SameSite 설정이 된 HTTP-Only 쿠키 사용

3. JWT 유효 기간은 어떻게 설정해야 하나요?

  • Access Token: 15분~1시간 정도로 짧게 설정
  • Refresh Token: 1주~1개월 정도로 설정

💡 Refresh Token을 사용해 Access Token 재발급 로직 구현 필수

4. 보안을 위해 어떤 점을 고려해야 하나요?

  • 민감 정보는 페이로드에 포함하지 않기
  • 강력한 서명 알고리즘 사용 (예: RS256)
  • HTTPS 필수
  • jti 클레임을 활용해 토큰 재사용 방지 및 블랙리스트 관리

5. Refresh Token은 어떻게 구현하나요?

  1. 로그인 시 Access + Refresh Token 동시 발급
  2. Refresh Token은 DB나 Redis 등에 저장
  3. Access Token 만료 시 Refresh Token으로 재발급
  4. 로그아웃 시 Refresh Token 삭제 및 무효화 처리

💡 실전 주제 심화 예제

✅ Redis 기반 Refresh Token 저장

@Service
public class RefreshTokenService {
    private final RedisTemplate<String, String> redisTemplate;

    public void storeToken(String username, String refreshToken) {
        redisTemplate.opsForValue().set(username, refreshToken, 7, TimeUnit.DAYS);
    }

    public boolean isTokenValid(String username, String token) {
        String stored = redisTemplate.opsForValue().get(username);
        return token.equals(stored);
    }
}

✅ OAuth2 통합 방식

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                    Authentication authentication) throws IOException {
    OAuth2User user = (OAuth2User) authentication.getPrincipal();
    String token = jwtUtil.generateAccessToken(user.getAttribute("email"));
    response.sendRedirect("https://your-frontend.com/oauth?token=" + token);
}

⚠️ JWT 잘못 사용하는 예시 (주의)

❌ 예: 비밀 키를 하드코딩하거나 공개 저장소에 커밋

private String secretKey = "123456"; // ❌ 보안 위협, 환경변수 또는 설정파일 사용해야 함

❌ 예: 민감 정보 페이로드에 저장

{
  "sub": "user123",
  "password": "plaintext-pass"  // ❌ 절대 저장 금지
}

✅ 해결: application.yml 또는 환경변수로 비밀 키 관리

jwt:
  secret: ${JWT_SECRET}

 


 

📌 요약 정리

  • JWT는 무상태 인증 구조로, 확장성과 분산 환경에 적합합니다.
  • Spring Security와 연동 시 필터 기반 인증 및 사용자 주입이 가능합니다.
  • Redis를 활용한 Refresh Token 저장, OAuth2 통합, 보안 실수 방지 등 다양한 실전 예제를 함께 다룹니다. 강화하려는 중급 개발자를 위한 실전 가이드입니다. 티스토리 등 블로그 플랫폼에 쉽게 게시할 수 있도록 마크다운으로 구성되어 있습니다.
반응형

관련글 더보기