본문 바로가기
📕 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/

 

이제 남은 것은 장바구니와 주문이다. 제대로 해보자

반응형