프로젝트 (Java)/예약마켓

[프로젝트] 5. JWT(Jason Web Tokens)와 Spring Security 적용

hihyuk 2024. 1. 25. 09:47

프로젝트에 JWT(Jason Web Tokens)와 Spring Security를 적용하는 과정은 보안성 높은 웹 애플리케이션을 구축하는 데 필수적인 단계입니다.

 

Spring Boot 버전 다운그레이드 및 Java 버전 조정

JWT 및 Spring Security 적용을 위해 Spring Boot와 Java 버전을 프로젝트 요구사항에 맞게 조정합니다. 이 과정은 종속성 충돌을 방지하고, 보안 라이브러리의 호환성을 보장하기 위해 필요합니다.

 

라이브러리 추가

build.gradle 파일에 다음 라이브러리를 추가하여 JPA, JWT, Spring Security를 프로젝트에 포함시킵니다.

 

application.yml에 추가

application.yml 파일에 JWT 및 데이터베이스 관련 설정을 추가합니다. 

 

WebSecurityConfig 클래스 생성

Spring Security 설정을 위한 WebSecurityConfig 클래스를 생성합니다. 이 클래스에서는 HTTP 요청에 대한 보안 설정을 정의합니다.

package com.myshop.global.config;

import com.myshop.global.filter.JwtTokenFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
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.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;

@EnableWebSecurity
@RequiredArgsConstructor
@Order(300)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenFilter jwtTokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable().cors()
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/api/**").permitAll()
                .antMatchers(HttpMethod.POST, "/api/**").permitAll()
                .antMatchers(
                        "/",
                        "/ping",
                        "/profile",
                        "/webTest",
                        "/active",
                        "/favicon.ico",
                        "/common/**",
                        "/open-api/**",
                        "/actuator/**",
                        "/swagger-ui/**",
                        "/v3/api-docs/**",
                        "/swagger-ui.html")
                .permitAll()
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .antMatcher("/**")
                .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

 

 

JWT 관련 클래스 생성

JWT 생성 및 검증을 위한 TokenManager 클래스와, 토큰 관련 정보를 담을 TokenDto, TokenResponseDto 클래스를 생성합니다. 또한, TokenContextTokenContextHolder를 통해 요청마다 사용자 정보를 관리합니다.

 

TokenManager.Class

package com.myshop.global.token;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.myshop.dto.TokenDto;
import com.myshop.dto.TokenResponseDto;
import com.myshop.global.context.TokenContext;
import com.myshop.global.context.TokenContextHolder;
import com.myshop.global.exception.UsernameFromTokenException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.Date;

@Component
@RequiredArgsConstructor
public class TokenManager {
    @Value("${jwt.secret}")
    private String jwtSecret;
    @Value("${jwt.issuer}")
    private String jwtIssuer;

    public final static Long LOCAL_ACCESS_TOKEN_TIME_OUT = 9999999L * 60 * 60;
    public final static Long ACCESS_TOKEN_TIME_OUT = 1000L * 60 * 60; // 1시간

    public TokenResponseDto generateToken(TokenDto tokenDto) {
        String token = newToken(tokenDto, LOCAL_ACCESS_TOKEN_TIME_OUT);
        return new TokenResponseDto(token);
    }

    private String newToken(TokenDto token, Long expireTime) {
        return JWT.create()
                .withClaim("user_id", token.getUserId())
                .withIssuedAt(new Date())
                .withIssuer(jwtIssuer)
                .withExpiresAt(new Date(System.currentTimeMillis() + expireTime))
                .sign(Algorithm.HMAC512(jwtSecret));
    }

    /**
     * Jwt Token 검증한다.
     * 토큰 만료시 우선 true로 리턴하고 이후 로직에서 RefreshToken을 검증한다.
     *
     * @param token
     */
    public void validateToken(String token) {
        if (ObjectUtils.isEmpty(token)) {
            throw new UsernameFromTokenException("JWT Empty. Please check header.");
        }
        JWTVerifier verifier = JWT.require(Algorithm.HMAC512(jwtSecret))
                .withIssuer(jwtIssuer)
                .build();

        DecodedJWT jwt = verifier.verify(token);

        TokenContext context = TokenContextHolder.getContext();

        Claim claim = jwt.getClaim("user_id");
        context.setUserId(claim.asLong());

        TokenContextHolder.setContext(context);
    }
}

 

TokenDto

package com.myshop.dto;

import lombok.Builder;
import lombok.Getter;
@Getter
public class TokenDto {
    private Long userId;

    @Builder
    public TokenDto(Long userId) {
        this.userId = userId;
    }
}

 

TokenContext

package com.myshop.global.context;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class TokenContext {
    private Long userId;
}

 

TokenContextHolder

package com.myshop.global.context;

public class TokenContextHolder  {
    private static final ThreadLocal<TokenContext> contextHolder = new ThreadLocal<>();

    /**
     * @name : setContext
     * @param : context
     * @description : TokenContext 셋팅한다.
     */
    public static void setContext(TokenContext context) {
        contextHolder.set(context);
    }

    /**
     * @name : getContext/product/supply/price/target/item/check
     * @return : TokenContext
     * @description : TokenContext를 얻는다.
     */
    public static TokenContext getContext() {
        TokenContext context = contextHolder.get();
        if (context == null) {
            context = new TokenContext();
            contextHolder.set(context);
        }
        return context;
    }

    /**
     * @name : clearContext
     * @description : context를 clear 한다.
     */
    public static void clearContext() {
        contextHolder.remove();
    }
}

 

TokenResponseDto

package com.myshop.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class TokenResponseDto {
    private String token;
}

 

 

토큰 생성 및 검증

사용자의 로그인 또는 회원가입 요청이 성공하면, TokenManager를 사용하여 JWT를 생성하고 응답으로 반환합니다. 모든 요청에 대해 JwtTokenFilter를 통해 토큰의 유효성을 검증하고, 필요한 사용자 정보를 TokenContext에 저장합니다.

 

결론

JWT와 Spring Security를 프로젝트에 적용함으로써, 안전한 인증 및 권한 부여 메커니즘을 구현할 수 있습니다. 이 과정은 복잡할 수 있지만, 프로젝트의 보안을 강화하고, 사용자 데이터를 보호하는 데 필수적인 단계입니다.