본문 바로가기

SPRING/SECURITY

[SpringSecurity6] 메서드 수준 보안 ( with JWT )

스프링 시큐리티는 다양하고 세밀한 보안 기능을 갖고 있습니다.

그 중 하나가 비지니스 로직을 포함한 메서드를 보호하기 위한 메서드 수준 보안입니다.

간단하게 어노테이션을 사용하여 권한에 따라 메서드를 보호할 수 있습니다

 

 

@EnableMethodSecurity

@EnableJpaAuditing
@SpringBootApplication
@EnableMethodSecurity
public class MinglecrmApplication {
    public static void main(String[] args) {
       SpringApplication.run(MinglecrmApplication.class, args);
    }
}

 

메서드 수준 보안을 위해 가장 먼저 스프링 어플리케이션 클래스에 @EnableMethodSecurity 어노테이션을 적용 시켜야 합니다.

 

 

현재 Jwt를 사용해 인증을 수행하고 있습니다. 전체 로직을 보여주기에는 볼륨이 너무 커지므로 토큰 인증 필터 코드를 통해 설명하겠습니다.

import com.team2final.minglecrm.service.jwt.JwtProvider;
import com.team2final.minglecrm.controller.employee.vo.Subject;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JWTTokenAuthenticationFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

    public JWTTokenAuthenticationFilter(JwtProvider jwtProvider) {
        this.jwtProvider = jwtProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String authorization = request.getHeader("Authorization");
        
        if(authorization!=null) {
            String atk = authorization.substring(7);
            try {
                Subject subject = jwtProvider.getSubject(atk);
                String requestURI = request.getRequestURI();
                
                if (subject.getType().equals("RTK") && !requestURI.equals("/api/v1/auth/renew")) {
                    throw new JwtException("토큰을 확인하세요");
                }
                
                String username = subject.getEmail();
                String authorities = subject.getAuthority();
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                        username, null, AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (JwtException e) {
                request.setAttribute("exception", e.getMessage());
            }
        }
        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return request.getServletPath().equals("/user/signin");
    }
}

 

위 코드에서 눈여겨 보아야 할 곳은 아래 부분입니다.

                String authorities = subject.getAuthority();
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                        username, null, AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
                SecurityContextHolder.getContext().setAuthentication(auth);

 

첫 번째 줄에 위치한 String authorities = subject.getAuthority();를 사용하여 토큰에 포함된 페이로드에서 콤마(,)로 구분되는 권한 목록을 불러옵니다. 예를 들면 다음과 같습니다. 

 

AuthorityUtils.commaSeparatedStringToAuthorityList 메서드는 "ROLE_MANAGER,ROLE_STAFF"라는 문자열을 받아 콤마로 구별한 후 SimpleGrantedAuthority객체 리스트로 변환시켜줍니다.

 

이렇게 만들어진 권한은 username(이메일)과 함께 Authentication객체를 상속한 UsernamePasswordAuthenticationToken객체에 담겨 SecurityContext에 저장되고 우리의 스프링 시큐리티 프레임워크가 이를 인지해 요청 승인/거절을 수행합니다

 

 

이제 테스트를 위한  RestAPI를 작성해보겠습니다.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @GetMapping("/authority/manager")
    @PreAuthorize("hasRole('MANAGER')")
    public ResponseEntity<String> authTestManager() {
        return new ResponseEntity<>("요청 승인", HttpStatus.OK);
    }
}

 

@PreAuthorize 어노테이션을 사용해 현재 인증된 사용자가 MANAGER라는 권한을 갖고 있을경우 메서드 요청을 승인하고, 그러지 않을 경우 차단합니다.

hasRole을 사용하고 있으므로 실제 권한은 접두사에 ROLE_이 붙어야합니다. 따라서 회원가입 시 ROLE을 붙혀주세요

 

 

 

 

atk에 있는 access token을 사용해 위에서 살펴본 API로 요청을 해봅시다.

 

이렇게 거절됩니다.

 

이번엔 회원가입 시 ROLE_MANAGER를 추가해서 요청을 보내보겠습니다.

 

마찬가지로 로그인 요청 후 토큰을 담아 요청을 보내보겠습니다.

 

보시는 것과 같이 요청이 승인됩니다.

메서드 수준 보안을 지원하기 위한 몇 가지 어노테이션이 더 존재합니다.

 

자세한 내용은 스프링 공식문서에서 확인하실 수 있습니다.

 

https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#meta-annotations