1. 서론
스프링 부트(Spring Boot)는 Java 기반 웹 애플리케이션을 손쉽게 개발할 수 있도록 도와주는 프레임워크입니다. 최근 들어 웹 애플리케이션에 대한 수요가 급증하면서, 보안 및 인증에 대한 이해가 중요해졌습니다. 이 강좌에서는 JSON Web Token(JWT)을 사용하여 로그인 및 로그아웃 기능을 구현하고, 필터를 통해 JWT 토큰을 검증하는 방법을 알아보겠습니다. 이 과정에서 스프링 부트의 다양한 기능을 활용하여 더 안전하고 효율적인 백엔드 애플리케이션을 구축하는 법을 배우게 될 것입니다.
2. JWT란 무엇인가?
JSON Web Token(JWT)은 두 개의 엔티티 간에 정보를 안전하게 전송하기 위해 사용하는 JSON 기반의 오브젝트입니다. JWT는 사용자 인증뿐만 아니라, 클레임 기반의 정보 전송을 위한 표준으로 자리 잡고 있습니다. JWT는 다음과 같이 세 가지 구성 요소로 나뉩니다:
- 헤더(Header): JWT의 타입과 해싱 알고리즘을 정의합니다.
- 페이로드(Payload): 전송하려는 클레임 정보가 포함됩니다.
- 서명(Signature): 헤더와 페이로드를 결합한 후 비밀 키를 사용하여 생성됩니다. 이를 통해 JWT의 무결성을 확인할 수 있습니다.
3. 스프링 부트 프로젝트 설정하기
스프링 부트 프로젝트를 시작하려면 기본적인 설정부터 해야 합니다. 이러한 설정을 위해 스프링 이니셜라이저(Spring Initializr)를 이용할 수 있습니다. 프로젝트 생성 시 선택해야 할 사항은 다음과 같습니다:
- Project: Maven Project
- Language: Java
- Spring Boot: Stable version (Latest)
- Dependencies: Spring Web, Spring Security, Spring Data JPA, Spring Boot DevTools, H2 Database
프로젝트가 생성되면 IDE에서 열고, 필요한 의존성을 추가할 수 있습니다.
4. 엔티티 클래스 및 리포지토리 생성하기
우선 유저(User) 엔티티를 정의해야 합니다. 간단한 User 엔티티는 사용자 정보를 저장하는 클래스입니다. 이를 통해 우리는 데이터베이스와 상호작용할 수 있습니다.
package com.example.demo.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
// Getters and Setters
}
리포지토리는 엔티티와 데이터베이스의 CRUD 작업을 수행하는 데 필요합니다.
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
5. 서비스 클래스 생성하기
비즈니스 로직을 처리하는 서비스 클래스가 필요합니다. 이 클래스에서는 사용자 등록, 로그인 등의 기능을 구현합니다.
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
public User register(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
public User findUserByUsername(String username) {
return userRepository.findByUsername(username);
}
}
6. 보안 설정 및 JWT 생성하기
스프링 시큐리티를 사용하여 애플리케이션의 보안을 강화할 수 있습니다. 이를 위해 SecurityConfig 클래스를 생성하고, JWT 토큰을 생성하는 클래스를 추가해야 합니다.
package com.example.demo.config;
import com.example.demo.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
7. JWT 생성 및 검증 구현하기
로그인 시 사용자의 정보를 바탕으로 JWT를 생성하여 클라이언트에 전달합니다. 이를 위해 JWT 유틸리티 클래스를 생성하여 토큰 생성 및 검증 작업을 수행합니다.
package com.example.demo.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
private final String SECRET_KEY = "secret_key";
private final long EXPIRATION_TIME = 86400000; // 1 day
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public boolean validateToken(String token, String username) {
final String extractedUsername = extractUsername(token);
return (extractedUsername.equals(username) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getExpiration();
}
public String extractUsername(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getSubject();
}
}
8. 로그인 및 로그아웃 기능 구현하기
이제 사용자가 로그인하면 JWT를 발급받고, 로그아웃 시에는 클라이언트가 토큰을 삭제하는 방식으로 구현합니다. 아래는 로그인 API의 예시입니다.
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.security.JwtUtil;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody User user) {
User foundUser = userService.findUserByUsername(user.getUsername());
if (foundUser != null && passwordEncoder.matches(user.getPassword(), foundUser.getPassword())) {
return ResponseEntity.ok(jwtUtil.generateToken(foundUser.getUsername()));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
@PostMapping("/logout")
public ResponseEntity<String> logout() {
// 로그아웃 기능은 클라이언트 측에서 구현하도록 합니다.
return ResponseEntity.ok("Successfully logged out.");
}
}
9. JWT 필터 구현하기
JWT 필터를 구현하여 모든 요청에서 JWT를 검증하고, 인증을 처리하는 과정이 필요합니다. 이를 위해 JwtAuthenticationFilter 클래스를 생성합니다.
package com.example.demo.security;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
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 authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
} catch (ExpiredJwtException e) {
System.out.println("JWT is expired");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
10. 결론
이번 강좌에서는 스프링 부트를 이용하여 JWT를 통한 로그인 및 로그아웃 기능을 구현하는 방법을 배웠습니다. 우리는 JWT 토큰을 생성하고 검증하는 방법, 그리고 스프링 시큐리티를 활용하여 애플리케이션의 보안을 강화하는 방법에 대해서도 알아보았습니다. 이 과정을 통해 백엔드 애플리케이션의 인증 및 권한 관리를 더 안전하게 처리할 수 있는 기반을 마련할 수 있습니다. 더불어, 스프링 부트의 다양한 기능을 활용하여 효율적이고 안전한 웹 애플리케이션 개발을 실현할 수 있을 것입니다.