이번 포스트에서는 @Controller에서 Spring Security 사용에 대해 알아보자.
Controller에서의 Spring Security
UserDetails 활용
Controller의 handler method에서 UserDetails를 파라미터로 받아서 활용할 수 있다. 이때 파라미터에는 @AuthenticationPrincipal 이라는 애너테이션이 선언되어 있어야 한다. 따라서 로그인 사용자 정보를 찾아서 더이상 직접 HttpSession을 사용하지 않아도 된다.
@GetMapping({"/", "/index"})
public String home(Model model, HttpSession session,
@AuthenticationPrincipal UserDetails userDetails) {
model.addAttribute("type", "home");
Object user = session.getAttribute("SPRING_SECURITY_CONTEXT");
Optional.ofNullable(user)
.ifPresent(
u -> {
Object principal = ((SecurityContext) u).getAuthentication().getPrincipal();
System.out.println(principal == userDetails);
});
Optional.ofNullable(userDetails)
.ifPresent(
u -> {
log.debug(
"사용자명: {}, 권한: {}", userDetails.getUsername(), userDetails.getAuthorities());
});
return "index";
}
Controller의 메서드 레벨에서 접근 제어
전역 레벨 설정 vs 메서드 레벨 설절
HttpSecurity를 통한 설정은 전역 레베렝서 Spring Security를 선언하는 방식이다. 이 방식은 다음의 특징을 갖는다.
- 전역적이고 선언적인 보안 규칙 설정
- URL 기반의 접근 제어
- 필터 체인을 통한 전체 애플리케이션 보안 설정
추가로 Controller의 요청 처리 메서드 호출 전/후에 권한을 체크하기 위해서는 @PreAuthorize와 @PostAuthorize를 사용할 수도 있다. 메서드 레벨로 접근할 때는 다음의 특징을 갖는다.
- 메서드 레벨의 세밀한 접근 제어
- 비지니스 로직 내부의 구체적인 권한 체크
- 메서드 실행 전/후의 권한 검증 가능
따라서 단순한 URL 기반의 보안이라면 HttpSecurity로 충분하지만 좀 더 세밀하고 복잡한 권한 제어가 필요하다면 @PreAuthorize등의 메서드 레벨 보안을 추가로 사용하는 것이 좋다.
@PreAuthorize와 @PostAuthorize
메서드 수준에서 Spring Security를 사용하기 위해서는 @EnableMethodSecurity 애너테이션이 설정되어있어야 한다.
@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity(prePostEnabled = true) // 메서드 레벨의 제어를 위한 설정
public class SecurityConfig { . . . }
@PreAuthorize와 @PostAuthorize 애너테이션들은 SPEL이라는 것을 이용해서 복합적으로 인가 정보를 확인한다. 대부분 메서드가 문자열로 사용되는 형태라 낯설지 않을 것이다.
표현식 |
내용 |
hasRole(role) |
역할이 부여된 권한과 일치하는지 확인함 예) "hasRole('ADMIN')" |
hasAnyRole(role, ... ) |
부여된 역할 중 일치하는 항목이 있는지 확인함 예) "hasAnyRole('ADMIN','STAFF')" |
isAuthenticated() |
인증된 사용자에 한함 예) "isAuthenticated()" |
authentication |
Authentication 객체 예) "authentication.credentials", "authentication.authenticated" |
principal |
Authentication의 getPrincipal() 실행 결과 예) "principal.username", "principal.enabled" |
Spring Security에서 SPEL은 and나 or를 이용한 연산이 가능해서 복잡한 권한 설정에 아주 유용하다.
"hasRole('admin') and #user == principal.username"
다음은 @PreAuthorize가 사용되는 형태이다.
@PreAuthorize("isAuthenticated()")
@GetMapping("/userinfo")
public @ResponseBody UserDetails printUserInfo(@AuthenticationPrincipal UserDetails user) {
return user;
}
/user는 인증이 완료된 경우만 동작하며 인증자의 정보를 REST 형태로 반환한다. 인증이 안된 경우라면 login 페이지로 이동한다.
@PreAuthorize("hasRole('ADMIN') and #user == principal.username")
@GetMapping("/update")
public @ResponseBody String updateUser(@RequestParam String user, @RequestParam String pass) {
return String.format("%s의 비밀번호를 %s로 변경합니다.", user, pass);
}
위 예는 ADMIN role이 있고 파라미터인 user가 로그인 사용자의 username과 같다면 사용할 수 있다는 뜻이다. 메서드의 파라미터에 접근하기 위해서는 #파라미터 명 형태로 사용한다. 만약 권한이 없는 경우라면 Access-Denied가 발생한다.
@PostAuthorize는 메서드 실행 후에 권한을 확인한다. 메서드 내에서 인증 정보를 변경한 후 그 결과에 대해 접근 권한을 확인할 때 유용하다.
@Secured는 레거시 옵션으로 @PreAuthorize의 사용이 권장된다.