🎯 개요
Spring Security + JWT 조합으로 기본 인증 구조를 만들었다면,
실무에서는 다음 기능들을 반드시 고려해야 합니다:
- Access Token 만료 이후 자동 로그아웃 방지 (Refresh Token)
- 사용자 비밀번호를 안전하게 암호화 (BCrypt)
- 인증/인가 실패 시 클라이언트가 파싱 가능한 JSON 형태로 응답 처리
이번 글에서는 위 3가지 기능을 하나씩 직접 구현해보며,
JWT 인증 시스템을 실무 수준으로 확장해보았습니다.
✔ 주요 기능 요약
- ✔ 로그인 시 Access + Refresh Token 동시 발급
- ✔ Refresh Token 유효 시 Access Token 재발급
- ✔ 회원가입 시 비밀번호 암호화 저장 (BCryptPasswordEncoder)
- ✔ 인증 실패(401), 인가 실패(403) 시 JSON 응답 메시지 제공
💻 예제 코드
1️⃣ Refresh Token 발급 및 재발급
- 로그인 시 Access + Refresh 동시 발급
- /reissue API로 Access Token 재발급 처리
@PostMapping("/login")
public Map<String, String> login(@RequestBody AuthRequest req) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword()));
String accessToken = jwtTokenProvider.generateAccessToken(req.getUsername(), "ROLE_USER");
String refreshToken = jwtTokenProvider.generateRefreshToken(req.getUsername());
refreshTokenStore.save(req.getUsername(), refreshToken);
return Map.of("accessToken", accessToken, "refreshToken", refreshToken);
}
@PostMapping("/reissue")
public Map<String, String> reissue(@RequestHeader("Refresh-Token") String token) {
String username = jwtTokenProvider.getUsername(token);
if (!refreshTokenStore.validate(username, token)) throw new RuntimeException("Refresh 불일치");
String newAccess = jwtTokenProvider.generateAccessToken(username, "ROLE_USER");
return Map.of("accessToken", newAccess);
}
2️⃣ 비밀번호 암호화 (BCrypt)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@PostMapping("/register")
public String register(@RequestBody AuthRequest req) {
User user = User.builder()
.username(req.getUsername())
.password(passwordEncoder.encode(req.getPassword()))
.role("ROLE_USER")
.build();
userRepository.save(user);
return "회원가입 완료";
}
3️⃣ 예외 커스터마이징 (JSON 응답)
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
public void commence(HttpServletRequest req, HttpServletResponse res, AuthenticationException ex) throws IOException {
res.setStatus(401);
res.setContentType("application/json");
res.getWriter().write("{ \"error\": \"인증이 필요한 요청입니다.\" }");
}
}
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
public void handle(HttpServletRequest req, HttpServletResponse res, AccessDeniedException ex) throws IOException {
res.setStatus(403);
res.setContentType("application/json");
res.getWriter().write("{ \"error\": \"접근 권한이 없습니다.\" }");
}
}
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
)
✍ 회고 및 팁
Refresh Token 발급 구조를 직접 구현해보며,
실제 사용자 경험에서 로그인 유지 흐름이 왜 중요한지 체감할 수 있었습니다.
또한, 비밀번호 암호화를 통해 보안의 기본을 다졌고,
예외 커스터마이징으로 API 사용성을 크게 개선할 수 있었습니다.
특히 실무에서는 다음 사항도 꼭 고려해야 합니다:
- Redis 기반 Refresh Token 저장소
- Refresh Token Rotation 전략
- 로그아웃 시 Refresh Token 무효화 처리
- FilterChain 순서 유의
📚 추가 자료
전체 예제 및 실습 코드는 👉 GitHub - mingstagram/jwt-demo 에 정리되어 있습니다.
'컴퓨터 과학 > Spring 기반 구조' 카테고리의 다른 글
💻 [Spring 기반 구조] 실무형 예외 처리 & Validation 구조 - 공통 응답 설계부터 커스텀 예외까지 (1) | 2025.05.15 |
---|---|
💻 [Spring 기반 구조] JWT 기반 인증 구조 이해하기 - 로그인부터 인가까지 흐름 잡기 (0) | 2025.05.14 |
💻 [Spring 기반 구조] 트랜잭션 관리 - @Transactional이 실제로 무슨 일을 할까? (0) | 2025.05.14 |
💻 [Spring 기반 구조] AOP - 관심사의 분리를 통해 핵심 로직을 보호하자 (0) | 2025.05.14 |
💻 [Spring 기반 구조] 빈 생명주기와 스코프 - 객체는 언제 생성되고, 어떻게 관리될까? (1) | 2025.05.14 |