SMALL

서버 개발을 하다 보면 오류 처리를 해야 될 경우가 매우 많이 존재한다.

오류를 제대로 처리하지 않는다면, 서버가 안정적이지 못하게 동작할 수 있다.

 

보통의 경우에는 Exception 처리를 try-catch를 통해 해결한다.

하지만 이러한 로직들은 비즈니스 로직이 존재하는 곳에 코드가 써져 

코드를 복잡하게 하고 유지보수를 어렵게 만든다.

 

이런 문제를 개선하기 위해 @ExceptionHandler와 @ControllerAdvice를 사용해 보자.

 

@ExceptionHandler

@Controller, @RestController가 적용된 bean 내에서 발생하는 예외를 처리할 수 있게 해주는 어노테이션

 

@RestController 
public class HelloController {
    @GetMapping("/error")
    public void test() {
    	throw new IllegalArgumentException("test error");
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handle(Exception ex) {
        return new ResponseEntity<>(new ResponseError(LocalDateTime.now(), "error"), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

@ExceptionHandler는 등록된 해당 Controller에만 적용이 된다.

다른 Controller에서 발생하는 예외는 잡을 수 없다.

여러 Controller에서 동일하게 발생하는 예외는 Controller에 적용된 @ExcptionHandler가 아닌 다른 방법으로 처리해야 한다.

 

@ControllerAdvice

@Controller, @RestController에서 발생한 예외를 한 곳에서 처리할 수 있는 어노테이션

스프링에서 예외처리를 전역적으로 핸들링하기 위해 사용

@ControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(IllegalArgumentException.class)
	public ResponseEntity<?> handleIllegalArguException(IllegalArgumentException e) {
		return new ResponseEntity<>(new ResponseError(LocalDateTime.now(), "error"), 
			HttpStatus.INTERNAL_SERVER_ERROR);
	}
}

Controller에 해당하는 모든 예외를 처리할 수 있지만, 사용자가 원한다면 특정 패키지로 제한을 할 수 있다

@ControllerAdvice("com.exam.controller")

 

@RestControllerAdvice

추가적으로 @RestControllerAdvice에 대해서 간단하게 알아보자

해당 어노테이션은 @ControllerAdvice + @ResponseBody 조합 어노테이션이라 생각하면 된다.

( @Controller, @RestController 관계와 비슷하게 @ResponseBody 설정이 추가된 어노테이션)

( https://joomn11.tistory.com/53?category=936835 )

 

다만 위에 예시처럼 Exception을 처리한 메서드의 리턴 값을 ResponseEntity로 지정을 한다면 @RestControllerAdvice 어노테이션을 사용하지 않고도 ResponseBody에 결괏값을 실을 수 있다.

 

@RestControllerAdvice 사용 예시

@RestControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(IllegalArgumentException.class)
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	public ResponseError handleIllegalArguException(IllegalArgumentException e) {
		return new ResponseError(LocalDateTime.now(), "error"));
	}
}
@Getter
@Setter
@AllArgsConstructor
public class ResponseError {
	private LocalDateTime timestamp;
    private String details;
}

다만, @RestControoerAdvice 어노테이션을 사용하는 경우 각각의 Handler 메서드에 @ResponseStatus 어노테이션을 추가해 주어야 한다.

( 해당 정보를 세팅하지 않는다면 디폴트로 200으로 응답)

LIST
SMALL

Spring Security에서 현재 로그인한 사용자의 정보를 가져오는 방법을 알아보도록 하자.

 

SecurityContextHolder 이용

Spring Security에서 전역으로 선언된 SecurityContextHolder를 통해서 정보를 가져올 수 있다. 

SecurityContextHolder는 Spring Security Architechture에서 간단하게 알아본 적이 있는데

( https://joomn11.tistory.com/90 )

해당 클래스를 통해서 인증을 통해 발급된 Authentication을 얻을 수 있다. 

 

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userName = auth.getName();

 

Controller 메서드 인자 이용 

 

Controller로 선언된 클래스의 메서드에서 인자로 Authentication 객체에 접근을 할 수 있다. 

@Controller 
public class UserController { 

    @GetMapping("/user") 
    public String userName(Authentication authentication) { 
        
        String userName = authentication.getName();
        
        return userName;
    } 
}

 

추가적으로, Principal 객체도 얻을 수 있다. 

@Controller 
public class UserController { 

    @GetMapping("/user") 
    public String userName(Principal principal) { 
        
        String userName = principal.getName();
        
        return userName;
    } 
}

 

LIST
SMALL

Spring Security는 Sping Application의 보안(인증, 인가)을 담당한다.

Spring Security는 Filter 기반으로 동작하기 때문에 Spring MVC와 분리되어 관리 및 동작한다.

Spring Security는 Session-Cookie방식으로 인증한다. 

 

Spring Security Architecture를 검색하면 아래의 그림을 많이 볼 수 있다.

해당 그림의 흐름으로 Spring Security의 구조 및 흐름을 알아보자.

 

출처 : www.springbootdev.com

1. 사용자가 로그인 요청을 한다. ( Http Request 전달 ) 

 

2,3 . AuthenticationFilter에서 AuthenticationTocken을 생성 

 

Spring Security는 Filter를 기반으로 동작하기 때문에 HttpRequest가 다양한 Security관련 Filter를 거치게 된다. 그중의 하나가 AuthenticationFilter이다. 

현재 위의 그림의 예시는 UsernamePasswordAuthenticationFilter에 해당하는 것으로 보인다. 

 

https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-security-filters

위의 링크는 Security관련 Filter의 정보이다. 

 

UsernamePasswordAuthenticationToken은 Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스이다.

User의 ID가 Principal 역할을 하고, Password가 Credential의 역할을 한다.

4. AuthenticaionManager는 등록된 AuthenticaionProvider(들)을 조회하여 인증을 요구

AuthenticaionManager의 실제 구현체가 ProviderManager이고 

ProviderManager는 AuthenticaionProvider(s) 들을 통해서 인증을 진행한다. 

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

	public List<AuthenticationProvider> getProviders() {
		return this.providers;
	}

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		for (AuthenticationProvider provider : getProviders()) {
			try {
				result = provider.authenticate(authentication);
				// ...
			}
		}
    
	}
}

 

5, 6, 7, 8, 9. AuthenticaionProvider는 UserDetailsService를 통해 사용자 정보 조회 및 인증완료된 Authenticaion 전달

 

10. 인증된 토큰을 SecurityContextHolder에 저장 

 

로그인 이후에는 SecurtiyContextHolder를 통해서 Authentication정보를 조회해 올 수 있다.

 

출처 : https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

모든 접근 주체는 Authentication가 생성된다. 이것은 SecurityContext에 보관되고 사용된다.

 

public interface Authentication extends Principal, Serializable { 
    Collection<? extends GrantedAuthority> getAuthorities(); //사용자의 권한 목록 
    Object getCredentials(); // 비밀번호 
    Object getDetails(); // 사용자 상세정보 
    Object getPrincipal(); // ID 
    boolean isAuthenticated(); //인증 여부 
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; 
}

 

LIST
SMALL

Spring Security에서 Annotation 형태로 Class, Method 레벨 단위로 권한별 접근제어 기능을 제공한다.

( https://joomn11.tistory.com/88 )

 

그중에서 @PreAuthorize, @PostAuthorize Annotation의 경우에는 SpringEL에서 제공하는 Expression 이외에도 사용자가 정의한 Custom Expression(Method)를 호출할 수 있는 기능을 제공한다.

해당 기능에 대해서 알아보도록 하자.

 

@PreAuthorize("isAuthenticated() and (( #user.name == principal.name ) or hasRole('ROLE_ADMIN'))")
@RequestMapping( value = "", method = RequestMethod.PUT)
public ResponseEntity<Project> updateProject( User user ){
    updateProject.update( user );
    return new ResponseEntity<Project>( new Project(), HttpStatus.OK );
}

우선 위의 예제와 같이 isAuthenticated, hasRole과 같은 Spring EL에서 기본제공하는 기능을 활용할 수도 있다. 

 

다만, 사용자가 원하는 기능이 기본으로 제공하는 기능에 존재하지 않는 경우에는 문제가 될 수 있다.

이러한 경우를 커버하기 위해서 Spring Security에서는 Custom Expression을 사용할 수 있는 기능을 열어 두었다.

 

 

Component 생성

먼저, Custom Expression(Method)를 정의할 Component를 생성한다. 

 

// @Service
@Component("loginService")
public class LoginService {
	public boolean pageReadRoleCheck() {
		return true;
	}
}

 

@PreAuthorize, @PostAuthorize Annotation에서 사용

@GetMapping("/api/v1/projects")
@PreAuthorize("@loginService.pageReadRoleCheck()")
public List<ProjectResponseDto> getProjectList() {
	return projectService.findAll();
}

 

위와 같이 간단하게 Custom Expression을 Spring Security Annotation에서 사용이 가능하다.

 

Param 정보 전달

추가적으로, Custom Expression에 필요한 정보를 Param으로 넘길 수도 있다.

#pj와 같이 사용하여 Annotation이 붙은 Method의 Param 정보도 전달 할 수 있다.

// @Service
@Component("loginService")
public class LoginService {
	public boolean pageReadRoleCheck(String id) {
		return true;
	}
}
/////
@GetMapping("/api/v1/projects")
@PreAuthorize("@loginService.pageReadRoleCheck(#pj.id)")
public List<ProjectResponseDto> getProjectList(Project pj) {
	return projectService.findAll();
}

 

returnObject

@PostAuthorize의 경우에는 해당 Method의 수행 결과 값도 전달 할 수 있다. 

returnObject라는 예약어를 사용하면 된다.

@PostAuthorize("@loginService.pageReadRoleCheck(returnObject.name)")
@RequestMapping( value = "/{id}", method = RequestMethod.GET )
public Project getProject( @PathVariable("id") long id ){
    return service.findOne(id);
}

 

Annotation 활성화 설정 추가

마지막으로, 이러한 Annotation을 활성화 시키기 위해서는 설정을 추가해 주어야 한다.

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class ServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
        //.....
   }
}

 

LIST

+ Recent posts