본문 바로가기

Spring

[Spring Boot + Security] @PreAuthorize로 토큰 확인하기

웹 애플리케이션을 개발하다 보면 로그인한 계정의 정보를 보여주는 마이페이지 등을 구현해야 할 때가 생긴다. 그런데 다른 계정의 토큰을 가진 사람이 여기에 접근하면 보안상 문제가 있기 때문에 컨트롤러 혹은 서비스에서 토큰 내부의 ID 값과 요청하는 ID 값을 확인하는 로직을 넣어주어야 한다.

 

문제 인식: 위처럼 요청하는 ID가 발급된 토큰 내부의 ID값과 일치하는지 확인하는 코드를 비즈니스 로직에 추가해주어야 한다. 그런데 비즈니스와는 상관없는 관심사 코드가 매번 들어가야 하므로 응집도가 떨어지는 단점이 있었다.

 

그래서 몇 가지 방법을 생각해봤다.

 

방법 1: token을 발급해주는 tokenProvider Bean 내부에서 검증해주는 로직을 작성하여 컨트롤러 or 서비스 내부에 넣는다.

-> 처음에 이 방법으로 했었는데 새로운 컨트롤러, 서비스를 만들 때마다 매번 tokenProvider를 주입받아야 하므로 단점을 덜어냈을뿐 근원은 해결하지 못했단 생각이 들었다.

 

방법 2: AOP를 사용하자.

-> 토큰 내부의 ID를 검증하는건 사실 관심사 밖이다. 스프링에선 이처럼 관심사 밖의 것들을 처리해주는 aop라는 도구가 있어 이걸 사용해보기로 했다.

--> securty에서 @preauthorize 애노테이션을 붙이면 해당 메서드를 수행하기 전 프록시로 감싸 검증을 수행한다. 또한 expression을 작성하여 유연하게 검증할 수 있다. 이 방식을 이용하여 ID 검증하는 기능을 @PreAuthorize을 사용하여 한 줄로 줄여보겠다.

 

스프링 시큐리티에서 @PreAuthorize를 사용하기 위해선 prePostEnabled를 활성화시켜야 한다.

@EnableGlobalMethodSecurity(prePostEnabled = true)

이제 TokenProvider 내부에 검증 로직을 작성해보자.

   ...
   private String getAccountIdWithToken(String authValue) {
     return Jwts.parserBuilder()
        .setSigningKey(key)
        .build()
        .parseClaimsJws(authValue.substring(7))
        .getBody()
        .getSubject();
  }
  
  public boolean isUserToken(String authValue, String accountId){
    //authValue엔 Authorization 헤더 값이 들어있음
    if(getAccountIdWithToken(authValue).equals(accountId)){
      System.out.println("=========비교========");
      System.out.println(getAccountIdWithToken(authValue) + "  vs  " + accountId);
      return true;
    }else{
      return false;
    }
  }

원하는 컨트롤러 or 서비스 메서드에 어노테이션을 붙이자.

내부 식에 @를 붙이면 Bean을 가져올 수 있다.

  @PreAuthorize("@tokenProvider.isUserToken(#authValue, #dto.fromAccountId)")
  @PostMapping(value = "/tutoring/{tutoringId}", produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<String> responseTutoring(
      @RequestHeader(name = "Authorization") String authValue,
      @PathVariable long tutoringId,
      @Valid @RequestBody TutoringResponseDto dto) {
    return ResponseEntity.status(HttpStatus.CREATED)
        .body(commandExecutor.responseTutoring(authValue, tutoringId, dto).getMessage());
  }

결과

토큰 내부 ID를 검증하는 로직 수행
일치하지 않으면 접근 거부

개선점: TokenProvider를 매번 주입받지 않고, 어노테이션 하나로 ID 검증 로직을 수행할 수 있게 되었다.

 

한계: 컴파일 타임에 에러를 잡아내지 못하고 런타임에 에러가 발생한다.. 주의해야 한다.

 

ref: https://developer.okta.com/blog/2019/06/20/spring-preauthorize