SpringBoot 프로젝트에 Security를 적용하고 권한 관리할 때 사용할 수 있는 어노테이션인 @Secured, @PreAuthorize, @PostAuthorize를 알아보자.

 

Spring Security의 API 권한 관리 방식은 SecurityConfig에 설정하는 방법과 메소드 수준의 권한 관리 방법이 있다.

SecurityConfig 권한 관리

@Configuration
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(
                        authorize -> authorize
                                .requestMatchers("/api/{version}/swagger/**", "/api/{version}/login/**").permitAll()
                                .requestMatchers("/api/{version}/member/**").hasAnyRole(Role.SUPER.getRole(), Role.ADMIN.getRole())
                                .anyRequest().authenticated()
                )
                
                ...
                .build();
    }
    
    ...
}

보통은 SecurityConfig에서 SecurityFilterChain에 authorizeHttpRequests()로 API 엔드포인트에 대한 권한을 설정한다.

 

메소드 수준의 API 권한 관리

@Configuration
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig {
	...
}

메소드에 @Secured, @PreAuthorize, @PostAuthorize 어노테이션을 적용하기 위해서는 위와 같이 활성화시켜줘야 한다.

@PreAuthorize, @PostAuthorize를 활성화시키는 prePostEnabled는 default 값이 true이므로 굳이 true로 설정하지 않아도 된다.

 

Method Security를 활성화했으면 이제 메소드에 적용만 하면 된다. 매우 간편하다.

@Secured

@Secured 어노테이션은 Spring EL(표현식)이 사용 불가하고 , 로 접근 가능한 권한들을 OR로만 설정 가능하다.

// ROLE_ADMIN OR ROLE_SUPER
@Secured({"ROLE_ADMIN", "ROLE_SUPER"})
@GetMapping("/search")
public ResponseEntity<Object> searchMember() {
    return ResponseEntity.ok(memberService.searchMember());
}
{
  "result": false,
  "code": "403",
  "message": "접근 권한이 없습니다."
}

ROLE_USER 권한으로 해당 API 호출 시 정상적으로 403 Forbidden 에러를 응답받는다.

 

@PreAuthorize, @PostAuthorize

이 두 어노테이션은 Spring EL(표현식)을 사용할 수 있고, AND, OR 사용이 가능하다.

 

Spring EL

hasRole([role]) 사용자의 권한과 동일한 경우
hasAnyRole([role1, role2, ...]) 사용자의 권한과 일치하는 것이 있는 경우
principal User 객체
authentication Security Context의 Authentication 객체
permitAll 모든 접근 허용
denyAll 모든 접근 비허용
isAnonymous() (비로그인) 사용자가 익명인 경우
isRememberMe() 사용자가 RememberMe인 경우
isAuthenticated() (로그인) 사용자가 익명이 아닌 경우 / 어떤 권한이든 인증된 사용자
isFullyAuthenticated() 사용자가 익명이거나 RememberMe가 아닌 경우

 

@PreAuthorize

@PreAuthorize("#userId == principal.username") // #1
@PreAuthorize("isAuthenticated()) // #2
@PreAuthorize("hasAnyRole('ROLE_ADMIN')") // #3
@GetMapping("/search")
public ResponseEntity<Object> searchMember(@RequestParam String userId) {
    return ResponseEntity.ok(memberService.searchMember());
}

이런 메소드가 있다고 했을 때 @PreAuthorize의 경우 #1, #2, #3처럼 사용할 수 있다.

#1 : '#'을 통해 값을 가져올 수 있다. userId가 인증객체의 username과 같은지 비교

#2 : 어떤 권한이든 상관없이 인증 유무 확인

#3 : 'ROLE_ADMIN' 권한 체크

 

@PostAuthorize

@PostAuthorize("returnObject.userId == principal.username")
@GetMapping("/{id}")
public MemberDto getMemberInfo(@PathVariable Long id) {
    return memberService.getMemberInfo(id);
}

@PostAuthorize는 returnObject를 통해 반환 값을 가져올 수 있다. 

예시 코드처럼 MemberDto의 userId와 인증객체의 username이 같은지 비교하여 권한 수준을 관리한다.

 

{
  "result": false,
  "code": "403",
  "message": "접근 권한이 없습니다."
}

@PreAuthorize, @PostAuthorize 조건에 부합하지 않은 경우 403 Forbidden 에러를 반환한다.

 

 

끝.