본문으로 바로가기

스프링 시큐리티 MVC 통합

category Coding/Java Spring 2023. 6. 19. 20:45
반응형

@EnableWebMvcSecurity

스프링 4.0부터 @EnableWebMvcSecurity는 deprecated됐다. 대안으로 @EnableWebSecurity를 사용하자. 스프링 시큐리티와 스프링 MVC를 통합하려면 @Configuration 어노테이션이 붙은 클래스에 @EnableWebSecurity를 추가해주면 된다.

스프링 시큐리티는 MVC의 WebMvcConfigurer를 사용한 구성 방법도 제공한다. 이는 WebMvcConfigurationSupport와 직접 통합하는것과 같이 좀 더 세밀한 설정이 필요할 때 수동으로 설정하기 위해 사용된다.

MvcRequestMatcher

스프링 시큐리티는 MVC가 MvcRequestMatcher를 사용하여 URL을 매칭시키는 방법에 대해 통합하는 방법을 제공한다. MvcRequestMatcher를 사용하기 위해서는 DispatcherServlet과 동일한 ApplicationContext에 스프링 시큐리티 설정을 위치해야한다. 이는 시큐리티의 MvcRequestMatcher가 mvcHandlerMappingIntrospector라는 이름의 HandlerMappingIntrospector빈이 URL매칭을 위해 사용됨을 예상하기 때문에 굉장히 중요한 부분이다.

public class SecurityInitializer extends
    AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[] { RootConfiguration.class,
        WebMvcConfiguration.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }
}

항상 HttpServletRequest 및 메소드에 보안 규칙을 일치시키는것을 강력하게 권고한다. 이는 코드의 굉장히 앞단에 위치하기 때문에 공격에 대한 피해를 줄여준다. 

@RequestMapping("/admin")
public String admin() {
	// ...
}

관리자 유저만 컨트롤러에 접근 가능하도록 만들기 위해 HttpServletRequest에 대해 인증 관련 매칭 룰을 제공해줄 수 있다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
		.authorizeHttpRequests((authorize) -> authorize
			.requestMatchers("/admin").hasRole("ADMIN")
		);
	return http.build();
}

위 설정으로 인해 /admin URL은 관리자 권한을 가진 유저만 접근이 가능하게 된다. 하지만 스프링 MVC 설정에 의해 /admin.html URL또한 admin() 메소드와 매핑된다. 문제는 위 시큐리티 설정이 오직 /admin URL만 매핑하고 있다는 점인데 이는 requestMatcher를 통해 손쉽게 해결할 수 있다.

requestMatcher의 DSL문법을 사용할 때 스프링 시큐리티는 classpath에서 스프링 MVC를 감지하고 자동으로 MvcRequestMatcher를 생성한다. 따라서 MVC에 정의한 URL도 시큐리티가 보호하게 된다.

스프링 MVC를 사용할 때 일반적으로 서블릿 경로 속성을 지정하게 된다. 이를 위해 MvcRequestMatcher.Builder를 사용하여 동일한 경로를 공유하는 여러개의 MvcRequestMatcher 인스턴스를 생성할 수 있다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
	MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector).servletPath("/path");
	http
		.authorizeHttpRequests((authorize) -> authorize
			.requestMatchers(mvcMatcherBuilder.pattern("/admin")).hasRole("ADMIN")
			.requestMatchers(mvcMatcherBuilder.pattern("/user")).hasRole("USER")
		);
	return http.build();
}

@AuthenticationPrincipal

스프링 시큐리티는 스프링 MVC 환경에서 자동으로 Authentication.getPrincipal()을 수행하는AuthenticationPrincipalArgumentResolver를 제공한다. 이는 @EnableWebSecurity 어노테이션을 사용하면 자동으로 추가되어 사용할 수 있게된다.

당신이 작성한 UserDetailService가 UserDetails를 상속받아 구현한 CustomUser를 리턴한다고 가정해보자. CustomUser는 인증된 유저의 정보를 나타내며 아래와 같이 사용할 수 있다.

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser() {
	Authentication authentication =
	SecurityContextHolder.getContext().getAuthentication();
	CustomUser custom = (CustomUser) authentication == null ? null : authentication.getPrincipal();

	// .. find messages for this user and return them ...
}

스프링 시큐리티 3.2버전부터 어노테이션을 통해 좀 더 직접적으로 접근할 수 있다.

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {

	// .. find messages for this user and return them ...
}

가끔씩 pricipal을 특정한 방법을 사용해 변환할 필요가 있을 수 있다. 예를 들어 만약 CustomUser가 final이어야할 필요가 있는 경우 이는 확장이 불가능하다. 이런 경우 UserDetailsService는 UserDetails를 구현하고 CustomUser에 접근하기 위한 getCustomUser등의 메소드를 제공하는 객체를 반환하도록 해야한다.

public class CustomUserUserDetails extends User {
		// ...
		public CustomUser getCustomUser() {
				return customUser;
		}
}

이제 SpEL 표현식을 사용하여 Authentication.getPrincipal()을 수행하고 CustomUser에 접근할 수 있다.

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) {

	// .. find messages for this user and return them ...
}

SpEL 표현식을 통해 빈에도 접근할 수 있다. 예를 들어 JPA를 통해 유저를 관리하고 현재 유저의 속성을 수정하려는 경우 다음과 같이 사용할 수 있다.

@PutMapping("/users/self")
public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") CustomUser attachedCustomUser,
		@RequestParam String firstName) {

	// change the firstName on an attached instance which will be persisted to the database
	attachedCustomUser.setFirstName(firstName);

	// ...
}

@AuthenticationPrincipal을 커스텀하게 어노테이션으로 생성하여 스프링 시큐리티에 대한 의존성을 제거할 수 있다. 다음 예제는 @CurrentUser라는 어노테이션을 통해 이를 해결하는 방법을 보여준다.

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {}

우리는 스프링 시큐리티에 대한 의존성을 하나의 파일로 격리시켰다. 이제 @CurrentUser 어노테이션을 사용하면 현재 인증 유저에 대해 CustomUser를 생성하라고 할 수 있다.

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {

	// .. find messages for this user and return them ...
}

Reference

https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html

반응형