ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Cursor + SpringBoot 개발설정4 + JWT
    Tool/VSCode&Cursor 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

     

     

    'Tool > VSCode&Cursor' 카테고리의 다른 글

    JWT란?  (2) 2024.12.13
    Cursor + SpringBoot 개발설정3 + openapi  (0) 2024.12.13
    Cursor + SpringBoot 개발설정2  (0) 2024.12.11
    Cursor Activity Bar 변경  (0) 2024.12.11
    Cursor + SpringBoot 개발설정1  (0) 2024.12.11

    댓글

Designed by Tistory.