스프링 부트 백엔드 개발 강좌, JWT로 로그인 로그아웃 구현, 토큰 제공자 추가하기

1. 서론

최근 마이크로서비스 아키텍처와 RESTful API의 발전으로 인해 백엔드 개발의 중요성이 더욱 부각되고 있습니다. 오늘 강좌에서는 스프링 부트를 이용한 백엔드 개발을 통해 JSON Web Token(JWT)을 사용하여 사용자 인증을 구현하는 방법에 대해 알아보겠습니다. 이 강좌는 로그인 및 로그아웃 기능을 포함한 JWT 기반의 토큰 제공자를 추가하는 것에 중점을 두고 있습니다.

2. 스프링 부트란?

스프링 부트는 스프링 프레임워크를 기반으로 하여 빠르고 쉽게 스프링 애플리케이션을 개발할 수 있도록 돕는 도구입니다. 복잡한 XML 설정 없이 데모 및 실제 애플리케이션을 손쉽게 개발할 수 있게 해줍니다. 또한, 스프링 부트는 내장형 서버를 지원하여 외부 서버에 배포하지 않고도 로컬에서 애플리케이션을 실행할 수 있습니다.

3. JWT의 이해

JSON Web Token(JWT)은 데이터 전송을 위한 개방형 표준으로, 클라이언트와 서버 간에 안전하게 정보를 전송하는 데 사용됩니다. JWT는 주로 인증 및 정보 교환에 사용됩니다. JWT는 세 부분으로 구성됩니다:

  • Header (헤더): 토큰의 유형과 암호화 알고리즘을 정의합니다.
  • Payload (페이로드): 사용자 정보를 저장하는 부분으로, 사용자 ID와 같은 정보가 포함됩니다.
  • Signature (서명): 헤더와 페이로드를 조합하여 비밀 키로 서명한 부분입니다.

4. 스프링 부트 프로젝트 설정

우선, 스프링 부트 프로젝트를 설정해야 합니다. Spring Initializr 웹사이트를 방문하여 새 프로젝트를 생성합니다. 다음의 종속성을 추가해야 합니다:

  • Spring Web
  • Spring Security
  • Spring Data JPA
  • H2 Database (또는 원하는 데이터베이스)
  • jjwt (JWT 사용을 위한 라이브러리)

5. 사용자의 엔티티 클래스 생성

사용자 정보를 저장하기 위한 엔티티 클래스를 생성합니다. 다음은 User 엔티티 클래스의 예입니다:


@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;

    private String password;

    // Getter and Setter methods
}

            

6. JWT 유틸리티 클래스 작성

JWT를 생성하고 검증하기 위한 유틸리티 클래스를 작성합니다.


@Component
public class JwtUtil {
    private String secretKey = "mySecretKey"; // 비밀 키

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) // 만료 시간 설정
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    public boolean validateToken(String token, String username) {
        final String extractedUsername = extractUsername(token);
        return (extractedUsername.equals(username) && !isTokenExpired(token));
    }

    public String extractUsername(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    private boolean isTokenExpired(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getExpiration().before(new Date());
    }
}

            

7. 스프링 시큐리티 설정

Spring Security를 설정하여 JWT 기반의 인증 프로세스를 구현합니다. 먼저, SecurityFilterChain을 설정합니다.


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests().antMatchers("/authenticate").permitAll()
            .anyRequest().authenticated();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password(passwordEncoder().encode("password")).roles("USER");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

            

8. JWT 요청 필터 작성

사용자 요청에서 JWT를 필터링하여 인증 여부를 확인하는 JWT Request Filter를 작성합니다.


@Component
public class JwtRequestFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;

    @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) {
            // 명시된 사용자 인증 처리
        }
        chain.doFilter(request, response);
    }
}

            

9. 로그인 및 토큰 생성 컨트롤러 작성

사용자의 로그인 요청을 처리하고, JWT를 생성하는 컨트롤러를 작성합니다.


@RestController
public class AuthController {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/authenticate")
    public ResponseEntity authenticate(@RequestBody AuthRequest authRequest) throws Exception {
        try {
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()));
        } catch (BadCredentialsException e) {
            throw new Exception("잘못된 사용자 이름 또는 비밀번호", e);
        }

        final String jwt = jwtUtil.generateToken(authRequest.getUsername());
        return ResponseEntity.ok(new AuthResponse(jwt));
    }
}

            

10. 로그아웃 관리

JWT은 클라이언트 측에서 관리되기 때문에 로그아웃 기능은 클라이언트에서 토큰을 삭제하여 단순히 수행할 수 있습니다. 하지만, 서버 측에서 로그아웃을 구현하는 방법도 있습니다. 예를 들어, 블랙리스트를 유지하여 만료되기 전에 로그아웃된 토큰을 차단할 수 있습니다.

11. 미들웨어 구성

미들웨어를 사용하여 요청을 전처리하고 후처리하는 기능을 추가할 수 있습니다. 예를 들어, 모든 요청에 대해 JWT를 검사하거나, 특정 엔드포인트에 대한 접근을 제한할 수 있습니다.

12. 테스트와 디버깅

상당한 수준의 테스트를 적용하여 코드의 품질을 보장해야 합니다. 각 서비스, 컨트롤러 및 리포지토리에 대한 단위 테스트를 작성합니다. 스프링 부트는 Junit과 Mockito를 통해 효과적인 테스트 환경을 제공합니다.

13. 결론

이 강좌에서는 스프링 부트와 JWT를 사용하는 로그인 및 로그아웃 구현 방법에 대해 자세히 살펴보았습니다. JWT는 클라이언트와 서버 간의 통신을 안전하게 처리하는 좋은 방법이며, 이를 통해 웹 애플리케이션의 보안을 한층 강화할 수 있습니다. 이러한 방식으로 백엔드 개발에 대한 이해도를 높이고, 실제 프로젝트에서는 이러한 구현을 통해 사용자 인증 기능을 보다 효율적으로 관리할 수 있을 것입니다.