Spring Boot와 Flutter로 JWT 인증 구현하며 배운 것들

Spring Security 없이 jjwt 라이브러리만으로 JWT 인증을 구현하고, Flutter 앱과 연동하는 방법을 단계별로 설명합니다. 토큰 생성부터 자동 헤더 주입까지 실무에서 바로 사용할 수 있는 패턴을 소개합니다.
ImRieul's avatar
Aug 29, 2025
Spring Boot와 Flutter로 JWT 인증 구현하며 배운 것들
안녕하세요, 임리을입니다.
JWT 자체는 개념적으로 간단하지만, 실제로 Spring Boot와 Flutter를 연동해서 구현하다 보니 생각지 못한 부분들이 많았습니다.
오늘은 제가 jjwt 라이브러리로 JWT를 구현하면서 배운 점들을 공유해보려고 합니다.
 
 

왜 Spring Security 대신 jjwt를 선택했나?


대부분의 튜토리얼에서는 Spring Security와 함께 JWT를 구현하는데, 저는 의도적으로 jjwt 라이브러리만 사용했습니다.
이유는 명확했습니다:
  • JWT의 동작 원리를 직접 이해하고 싶었고
  • 필요한 API에만 선택적으로 인증을 적용하고 싶었고
  • Spring Security의 복잡한 설정보다는 학습용으로 단순하게 시작하고 싶었습니다
 
 

JWT 토큰 생성: 핵심만 간단하게


jjwt로 토큰을 만드는 건 정말 직관적입니다:
@Component public class JwtUtil { @Value("${jwt.secret}") private String secretKey; public String generateToken(String email) { return Jwts.builder() .subject(email) // 사용자 식별 정보 .issuedAt(new Date()) // 발급 시간 .expiration(new Date(System.currentTimeMillis() + 86400000)) // 24시간 후 만료 .signWith(getSigningKey()) .compact(); } public boolean validateToken(String token) { try { Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token); return true; } catch (Exception e) { return false; } } }
핵심은 subject에 사용자 정보를 넣고, secret key로 서명하는 것뿐입니다.
 
 

특정 API만 보호하는 필터 전략


모든 API에 인증이 필요한 건 아닙니다. 로그인 API는 당연히 토큰 없이 접근해야 하고 있습니다.
Spring Security 없이 이걸 해결하려면 FilterRegistrationBean을 사용해야 했습니다:
@Bean public FilterRegistrationBean<JwtAuthenticationFilter> jwtFilterRegistration() { FilterRegistrationBean<JwtAuthenticationFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(jwtAuthenticationFilter); registration.addUrlPatterns("/feeds/*"); // 보호할 API만 지정 registration.setOrder(1); return registration; }
이제 /auth/login은 필터를 거치지 않고, /feeds만 JWT 검증을 거칩니다.
 
 

Flutter에서 JWT 자동 주입하기


Flutter에서는 매 요청마다 토큰을 헤더에 넣어야 하는데, HttpClient의 싱글턴 패턴으로 해결했습니다:
class HttpClient { static Dio get dio { if (_dio == null) { _dio = Dio(); _setupDio(); } return _dio!; } static void _setupDio() { _dio!.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { final prefs = await SharedPreferences.getInstance(); final jwt = prefs.getString('jwt_token'); if (jwt != null) { options.headers['Authorization'] = 'Bearer $jwt'; } handler.next(options); }, ), ); } }
이제 HttpClient.dio.get('/feeds')를 호출하면 자동으로 JWT가 헤더에 추가됩니다.
 
 

로그인부터 인증까지 전체 플로우


실제 사용할 때는 이런 식으로 동작합니다:

1. 로그인으로 토큰 받기

Future<void> login(String email, String password) async { final response = await HttpClient.dio.post('/auth/login', data: { 'email': email, 'password': password, }); // SharedPreferences에 토큰 저장 final prefs = await SharedPreferences.getInstance(); await prefs.setString('jwt_token', response.data['token']); }

2. 보호된 API 호출

Future<List<Feed>> getFeeds() async { // JWT가 자동으로 헤더에 추가됨 final response = await HttpClient.dio.get('/feeds'); return response.data.map<Feed>((json) => Feed.fromJson(json)).toList(); }

3. 로그아웃시 토큰 삭제

Future<void> logout() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove('jwt_token'); }
 
 

실제로 써보니 느낀 jjwt의 장단점


장점:
  • JWT 동작 원리를 직접 이해할 수 있음
  • 디버깅할 때 토큰 생성/검증 과정을 명확히 볼 수 있음
  • 필요한 기능만 구현해서 가벼움
단점:
  • Refresh Token 같은 고급 기능은 직접 구현해야 함
  • 보안 관련 기능들을 하나씩 챙겨야 함
 
 

마무리: 기본기가 중요하다


JWT를 jjwt로 직접 구현해보니 토큰 기반 인증이 어떻게 동작하는지 확실히 알게 됐습니다.
나중에 더 복잡한 요구사항이 생기면 Spring Security로 전환할 수도 있지만, 기본 원리를 이해하고 있으니까 그때도 어려워하지 않을 것 같습니다.
특히 Flutter와 연동하면서 싱글턴 패턴으로 HTTP 클라이언트를 관리하고, Interceptor로 JWT를 자동 주입하는 패턴은 다른 프로젝트에서도 계속 쓸 것 같습니다.
감사합니다.
Share article

Rieul's Index