๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ“• Spring Framework/Spring Project

2022.05.20 ใ€Š์Šคํ”„๋ง ๋ถ€ํŠธ ๊ถŒํ•œ ์ฒ˜๋ฆฌใ€‹

by GroovyArea 2022. 5. 20.
์Šคํ”„๋ง ๋ถ€ํŠธ ํ”„๋กœ์ ํŠธ ์ค‘์ด๋‹ค. jwt๋ฅผ ์ด์šฉํ•œ ์ธ์ฆ์€ ๋‹ค ๋๋‚ฌ๊ณ  ์ธ๊ฐ€ ์ž‘์—…๋งŒ ๋‚จ์•˜๋‹ค.
๋Œ€๋ถ€๋ถ„ ์Šคํ”„๋ง ์„œํ๋ฆฌํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ถŒํ•œ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š”๋“ฏํ•˜๋‹ค. ๋‚˜๋Š” ์ผ๋‹จ ์„œํ๋ฆฌํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์งœ๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ณ ๋ฏผ์„ ์ข€ ๋งŽ์ด ํ•ด๋ดค๋‹ค.
๊ฒฐ๋ก ์„ ๋‚ด๋ฆฌ์ž๋ฉด ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ์ด์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์‹ฌํ–ˆ๋‹ค. ๊ด€๋ฆฌ์ž์ธ์ง€ ํšŒ์›์ธ์ง€ ์ธ์ฆ์ด ํ•„์š”ํ•œ ์ž‘์—…์ด๋“ ์ง€ ์ค‘๋ณต๋˜๋Š” ๋กœ์ง์ด ๋„ˆ๋ฌด ๋งŽ์•„์ง€๋ฏ€๋กœ ์ด๊ฒƒ์€ ์ธํ„ฐ์…‰ํ„ฐ์—์„œ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ์ „์— ์ธ๊ฐ€ ์ž‘์—…์„ ํ•ด์ค˜์•ผ ํ•˜๋Š” ๊ฒƒ์ด ์ฃผ๋œ ์ด์œ ๋‹ค.
ํ•œ๋ฒˆ ํ•ด๋ณด์ž~

=> ์ „๋ฐ˜์ ์ธ ๊ณ„ํš ์ž‘์„ฑ

 

์ค‘๊ฐ„ ์ •๋ฆฌ

ํ˜ผ์ž ํ”„๋กœ์ ํŠธ๋ฅผ ํ•ด์„œ ์ธ์ง€ ์†๋„๊ฐ€ ๋„ˆ๋ฌด ๋‚˜์งˆ ์•Š๋Š”๋‹ค. 

์ƒ๊ฐ์„ ํ•ด๋ณด์•˜๋‹ค.

์–ด๋А ํ•œ ๊ธฐ๋Šฅ์ด๋“  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•˜๊ฒŒ ์„ค๊ณ„ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. ๊ฐ€์žฅ ์‹œ๊ฐ„์„ ๋งŽ์ด ์Ÿ์€ ๋ถ€๋ถ„์€ Restful ํ•œ ์„ค๊ณ„ ๋ฐฉ์‹๊ณผ ์ธ์ฆ ์ธ๊ฐ€ ๋ถ€๋ถ„์ด๋‹ค. ์ด์ œ ๊ฒจ์šฐ ๋๋‚ธ ๊ฒƒ ๊ฐ™๋‹ค. JWT๋ฅผ ์ด์šฉํ•ด Redis๋ฅผ ์ ์šฉ์‹œํ‚ค๋‹ˆ ์ด๊ฒŒ ์€๊ทผํžˆ ๊ฐ„๋‹จํ•˜์ง€๋งŒ์€ ์•Š๋‹ค.

 

๋ฌธ์ œ์ 

1. RestFul ํ•œ ์‘๋‹ต

-> ๋‹จ์ˆœํžˆ ResponseEntity๋ฅผ ์‚ฌ์šฉํ•ด ์‘๋‹ต ์ฝ”๋“œ, ๋ฐ์ดํ„ฐ, ํ˜•์‹ ๋“ฑ์„ ๋ณด๋‚ด๋ฉด ๋˜์ง€ ์•Š๋‚˜ ์ƒ๊ฐ์„ ํ–ˆ์—ˆ๋‹ค. 

 

ํด๋ผ์ด์–ธํŠธ ๋‹จ ์š”๊ตฌ ํ˜•์‹์— ๋”ฐ๋ผ ์‘๋‹ต์€ ์œ ์—ฐํ•˜๊ฒŒ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์„ ๋ฐฐ์› ๋‹ค. 

์˜ˆ๋ฅผ ๋“ค์–ด ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€๋งŒ ๋ณด๋‚ผ ๊ฒฝ์šฐ, ๊ตณ์ด ๋ฐ์ดํ„ฐ ํ˜•์‹๊นŒ์ง€๋Š” ๋ณด๋‚ผ ํ•„์š”๊ฐ€ ์—†๋‹ค. 

์ƒํƒœ์ฝ”๋“œ์™€ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋งŒ ์‘๋‹ต

2. ์ธํ„ฐ์…‰ํ„ฐ์™€ ํ•„ํ„ฐ

์ฐธ์กฐ : https://goddaehee.tistory.com/154

 

-> ๊ถŒํ•œ ์ฒ˜๋ฆฌ์—์„œ ํ•„์ˆ˜์ ์ธ ์กด์žฌ๋“ค์ด๋‹ค.

 

์Šคํ”„๋ง ๊ฐœ๊ตฌ๋ฆฌ ์ฑ…์—์„œ ๋ดค๋“ฏ์ด ์Šคํ”„๋ง์€ ์ •๋ง ํ•˜๋ฉด ํ• ์ˆ˜๋ก ์™„๋ฒฝํ•œ ๊ฐ์ฒด์ง€ํ–ฅ๊ณผ ๋””์ž์ธ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ์•„๊ฐ„๋‹ค. 

 

์ธ์ฆ ์ฒ˜๋ฆฌ๋Š” ๋ณดํ†ต ๊ฐ€์žฅ ๋Œ€๋žต์ ์ธ ๊ฒ€์ฆ์ด๋ฏ€๋กœ ํ•ธ๋“ค๋Ÿฌ ์ง์ „์ด ์•„๋‹ˆ๋ผ ๋””์Šค ํŒจ์ณ ์„œ๋ธ”๋ฆฟ์ด ๋ฐ›๊ธฐ ์ „ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ๊ฒŒ ๋งž๋‹ค. 

์„œํ๋ฆฌํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๊ตฌํ˜„ํ•˜๋‹ค๋ณด๋‹ˆ ํ•„ํ„ฐ์—์„œ ๋ฐœ์ƒ์‹œํ‚จ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์—์„œ ๊ณ ๋ฐฐ๋ฅผ ๋งˆ์…จ๋‹ค. 

๋‚˜๋Š” ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ธ์ฆ, ๊ถŒํ•œ ์ฒ˜๋ฆฌ๋ฅผ ํ–ˆ๋‹ค.

public class JwtRequestFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(JwtRequestFilter.class);
    private final AuthorizationExtractor authorizationExtractor;
    private final JwtTokenProvider jwtTokenProvider;

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

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = authorizationExtractor.extract(request, "Bearer");

        try {
            jwtTokenProvider.validateToken(token);
        } catch (ExpiredJwtException e) {
            logger.error("Expired Jwt token : {}", e.getMessage());
            setErrorResponse(response, "Expired Jwt token");
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token : {}", e.getMessage());
            setErrorResponse(response, "Unsupported JWT token");
        } catch (MalformedJwtException e) {
            logger.error("Invalid JWT token : {}", e.getMessage());
            setErrorResponse(response, "Invalid JWT token");
        } catch (SignatureException e) {
            logger.error("Invalid JWT token signature : {}", e.getMessage());
            setErrorResponse(response, "Invalid JWT token signature");
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims string is empty : {}", e.getMessage());
            setErrorResponse(response, "JWT claims string is empty");
        } catch (ClassCastException e) {
            logger.error("JWT claims inspect fail : {}", e.getMessage());
            setErrorResponse(response, "JWT claims inspect fail");
        }
    }

    private void setErrorResponse(HttpServletResponse response, String message) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        response.setCharacterEncoding("utf-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.getWriter().write(objectMapper.writeValueAsString(new Message.Builder("null")
                .httpStatus(HttpStatus.UNAUTHORIZED)
                .message(message)
                .build())
        );
    }
}

=> ๊ฒฐ๊ตญ ์‚ฌ์šฉํ•˜์ง„ ์•Š์•˜์ง€๋งŒ ์ฒ˜์ฐธํ•œ ํ”์ ๋“ค..

 

๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•œ ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๋“ฑ๋กํ•˜๊ณ ,

 

/**
 * ๊ถŒํ•œ ์ฒ˜๋ฆฌ ์ธํ„ฐ์…‰ํ„ฐ <br>
 * ํ† ํฐ ๊ฒ€์ฆ ๋ฐ ์—๋„ˆํ…Œ์ด์…˜ ๊ถŒํ•œ ์ฒ˜๋ฆฌ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
 *
 * <pre>
 *     <b>History</b>
 *     ์ž‘์„ฑ์ž, 1.0, 2022.05.20 ์ตœ์ดˆ ์ž‘์„ฑ
 * </pre>
 *
 * @author ๊น€๋‚จ์˜
 * @version 1.0
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);
    private static final String BEARER_TOKEN = "Bearer";

    private final AuthorizationExtractor authorizationExtractor;
    private final JwtTokenProvider jwtTokenProvider;
    private final RedisTemplate<String, String> redisTemplate;

    public AuthInterceptor(AuthorizationExtractor authorizationExtractor, JwtTokenProvider jwtTokenProvider, RedisTemplate<String, String> redisTemplate) {
        this.authorizationExtractor = authorizationExtractor;
        this.jwtTokenProvider = jwtTokenProvider;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        /* ํ•ธ๋“ค๋Ÿฌ๋ฉ”์„œ๋“œ ์—๋„ˆํ…Œ์ด์…˜ ๊ฐ’ ์ถ”์ถœ */
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Auth auth = handlerMethod.getMethodAnnotation(Auth.class);

        if (auth == null) {
            return true;
        }

        /* ํ† ํฐ ์ถ”์ถœ ๋ฐ ๊ฒ€์ฆ */
        String requestToken = authorizationExtractor.extract(request, BEARER_TOKEN);
        jwtTokenProvider.validateToken(requestToken);

        /* ํ† ํฐ body์— ์กด์žฌํ•˜๋Š” ์•„์ด๋””์™€ ๋“ฑ๊ธ‰ */
        final String tokenUserId = jwtTokenProvider.getUserId(requestToken);
        final String tokenUserRole = jwtTokenProvider.getUserGrade(requestToken);

        /* Redis DB์— ์ €์žฅ๋œ ํ† ํฐ ์ถ”์ถœ */
        final ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
        final String redisToken = valueOperations.get(tokenUserId);

        /* DB์— ํ† ํฐ์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ */
        if (redisToken == null) {
            throw new RedisNullTokenException(AuthMessages.NULL_TOKEN.getMessage());
        }

        /* DB ํ† ํฐ๊ณผ ๋กœ๊ทธ์ธ ์œ ์ € ํ† ํฐ ์ •๋ณด๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ */
        if (!redisToken.equals(requestToken)) {
            throw new TokenMismatchException(AuthMessages.INVALID_TOKEN.getMessage());
        }

        /* ์—๋„ˆํ…Œ์ด์…˜ ๊ฐ’ => ๊ด€๋ฆฌ์ž์ผ ๊ฒฝ์šฐ */
        if (auth.role() == ADMIN) {
            /* ๋กœ๊ทธ์ธ ์œ ์ € ๊ถŒํ•œ์ด ๊ด€๋ฆฌ์ž๊ฐ€ ์•„๋‹ ๊ฒฝ์šฐ */
            if (!tokenUserRole.equals(ADMIN.toString())) {
                throw new AuthenticationException(AuthMessages.NOT_ADMIN_AUTH.getMessage());
            }
        }
        return true;
    }
}

ํ† ํฐ๊ณผ ์—๋„ˆํ…Œ์ด์…˜๊นŒ์ง€ ๋น„๊ตํ–ˆ๋‹ค. 

 

๊ถŒํ•œ ์—๋„ˆํ…Œ์ด์…˜ ์ •์˜

 

์ด๋Ÿฐ ์‹์œผ๋กœ ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์„œ๋“œ์— ์—๋„ˆํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค. 

 

์ž˜ ๋˜๋„ค์š”! ๋ณธ ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ์ผ๋ฐ˜ ๋ฉค๋ฒ„ ๋“ฑ๊ธ‰์ž„.

 

๋‚ด๊ฐ€ ๊ถŒํ•œ ์ฒ˜๋ฆฌ์—์„œ ์—๋„ˆํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•œ ์ด์œ 

ํ”„๋กœ์ ํŠธ ํฌ๊ธฐ ๋‹จ์œ„๋กœ ๋ณด์•˜์„ ๋•Œ ๊ทœ๋ชจ๊ฐ€ ์ปค์งˆ ๊ฒฝ์šฐ ์ฝ”๋“œ๋ฅผ ๊ณต์œ ํ•˜๊ฒŒ ๋  ํ…๋ฐ ์ด๋•Œ ์—๋„ˆํ…Œ์ด์…˜์„ ์ด์šฉํ•˜์—ฌ ๋ช…์‹œ์ ์ธ ๊ถŒํ•œ์„ ํŒ€์›๋“ค์ด ์•Œ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ƒ๊ฐ์„ ํ–ˆ๋‹ค.

 

์ฐธ์กฐ : https://velog.io/@kyle/%ED%9A%8C%EC%9B%90%EC%97%90-%EA%B6%8C%ED%95%9C%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%A0%EA%B9%8C

์ฐธ์กฐ : https://www.bottlehs.com/springboot/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-spring-security%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B8%EC%A6%9D-%EB%B0%8F-%EA%B6%8C%ED%95%9C%EB%B6%80%EC%97%AC/

 

์ด์ œ ๋‚จ์€ ๊ฒƒ์€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์™€ ์ฃผ๋ฌธ์ด๋‹ค. ์ œ๋Œ€๋กœ ํ•ด๋ณด์ž

๋ฐ˜์‘ํ˜•