
안녕하세요, 임리을입니다.
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