Bibi's DevLog ๐Ÿค“๐ŸŽ

[Spring] JWT๋กœ ํ† ํฐ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๋งŒ๋“ค๊ธฐ ๋ณธ๋ฌธ

๐Ÿ–ฅ BE ๋ฐฑ์—”๋“œ/Spring ์Šคํ”„๋ง

[Spring] JWT๋กœ ํ† ํฐ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๋งŒ๋“ค๊ธฐ

๋น„๋น„ bibi 2021. 6. 4. 23:37

[airbnbํ”„๋กœ์ ํŠธ] JWT ์ฝ”๋“œ ์„ค๋ช…

์ถœ์ฒ˜ : ์ „์ฒด ์ฝ”๋“œ ์ถœ์ฒ˜๋Š” https://ocblog.tistory.com/56, ์„ค๋ช…์€ yeon์ด ํ•ด ์ฃผ์…จ์Šต๋‹ˆ๋‹ค๐Ÿ™‡โ€โ™‚๏ธ

JWT ํ† ํฐ ์™œ ์“ฐ๋Š”๊ฐ€?

๋กœ๊ทธ์ธ ์‹œ ์„œ๋ฒ„๊ฐ€ ํ† ํฐ์„ ๋งŒ๋“ค์–ด ํด๋ผ์ด์–ธํŠธ์— ๋ฐœ๊ธ‰ํ•ด ์ค€๋‹ค.

์ดํ›„ ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ URL์—์„œ, ํ—ค๋”์— ํ† ํฐ์ด ๋“ค์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.

  • ํ† ํฐ์ด ์žˆ์œผ๋ฉด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ํ† ํฐ์ด ์—†์œผ๋ฉด ๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ๊ฐ„์ฃผํ•˜๊ณ  ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ด์šฉํ•  ์ˆ˜ ์—†๋‹ค.

์ธํ„ฐ์…‰ํ„ฐ interceptor

์ธํ„ฐ์…‰ํ„ฐ : ์ปจํŠธ๋กค๋Ÿฌ์— ์˜ค๊ธฐ ์ „์— ์กด์žฌํ•˜๋Š” ๋‹จ๊ณ„.

ํ”„๋ก ํŠธ๊ฐ€ HTTP Header์— ์šฐ๋ฆฌ๊ฐ€ ๋ฐœ๊ธ‰ํ•œ ํ† ํฐ์„ ๋‹ด์•„์„œ ๋ณด๋‚ด์ฃผ๋ฉด ("Authorization" : Bearer ํ† ํฐ) ์šฐ๋ฆฌ๊ฐ€ ๋ฐœ๊ธ‰ํ•œ ํ† ํฐ์ด ๋งž๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.

ํ† ํฐ ํ™•์ธ ์ž‘์—…์ด ๋ฐ˜๋ณต์ ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์•„๋‹Œ ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด ๊ฑฐ๊ธฐ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

(์•„๋ž˜์˜ BearerInterceptor ์ฝ”๋“œ ์„ค๋ช… ์ฐธ์กฐ)

AppConfig

@Configuration
public class AppConfig implements WebMvcConfigurer {

    private static final Logger logger = LoggerFactory.getLogger(AppConfig.class);

    private final BearerAuthInterceptor bearerAuthInterceptor;

    public AppConfig(BearerAuthInterceptor bearerAuthInterceptor) { // ์ธํ„ฐ์…‰ํ„ฐ ๊ตฌํ˜„์ฒด๋ฅผ ๋งŒ๋“ค์–ด ๋“ฑ๋กํ•จ
        this.bearerAuthInterceptor = bearerAuthInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        logger.info(">>> ์ธํ„ฐ์…‰ํ„ฐ ๋“ฑ๋ก");
        registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/api/booking")
                .addPathPatterns("/api/booking/{bookingId}")
                .addPathPatterns("/api/rooms/{userId}/wish/{roomId}");
    }
}

AppConfig๋Š” WebMvcConfigurer๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

  • addInterceptors()
    • ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๋“ฑ๋กํ•  ๋•Œ, ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๊ฑฐ์น  url์„ ๋“ฑ๋กํ•œ๋‹ค.
    • = ์ฆ‰ ๋กœ๊ทธ์ธ์ƒํƒœ์ธ์ง€ ํ™•์ธ์ด ๋˜์–ด์•ผ๋งŒ ์ œ๊ณต๋˜๋Š” ๊ธฐ๋Šฅ๊ณผ ์—ฐ๊ฒฐ๋œ url๋“ค์„ ๋“ฑ๋กํ•˜๋ฉด ๋œ๋‹ค.

BearerAuthInterceptor

package com.codesquad.airbnb.jwt;

import com.codesquad.airbnb.exception.TokenEmptyException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class BearerAuthInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(BearerAuthInterceptor.class);

    private AuthorizationExtractor authorizationExtractor;
    private JwtTokenProvider jwtTokenProvider;

    public BearerAuthInterceptor(AuthorizationExtractor authorizationExtractor, JwtTokenProvider jwtTokenProvider) {
        this.authorizationExtractor = authorizationExtractor;
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        logger.info(">>> interceptor.preHandle ํ˜ธ์ถœ");
        String token = authorizationExtractor.extract(request, "Bearer");
        if (token.isEmpty()) {
            throw new TokenEmptyException();
        }

        if (!jwtTokenProvider.validateToken(token)) {
            throw new IllegalArgumentException("์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ");
        }

        Long id = jwtTokenProvider.getSubject(token);
        request.setAttribute("id", id);
        return true;
    }
}

HandlerInterceptor๋ฅผ ๊ตฌํ˜„ํ•œ BearerAuthInterceptor์ธํ„ฐ์…‰ํ„ฐ๋ฅผ, @Configuration์ด ๋ถ™์€ AppConfig์—์„œ ์ธํ„ฐ์…‰ํ„ฐ๋กœ ๋“ฑ๋กํ•œ๋‹ค. (addInterceptors())

  • preHandle() : HandlerInterceptor์˜ ์ถ”์ƒ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•œ ๊ฒƒ.
    • ์ด ๊ฒฐ๊ณผ๊ฐ’์ด true์—ฌ์•ผ ๋‹ค์Œ์œผ๋กœ ๋„˜์–ด๊ฐ„๋‹ค.
    • true๊ฐ€ ๋ฐ˜ํ™˜์ด ๋˜์–ด์•ผ๋งŒ controller์— ๋„๋‹ฌํ•œ๋‹ค.
    • request-์ธํ„ฐ์…‰ํ„ฐ - ํ—ค๋”์— ์žˆ๋Š” ํ† ํฐ์„ ์ถ”์ถœํ•œ๋‹ค. (authorizationExtractor.extract())
    • ํ† ํฐ์ด ๋น„์–ด์žˆ๋‹ค = ๋กœ๊ทธ์ธ์ด ์•ˆ ๋˜์–ด์žˆ๋‹ค -> ์˜ˆ์™ธ ๋ฐœ์ƒ
    • jwtTokenProvider.validateToken() : ์œ ํšจํ•œ ํ† ํฐ์ธ์ง€ ๊ฒ€์ฆ. ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ
    • ํ™•์ธ์ด ๋๋‚˜๋ฉด (์šฐ๋ฆฌ๋Š” userId๋ฅผ ์ด์šฉํ•ด ํ† ํฐ์„ ๋งŒ๋“œ๋Š”๋ฐ) ํ† ํฐ์„ ๋””์ฝ”๋”ฉํ•ด, ํ† ํฐ์— ๋“ค์–ด์žˆ๋Š” userId๋ฅผ ํ™•์ธํ•œ๋‹ค(jwtTokenProvider.getSubject()).

AuthorizationExtractor

package com.codesquad.airbnb.jwt;

import org.apache.logging.log4j.util.Strings;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

@Component
public class AuthorizationExtractor {

    public static final String AUTHORIZATION = "Authorization";
    public static final String ACCESS_TOKEN_TYPE = AuthorizationExtractor.class.getSimpleName();

    public String extract(HttpServletRequest request, String type) {
        Enumeration<String> headers = request.getHeaders(AUTHORIZATION);
        while (headers.hasMoreElements()) {
            String value = headers.nextElement();
            if (value.toLowerCase().startsWith(type.toLowerCase())) {
                return value.substring(type.length()).trim();
            }
        }
        return Strings.EMPTY;
    }
}
  • HTTP header์— ๋“ค์–ด ์žˆ๋Š” ํ† ํฐ์„ ์ถ”์ธจํ•˜๋Š” ์—ญํ• ์˜ ๋ฉ”์„œ๋“œ์ด๋‹ค.

JwtTokenProvider

package com.codesquad.airbnb.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Base64;
import java.util.Date;

@Component
public class JwtTokenProvider {

    private final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

    private String secretKey;
    private long validityInMilliseconds;

    public JwtTokenProvider(@Value("&{security.jwt.token.secret-key}") String secretKey, @Value("${security.jwt.token.expire-length}") long validityInMilliseconds) {
        this.secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
        this.validityInMilliseconds = validityInMilliseconds;
    }

    // ํ† ํฐ ์ƒ์„ฑ
    public String createToken(Long id) {
        Claims claims = Jwts.claims().setSubject(String.valueOf(id));

        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);
        logger.info("now: {}", now);
        logger.info("validity: {}", validity);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    // ํ† ํฐ์—์„œ ๊ฐ’ ์ถ”์ถœ
    public Long getSubject(String token) {
        return Long.valueOf(Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody()
                .getSubject());
    }

    // ์œ ํšจํ•œ ํ† ํฐ์ธ์ง€ ํ™•์ธ
    public boolean validateToken(String token) {
        Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
        return !claims.getBody().getExpiration().before(new Date());
    }
}
  • JwtTokenProvider : ํ† ํฐ์„ ์•”ํ˜ธํ™”, ๋ณตํ˜ธํ™”, ๊ฒ€์ฆ ๋“ฑ์˜ ์ž‘์—…์„ ํ•˜๋Š” ํ•˜๋Š” ํด๋ž˜์Šค
  • getSubject() : ํ† ํฐ์„ ๊ฐ’์„ ์ถ”์ถœํ•ด ๋ฐ›์•„์˜ค๋Š” ๋ฉ”์„œ๋“œ
  • ํ† ํฐ ์ƒ์„ฑ, ์ถ”์ถœ, ์œ ํšจ์„ฑ ํ™•์ธ ๋ฉ”์„œ๋“œ๋“ค์ด ๋“ค์–ด์žˆ๋‹ค.
  • ํ† ํฐ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ์‹œํฌ๋ฆฟ ํ‚ค์™€ ์œ ํšจ๊ธฐ๊ฐ„ ๋“ฑ์„ application.properties์˜ ์„ค์ •์—์„œ ๊ฐ€์ ธ์™€ ํ† ํฐ์„ ๋งŒ๋“ ๋‹ค.
  • ์ƒ์„ฑ : Jwts๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•ด ์‹œํฌ๋ฆฟ ํ‚ค๋กœ ํ† ํฐ ์•”ํ˜ธํ™”์™€ ์œ ํšจ๊ธฐ๊ฐ„์„ ์„ค์ •ํ•œ๋‹ค.
    • ํ† ํฐ์˜ sub ๋“ฑ์„ claim์œผ๋กœ ์„ค์ •ํ•œ๋‹ค.

TokenRespnose

package com.codesquad.airbnb.jwt;

public class TokenResponse {

    private String accessToken;
    private String tokenType;

    public TokenResponse(String accessToken, String tokenType) {
        this.accessToken = accessToken;
        this.tokenType = tokenType;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public String getTokenType() {
        return tokenType;
    }
}
  • TokenResponse : JWTํ† ํฐ ๊ฐ’์„ ๋‹ด์•„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ Response DTO ํด๋ž˜์Šค์ด๋‹ค.