JWT로 로그인/로그아웃 구현 및 토큰 API 구현하기
최근 웹 애플리케이션의 아키텍처는 점점 더 복잡해지고 있으며, 특히 보안 문제는 모든 개발자에게 중요한 이슈입니다. 본 강좌에서는 스프링 부트(Spring Boot)를 사용하여 JWT(JSON Web Token)를 기반으로 한 로그인 및 로그아웃 기능과 토큰 API를 구현하는 방법을 자세히 살펴보겠습니다. 이 강좌는 기본적인 스프링 부트 개발 경험이 있는 개발자를 대상으로 하며, JWT에 대한 이해를 포함하고 있습니다.
목차
1. JWT란 무엇인가?
JWT는 JSON Web Token의 약자로, 사용자 인증 정보를 안전하게 전송하기 위한 개방형 표준(RFC 7519)입니다. JWT는 자주 사용되는 인증 방식 중 하나로, 클라이언트와 서버 간의 상태를 유지할 필요 없이 사용자가 로그인했음을 증명하는 데 사용됩니다. JWT는 세 부분으로 구성되어 있습니다:
- 헤더(Header): 토큰의 유형과 서명 알고리즘을 지정합니다.
- 페이로드(Payload): 사용자의 정보 및 클레임(claim)을 포함합니다.
- 서명(Signature): 헤더와 페이로드를 기반으로 생성된 서명입니다.
JWT의 가장 큰 장점 중 하나는 Stateless 방식으로 인증을 처리할 수 있다는 점입니다. 이는 서버가 세션 정보를 저장하지 않기 때문에 서버의 부담을 줄이고, 스케일 아웃이 용이하다는 것을 의미합니다.
2. 프로젝트 설정
프로젝트를 시작하기 전에 스프링 부트 프로젝트를 생성해야 합니다. Spring Initializr를 사용하여 간단하게 프로젝트를 설정할 수 있습니다. 다음 의존성을 추가합니다:
- Spring Web
- Spring Security
- Spring Data JPA
- H2 Database (또는 원하는 데이터베이스)
- jjwt (Java JWT 라이브러리)
프로젝트를 생성한 후, 필요한 라이브러리를 `pom.xml`에 추가합니다:
io.jsonwebtoken
jjwt
0.9.1
3. 사용자 인증 및 JWT 발급
사용자 인증을 위해 User 엔티티와 UserRepository를 생성합니다. 그 후 서비스 계층을 구성하고 JWT를 생성하는 로직을 구현합니다.
// User 엔티티 예시
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
// getters and setters
}
// UserRepository 예시
public interface UserRepository extends JpaRepository {
Optional findByUsername(String username);
}
이제 JWT를 생성하는 기능을 추가합니다:
// JwtUtil.java
@Component
public class JwtUtil {
private String secretKey = "secret"; // 실무에서는 환경변수나 별도의 파일에서 관리
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10시간
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
}
3.1 로그인 API 구현
이제 로그인 요청을 처리할 Controller를 구현합니다:
// AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserRepository userRepository;
@PostMapping("/login")
public ResponseEntity login(@RequestBody User user) {
authenticate(user.getUsername(), user.getPassword());
String token = jwtUtil.generateToken(user.getUsername());
return ResponseEntity.ok(token);
}
private void authenticate(String username, String password) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
4. 토큰 검증 및 인가
JWT를 사용하여 인증된 요청을 처리하려면, 필터를 구현하여 요청의 헤더에 있는 토큰을 검증해야 합니다.
// JwtRequestFilter.java
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
5. 로그아웃 기능 구현
JWT는 상태를 유지하지 않기 때문에 로그아웃 기능을 구현할 때는 클라이언트에서 토큰을 삭제하면 됩니다. 하지만, 서버에서 블랙리스트를 관리하는 방법으로 로그아웃 기능을 구현할 수도 있습니다.
// 로그아웃 예시
@PostMapping("/logout")
public ResponseEntity logout(HttpServletRequest request) {
String token = request.getHeader("Authorization").substring(7);
// 여기에 블랙리스트에 추가하는 로직 구현
return ResponseEntity.ok("Logout successful");
}
6. API 테스트
Postman과 같은 도구를 사용하여 API를 테스트할 수 있습니다. 로그인 요청 후, 서버에서 발급한 JWT를 사용하여 보호된 리소스에 접근할 수 있는지 확인합니다.
7. 결론
이 강좌에서는 스프링 부트를 사용하여 JWT 기반 인증 시스템을 구현하는 방법에 대해 알아보았습니다. 시스템의 요구사항에 따라 다양한 방식으로 JWT를 활용할 수 있으며, 보안은 항상 중요한 고려사항임을 명심해야 합니다. 앞으로 더 발전된 기능을 추가하여 시스템을 확장해 나가시길 바랍니다.