JWT를 사용한 Token 로그인

oceann·2023년 11월 14일

🚀Logic

아래 SecurityConfig에서 설명할 테지만 Filter는 사용자 인증/인가 과정인데, Spring Security에서 자동으로 거치는 UsernamePasswordAuthenticationFilter를 거칠 때 JwtAuthenticationFilter를 먼저 거쳐 인증된 사용자인지 확인하도록 한다.

🚀패키지 구조

🚀build.gradle에 dependencies 추가

dependencies {
	// Spring Security
	implementation("org.springframework.boot:spring-boot-starter-security")
	testImplementation("org.springframework.security:spring-security-test")
	
	// JWT
	implementation("io.jsonwebtoken:jjwt:0.9.1")
	// javax.xml.bind.DatatypeConverter 오류 해결
  	implementation("com.sun.xml.bind:jaxb-impl:4.0.1")
  	implementation("com.sun.xml.bind:jaxb-core:4.0.1")
  	implementation("javax.xml.bind:jaxb-api:2.4.0-b180830.0359")
}

🚀LoginDTO

  • 로그인 할 때는 userID, pwd만 필요
@Getter @Setter
@NoArgsConstructor
@ToString
public class LoginDTO {
    private String userID;
    private String pwd;
}

⭐JWT⭐

🚀JwtProvider

  • userRole 지정 전
@Component
@RequiredArgsConstructor
public class JwtProvider {
    private String secretKey = "ERA jwt secret key";
    private final long tokenValidTime = 60 * 60 * 1000L; // 유효 시간 60분
    private final UserDetailsService userDetailsService;

    // 객체 초기화 시 secretKey를 Base64로 encoding
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // JWT 생성
    public String createToken(String userID) { // List<String> roles
        Claims claims = Jwts.claims().setSubject(userID); // JWT payload에 저장되는 정보 단위
        // claims.put("roles", roles);
        Date now = new Date();

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + tokenValidTime))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    // 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserID(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }
    // token에서 UserID 뽑기
    public String getUserID(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }
    // 토큰 유효성, 만료 일자 확인
    public boolean validateToken(String jwt) {
        try {
            if (!jwt.substring(0, "BEARER ".length()).equalsIgnoreCase("BEARER ")) {
                return false;
            } else {
                jwt = jwt.split(" ")[1].trim();
            }

            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt);
            return !claims.getBody().getExpiration().before(new Date());
        } catch(Exception e) {
            return false;
        }
    }
    // request의 header에서 token 가져오기
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("Authorization");
    }
}

🚀SecurityConfig

  • SecurityFilterChain에 추가
  • csrf(AbstractHttpConfigurer::disable)
    token을 사용하면 서버에 인증 정보 보관 ✖️. (session 기반 인증은 STATEFUL하여 서버에 client의 이전 상태를 기록) 따라서 csrf를 disable로 설정하고, sessionManagementSTATELESS로 설정
  • addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class)
    사용자로부터 로그인 request가 들어오면 인증/인가 작업을 위해 filter를 거치는데, Spring Security에 포함되는 기존 filter에 앞서 jwt 관련하여 custom한 JwtAuthenticationFilter를 사용한다(아래 코드 있음). 이때, 앞에 오는 filter를 뒤에 오는 filter보다 먼저 적용하겠다는 뜻이다. 이 외에도 addFilter, addFilterAfter도 있음!
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtProvider jwtProvider;

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable) // token을 사용하므로 csrf disable
                .sessionManagement((sessionManagement) -> // Spring Security가 Session을 아예 배재(생성, 사용 X)
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

🚀JwtAuthenticationFilter

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtProvider jwtProvider;

    public JwtAuthenticationFilter(JwtProvider jwtProvider) {
        this.jwtProvider = jwtProvider;
    }

    // Http 요청이 들어오면 가장 먼저 거치는 filter
    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        // Header에서 Token 가져오기
        String token = jwtProvider.resolveToken(request);

        // 토큰이 유효하다면,
        if (token != null && jwtProvider.validateToken(token)) {
            // 토큰으로부터 인증 정보를 받아
            Authentication authentication = jwtProvider.getAuthentication(token);
            // SecurityContext 객체에 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response); // 다음 filter 실행
    }
}

🔥Issue

❗순환 참조 오류

  • userController
  • userService
  • securityConfig
  • jwtProvider
  • inMemoryUserDetailsManager
  • passwordEncoder → @Bean이라서 이미 생성되어 있는데 여기저기서 자꾸 불러서 문제였음..

💡해결

  • static으로 선언: 표준 인스턴스화된 객체 → 정적 메소드를 직접 호출
  • SecurityConfig.java
profile
🌈🌼🌸☀️

0개의 댓글