Tool/VSCode&Cursor

Cursor + SpringBoot 개발설정4 + JWT

건담아빠 2024. 12. 13. 15:38

SpringSecurity를 설정하면서 인증은 JWT을 사용하며 accessToken을 발급 받고 refreshToken으로 재발급 받는 부분을 작업해보자.

 

개발 환경 설정

의존성 추가

  • JWT
  • Spring Security
  • JPA
  • H2
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// H2 (테스트용 인메모리 DB)
runtimeOnly 'com.h2database:h2'

 

환경설정

  • application.yml
spring:
  application:
    name: spring-boot-api

  # H2 데이터베이스 설정
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: update
    show-sql: true

  # H2 콘솔 활성화
  h2:
    console:
      enabled: true
      path: /h2-console

jwt:
  # secret: your-256-bit-secret-key-here
  secret: 12345678901234567890123456789012212313223121321312dsfasdf
  access-token-validity: 3600000 # 1시간
  refresh-token-validity: 604800000 # 7일

...

 

SpringSecurity 및 JWT 인증 추가

Auth Controller 

로그인 후 accessToken을 발급하고 refresh토큰으로 갱신하는 컨트롤러 추가

  • AuthController
package com.test.spring_boot_demo.controller.v1;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import com.test.spring_boot_demo.constants.JwtConstants;
import com.test.spring_boot_demo.core.utils.JwtTokenUtil;
import com.test.spring_boot_demo.dto.JwtResponse;
import com.test.spring_boot_demo.dto.LoginRequest;

@RestController
@RequestMapping("/v1/api/auth")
public class AuthController {

  @Autowired
  private JwtTokenUtil jwtTokenUtil;

  @Autowired
  private AuthenticationManager authenticationManager;

  @PostMapping("/login")
  public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
    Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(
            loginRequest.getUsername(),
            loginRequest.getPassword()));

    UserDetails userDetails = (UserDetails) authentication.getPrincipal();

    String accessToken = jwtTokenUtil.generateAccessToken(userDetails.getUsername());
    String refreshToken = jwtTokenUtil.generateRefreshToken(userDetails.getUsername());

    return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken));
  }

  @PostMapping("/refresh")
  public ResponseEntity<?> refreshToken(@RequestHeader(JwtConstants.REFRESH_TOKEN_HEADER) String refreshToken) {
    if (jwtTokenUtil.validateToken(refreshToken)) {
      String username = jwtTokenUtil.getUsernameFromToken(refreshToken);
      String newAccessToken = jwtTokenUtil.generateAccessToken(username);

      return ResponseEntity.ok(new JwtResponse(newAccessToken, refreshToken));
    }
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
  }
}

 

SpringSecurity 및 Filter 설정

  • SecurityConfig
package com.test.spring_boot_demo.core.config;

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.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Autowired
  private JwtAuthenticationFilter jwtAuthenticationFilter;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/v1/api/auth/**").permitAll()
            .requestMatchers("/swagger-ui/**").permitAll()
            .requestMatchers("/v3/api-docs/**").permitAll()
            .requestMatchers("/h2-console/**").permitAll() // H2 콘솔 접근 허용
            .anyRequest().authenticated())
        .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
        .build();
  }

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

  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
    return authConfig.getAuthenticationManager();
  }
}

 

  • CustomUserDetailsService
package com.test.spring_boot_demo.core.config;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // For testing purposes, create a hardcoded user
    if ("admin".equals(username)) {
      // UserEntity userEntity = userRepository.findByUsername(username);
      return User.builder()
          .username("admin")
          // This is "password" encoded with BCrypt
          .password("$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
          .roles("ADMIN")
          .build();
    }

    throw new UsernameNotFoundException("User not found: " + username);
  }
}

 

  • JwtAuthenticationFilter
package com.test.spring_boot_demo.core.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import org.springframework.lang.NonNull;

import com.test.spring_boot_demo.constants.JwtConstants;
import com.test.spring_boot_demo.core.utils.JwtTokenUtil;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

  @Autowired
  private JwtTokenUtil jwtTokenUtil;

  @Autowired
  private UserDetailsService userDetailsService;

  @Override
  protected void doFilterInternal(@NonNull HttpServletRequest request,
      @NonNull HttpServletResponse response,
      @NonNull FilterChain filterChain) throws ServletException, IOException {
    try {
      String authHeader = request.getHeader(JwtConstants.HEADER_STRING);

      if (authHeader != null && authHeader.startsWith(JwtConstants.TOKEN_PREFIX)) {
        String token = authHeader.substring(7);

        try {
          if (jwtTokenUtil.validateToken(token)) {
            String username = jwtTokenUtil.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                userDetails,
                null,
                userDetails.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authentication);
          }
        } catch (io.jsonwebtoken.io.DecodingException e) {
          System.err.println("JWT Token 디코딩 실패: " + e.getMessage());
          System.err.println("Token: " + token);
        } catch (Exception e) {
          System.err.println("JWT Token 처리 중 오류 발생: " + e.getMessage());
        }
      }

      filterChain.doFilter(request, response);
    } catch (Exception e) {
      System.err.println("Filter 처리 중 오류 발생: " + e.getMessage());
      throw e;
    }
  }
}


Utils

  • JwtTokenUtil
package com.test.spring_boot_demo.core.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.function.Function;

@Component
public class JwtTokenUtil {
  @Value("${jwt.secret}")
  private String secret;

  @Value("${jwt.access-token-validity}")
  private long accessTokenValidity;

  @Value("${jwt.refresh-token-validity}")
  private long refreshTokenValidity;

  public String generateAccessToken(String username) {
    return generateToken(username, accessTokenValidity);
  }

  public String generateRefreshToken(String username) {
    return generateToken(username, refreshTokenValidity);
  }

  private String generateToken(String username, long validity) {
    return Jwts.builder()
        .setSubject(username)
        .setIssuedAt(new Date(System.currentTimeMillis()))
        .setExpiration(new Date(System.currentTimeMillis() + validity))
        .signWith(getSigningKey(), SignatureAlgorithm.HS256)
        .compact();
  }

  public String getUsernameFromToken(String token) {
    return getClaimFromToken(token, Claims::getSubject);
  }

  public boolean validateToken(String token) {
    try {
      Jwts.parserBuilder()
          .setSigningKey(getSigningKey())
          .build()
          .parseClaimsJws(token);
      return true;
    } catch (JwtException | IllegalArgumentException e) {
      return false;
    }
  }

  private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = getAllClaimsFromToken(token);
    return claimsResolver.apply(claims);
  }

  private Claims getAllClaimsFromToken(String token) {
    return Jwts.parserBuilder()
        .setSigningKey(getSigningKey())
        .build()
        .parseClaimsJws(token)
        .getBody();
  }

  private Key getSigningKey() {
    byte[] keyBytes = Decoders.BASE64.decode(secret);
    return Keys.hmacShaKeyFor(keyBytes);
  }
}

 

Properties

  • JwtProperties
package com.test.spring_boot_demo.core.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
  private String secret;
  private long accessTokenValidity;
  private long refreshTokenValidity;
}

 

Constants

  • JwtConstants
package com.test.spring_boot_demo.constants;

public class JwtConstants {
  public static final String TOKEN_PREFIX = "Bearer ";
  public static final String HEADER_STRING = "Authorization";
  public static final String REFRESH_TOKEN_HEADER = "Refresh-Token";
}

 

 

Entity

  • UserEntity
package com.test.spring_boot_demo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(unique = true)
  private String username;

  private String password;

  private String role;
}

 

Dto

  • JwtResponse
package com.test.spring_boot_demo.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class JwtResponse {
  private String accessToken;
  private String refreshToken;
}

 

  • LoginRequest
package com.test.spring_boot_demo.dto;

import lombok.Data;

@Data
public class LoginRequest {
  private String username;
  private String password;
}

 

깃허브

https://github.com/dchkang83/react-and-java/tree/main/spring-boot-api