Spring ๐ŸŒฑ

JWT ์ธ์ฆโˆ™์ธ๊ฐ€ ๊ตฌํ˜„ํ•˜๊ธฐ

z.zzz 2024. 2. 11. 15:59

JWT๋ž€

JWT(JSON Web Token)๋ž€? | JWT ๊ตฌ์„ฑ์š”์†Œ
ํ† ํฐ์ด ํ•„์š”ํ•œ ์ด์œ ์™€ JWT ๋™์ž‘ ๋ฐฉ์‹


์ธ์ฆ, ์ธ๊ฐ€ ํ๋ฆ„(+ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ)


JwtProvider ๊ตฌํ˜„

JwtProvider๋Š” Jwt๋ฅผ ์ƒ์„ฑ, ํŒŒ์‹ฑ, ๊ฒ€์ฆํ•˜๋Š” ํด๋ž˜์Šค๋‹ค.
๊ตฌํ˜„์—” jjwt ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

jwt ๊ด€๋ จ ์ •๋ณด์ธ secret, expire-length๋ฅผ application.yml์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ–ˆ๋‹ค.

jwt:
  header: Authorization
  secret: (ํŠน์ • ๋ฌธ์ž์—ด์„ Base64๋กœ ์ธ์ฝ”๋”ฉํ•œ ๊ฐ’)
  access-token:
    expire-length: 3600
  refresh-token:
    expire-length: 86400
private final Key key;

@Value("${jwt.access-token.expire-length}")
private long accessTokenExpireLengthInSeconds;

@Value("${jwt.refresh-token.expire-length}")
private long refreshTokenExpireLengthInSeconds;

public JwtProvider(
    @Value("${jwt.secret}") String secret) {
    byte[] keyBytes = Decoders.BASE64.decode(secret);
    this.key = Keys.hmacShaKeyFor(keyBytes);
}

Jwts.builder๋กœ ํ† ํฐ์„ ์ƒ์„ฑํ•˜๊ณ  Jwts.parseBuilder์™€ parseClaimJws๋กœ claims๋ฅผ ํŒŒ์‹ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

public Jwt createJwt(Map<String, Object> claims) {
    String accessToken = createToken(claims, getExpireDateAccessToken());
    String refreshToken = createToken(new HashMap<>(), getExpireDateRefreshToken());
    return Jwt.builder()
        .accessToken(accessToken)
        .refreshToken(refreshToken)
        .build();
}

public String createToken(Map<String, Object> claims, Date expireDate) {
    return Jwts.builder()
        .setClaims(claims)
        .setExpiration(expireDate)
        .signWith(key, SignatureAlgorithm.HS256)
        .compact();
}

public Claims getClaims(String token) {
    return Jwts.parserBuilder()
        .setSigningKey(key)
        .build()
        .parseClaimsJws(token)
        .getBody();
}

Jwt ํด๋ž˜์Šค๋Š” access token๊ณผ refresh token ํ•„๋“œ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

//Jwt.java
@Getter
public class Jwt {

    private String accessToken;
    private String refreshToken;

    @Builder
    public Jwt(String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }
}

๋กœ๊ทธ์ธ ๊ตฌํ˜„

๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•˜๋ฉด ํ† ํฐ์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

//UserController.java
@PostMapping("/users/sign-in")
public ResponseEntity<ApiResponse<Jwt>> signIn(@Valid @RequestBody UserLoginDto loginDto) 
    throws JsonProcessingException {
    Jwt jwt = usersService.signIn(loginDto);
    return ResponseEntity.ok(success(SuccessCode.GET_SUCCESS, jwt));
}
//UserService.java
@Transactional
public Jwt signIn(UserLoginDto loginDto) throws JsonProcessingException {

    // db์— ์žˆ๋Š”(ํšŒ์›๊ฐ€์ž…ํ•œ) ์œ ์ €์ธ์ง€ ๊ฒ€์ฆ
    Users user = usersRepository.findByTel(loginDto.getTel())
        .orElseThrow(() -> new IllegalArgumentException("ํšŒ์›๊ฐ€์ž…ํ•˜์ง€ ์•Š์€ ์œ ์ €์ž…๋‹ˆ๋‹ค."));

    // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ
    if (!loginDto.getPassword().equals(user.getPassword())) {
        throw new IllegalArgumentException("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
    }

    // ํ† ํฐ ์ƒ์„ฑ
    Jwt jwt = authService.createJwt(user);

    // refreshToken ์ €์žฅ
    user.updateRefreshToken(jwt.getRefreshToken());

    return jwt;
}
//AuthService.java
public Jwt createJwt(Users user) throws JsonProcessingException {

    // claims ์ƒ์„ฑ
    Map<String, Object> claims = new HashMap<>();
    AuthenticateUser authenticateUser = new AuthenticateUser(user);
    String authenticateUserJson = objectMapper.writeValueAsString(authenticateUser);
    claims.put(AUTHENTICATE_USER, authenticateUserJson);

    // ํ† ํฐ ์ƒ์„ฑ
    return jwtProvider.createJwt(claims);
}

ํ•„ํ„ฐ๋ฅผ ํ†ตํ•œ ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๊ธฐ๋Šฅ ๊ตฌํ˜„

ํŽ˜์ด์ง€ ์ ‘๊ทผ ์š”์ฒญ ์‹œ, ํ† ํฐ์ด ์œ ํšจํ•œ ๊ฒฝ์šฐ๋งŒ ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•˜๋Š” ํ•„ํ„ฐ๋ฅผ ๊ตฌํ˜„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.


์ธ๊ฐ€ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์€ ์š”์ฒญ์— ๋Œ€ํ•ด์„  ํ† ํฐ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜์ง€ ์•Š๋„๋ก whiteListUris๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.


resolveToken์œผ๋กœ request header์˜ Authorization์— ๋‹ด๊ธด accessToken์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.


validateToken์œผ๋กœ accessToken์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
์œ ํšจํ•œ ํ† ํฐ์ด๋ฉด userId, role ์ •๋ณด๋ฅผ request์˜ attribute๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
request header์— ํ† ํฐ์ด ์—†๊ฑฐ๋‚˜, ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ด๋ฉด 401 UNAUTHORIZED ์‘๋‹ต์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค.

@RequiredArgsConstructor
@Component
public class JwtValidationFilter implements Filter {

    private final JwtProvider jwtProvider;
    private final ObjectMapper objectMapper;

    private final String[] whiteListUris
        = new String[] {"*/users*", "/auth/refresh/token", "*/h2-console*"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
        IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        // ํ† ํฐ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š” ๊ฒฝ์šฐ
        if (whiteListCheck(httpServletRequest.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        // ์š”์ฒญ ํ—ค๋”์— ํ† ํฐ์ด ์—†๋Š” ๊ฒฝ์šฐ
        if (!isContainToken(httpServletRequest)) {
            httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), "์ธ์ฆ ์˜ค๋ฅ˜");
            return;
        }

        String accessToken = jwtProvider.resolveToken(httpServletRequest);
        JwtExceptionType jwtException = jwtProvider.validateToken(accessToken);

        if (jwtException == JwtExceptionType.VALID_JWT_TOKEN) {
            request.setAttribute(AUTHENTICATE_USER, getAuthenticateUser(accessToken));
        } else if (jwtException == JwtExceptionType.EXPIRED_JWT_TOKEN) {
            httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), "ํ† ํฐ ๋งŒ๋ฃŒ");
            return;
        }

        chain.doFilter(request, response);
    }

    private boolean whiteListCheck(String uri) {
        return PatternMatchUtils.simpleMatch(whiteListUris, uri);
    }

    private boolean isContainToken(HttpServletRequest request) {
        String authorization = request.getHeader(AUTHORIZATION_HEADER);
        return authorization != null && authorization.startsWith(BEARER_TOKEN_PREFIX);
    }

    private AuthenticateUser getAuthenticateUser(String token) throws JsonProcessingException {
        Claims claims = jwtProvider.getClaims(token);
        String authenticateUserJson = claims.get(AUTHENTICATE_USER, String.class);
        return objectMapper.readValue(authenticateUserJson, AuthenticateUser.class);
    }
}
// JwtProvider.java
// Request Header์—์„œ token ๊ฐ’ ๊ฐ€์ ธ์˜ด
public String resolveToken(HttpServletRequest request) {
    String header = request.getHeader(AUTHORIZATION_HEADER);
    return header.split(" ")[1];
}

// jwt ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
public JwtExceptionType validateToken(String token) {
    try {
        getClaims(token);
        return JwtExceptionType.VALID_JWT_TOKEN;
    } catch (SignatureException exception) {
        log.info("JWT ์„œ๋ช…์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค");
        return JwtExceptionType.INVALID_JWT_SIGNATURE;
    } catch (MalformedJwtException exception) {
        log.info("JWT๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.");
        return JwtExceptionType.INVALID_JWT_TOKEN;
    } catch (ExpiredJwtException exception) {
        log.info("๋งŒ๋ฃŒ๋œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.");
        return JwtExceptionType.EXPIRED_JWT_TOKEN;
    } catch (UnsupportedJwtException exception) {
        log.info("์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์˜ ํ† ํฐ์ž…๋‹ˆ๋‹ค.");
        return JwtExceptionType.UNSUPPORTED_JWT_TOKEN;
    } catch (IllegalArgumentException exception) {
        log.info("JWT Claim์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.");
        return JwtExceptionType.EMPTY_JWT;
    }
}

์ด๋ ‡๊ฒŒ ์ƒ์„ฑํ•œ ํ•„ํ„ฐ๋Š” FilterRegistrationBean์„ ํ†ตํ•ด ๋“ฑ๋กํ•ด์•ผ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
FilterRegisterationBean๋ฅผ ๋“ฑ๋กํ•˜๊ธฐ ์œ„ํ•œ WebConfig๋ผ๋Š” Configuration ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
setFilter๋กœ filter๋ฅผ ๋“ฑ๋กํ•˜๊ณ  setOrder๋กœ ์ˆœ์„œ๋ฅผ ์ง€์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean<Filter> jwtValidationFilter(JwtProvider jwtProvider, ObjectMapper objectMapper) {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new JwtValidationFilter(jwtProvider, objectMapper));
        filterFilterRegistrationBean.setOrder(1);
        return filterFilterRegistrationBean;
    }
}

์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ํ†ตํ•œ ์ธ๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„

ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์€ ํŠน์ • ์š”์ฒญ, ์ปจํŠธ๋กค๋Ÿฌ์— ๊ด€๊ณ„์—†์ด ์ „์—ญ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ์ž‘์—…์ด๋ฏ€๋กœ ํ•„ํ„ฐ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.


๋ฐ˜๋ฉด ํŠน์ • url์— ๋Œ€ํ•œ ์ธ๊ฐ€ ๊ธฐ๋Šฅ์€ ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ๊ณผ ๊ด€๋ จ๋˜์–ด ์ „์—ญ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ์ž‘์—…๋“ค์ด๋ฏ€๋กœ ์ธํ„ฐ์…‰ํ„ฐ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.


์œ ์ €๋Š” ADMIN, CUSTOMER, DRIVER ์ค‘ ํ•˜๋‚˜์˜ ๊ถŒํ•œ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
์ด๋•Œ ๊ณ ๊ฐ ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์œ ์ €๋Š” ๊ธฐ์‚ฌ ํŽ˜์ด์ง€์— ์ ‘๊ทผ ํ•  ์ˆ˜ ์—†์œผ๋ฉฐ, ๊ธฐ์‚ฌ ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์œ ์ €๋Š” ๊ณ ๊ฐ ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.


์ด์™€ ๊ฐ™์€ ์ธ๊ฐ€๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” UserAuthorizationInterceptor๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.


ํ† ํฐ ์œ ํšจ์„ฑ์ด ๊ฒ€์ฆ๋œ ์œ ์ €์— ํ•œํ•ด์„œ ์ธ๊ฐ€ ์ฒ˜๋ฆฌ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
์š”์ฒญํ•œ url์ด ์—†๋Š” ๊ฒฝ์šฐ 404 NOT FOUND๋ฅผ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.


hasAuthority๋กœ ์œ ์ € role์— ๋”ฐ๋ฅธ ์ปจํŠธ๋กค๋Ÿฌ ์ ‘๊ทผ ๊ถŒํ•œ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
์ ‘๊ทผ ๊ถŒํ•œ์ด ์žˆ๋‹ค๋ฉด ๋‹ค์Œ ์ธํ„ฐ์…‰ํ„ฐ๋‚˜ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†๋‹ค๋ฉด 401 UNAUTHORIZED ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

public class UserAuthorizationInterceptor implements HandlerInterceptor {

    private static final String CUSTOMER_URL = "/reservations";
    private static final String DRIVER_URL = "/orders";

    @Override
    public boolean preHandle(HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull Object handler)
        throws IOException {
        if (request.getAttribute(AUTHENTICATE_USER) != null) {
            AuthenticateUser authenticateUser = (AuthenticateUser)request.getAttribute(AUTHENTICATE_USER);
            Role role = authenticateUser.getRole();
            String baseUrl = getBaseUrl(handler);

            if (baseUrl == null) {
                response.sendError(HttpStatus.NOT_FOUND.value(), "์š”์ฒญํ•œ ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ");
                return false;
            }

            // url์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ ํ™•์ธ
            if (hasAuthority(baseUrl, role)) {
                return true;
            }
        }
        response.sendError(HttpStatus.UNAUTHORIZED.value(), "์—‘์„ธ์Šค ๊ถŒํ•œ ์—†์Œ");
        return false;
    }

    private static String getBaseUrl(Object handler) {
        if (handler instanceof HandlerMethod handlerMethod) { //ํ˜ธ์ถœํ•  ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์˜ ๋ชจ๋“  ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Œ
            return handlerMethod.getMethod().getDeclaringClass().getAnnotation(RequestMapping.class).value()[0];
        }
        return null;
    }

    private boolean hasAuthority(String url, Role role) {
        if ((role.equals(Role.CUSTOMER) && url.endsWith(DRIVER_URL))
            || (role.equals(Role.DRIVER) && url.endsWith(CUSTOMER_URL))) {
            return false;
        }
        return true;
    }
}

์ด๋ ‡๊ฒŒ ์ƒ์„ฑํ•œ ์ธํ„ฐ์…‰ํ„ฐ๋Š” InterceptorRegistry๋ฅผ ํ†ตํ•ด ๋“ฑ๋กํ•ด์•ผ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
WebConfig์— InterceptorRegistry.addInterceptor๋กœ ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๋“ฑ๋กํ–ˆ์Šต๋‹ˆ๋‹ค.
ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ์€ jwt ๋ฐœ๊ธ‰ ์ „์— ์ˆ˜ํ–‰๋˜๋Š” ์ž‘์—…์ด๋ฏ€๋กœ excludePathPatterns๋กœ ์ธ๊ฐ€ ์ฒ˜๋ฆฌ์—์„œ ์ œ์™ธํ–ˆ์Šต๋‹ˆ๋‹ค.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserAuthorizationInterceptor())
            .order(1)
            .excludePathPatterns("/api/v1/users/**", "*/h2-console*", "/css/**", "/*.ico", "/error");
    }
}

  • (ํ•„์ˆ˜) ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํ•ด์•ผํ•จ
  • (์„ ํƒ) ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋ฐœ๊ธ‰ / ๋กœ๊ทธ์•„์›ƒ ๊ตฌํ˜„ํ• ์ง€ ์–˜๊ธฐํ•ด๋ด์•ผํ•จ
  • ๊ตฌํ˜„ํ•˜๊ณ  ๋‚˜๋‹ˆ๊นŒ ๋น„ํšŒ์›์€ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผํ•˜๋‚˜ ์‹ถ์€๋ฐ ๋น„ํšŒ์›์ด ์ ‘๊ทผํ•˜๋Š” url๋งŒ ์ธ์ฆ, ์ธ๊ฐ€ ์•ˆ ํ•˜๋ฉด ๋  ๊ฒƒ ๊ฐ™๊ธฐ๋„ ํ•˜๊ณ