스프링에 대해서 여러가지 공부를 하던중, 정리 해놓으면 좋을 것 같은 주제를 정리하려고 한다.
이번에 다를 주제는 @Secured(),@PreAuthorize, @PostAuthorize이다.
해당 주제에 대해서 작성을 하게 된 이유는 인가처리 때문이다. 인가 처리에 대한 부분을 메서드 부분에서 작성하게 된다면 비지니스 로직과 겹치게 되어, 굉장히 복잡한 코드가 작성되게 된다. 그래서 어떻게 해결할까에 대해서 공부를 해보았고, 해당 어노테이션을 이용해서 인가에 대한 처리를 해주면, 깔끔하게 코드를 작성할 수 있어 해당 내용을 공부하고 정리해보려고 한다.
// SecurityConfig.java
@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됨.
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
SecurityConfig 파일에 @EnableMethodSecurity(securedEnabled = true, prePostEnabled = true) 와 같이 설정해 주어야지 어노테이션들을 사용할 수 있다.
@Secured("ROLE_ADMIN")
@GetMapping("/info")
public @ResponseBody String info() {
return "개인정보";
}
자신이 권한을 검사해주고 싶은 곳에 @Secured 애노테이션을 붙여주면, 애노테이션에 인자로 받은 권한이 유저에게 있을 때만 실행하도록 할 수 있다.
먼저, 두 메서드의 차이는 아래와 같다.
그럼 @Secured()도 비슷한 것 같은데 무슨 차이가 있는지 궁금해 할 것이다. 차이점은 아래와 같다.
- hasRole([role]) : 현재 사용자의 권한이 파라미터의 권한과 동일한 경우 true
- hasAnyRole([role1,role2]) : 현재 사용자의 권한디 파라미터의 권한 중 일치하는 것이 있는 경우 true
- principal : 사용자를 증명하는 주요객체(User)를 직접 접근할 수 있다.
- authentication : SecurityContext에 있는 authentication 객체에 접근 할 수 있다.
- permitAll : 모든 접근 허용
- denyAll : 모든 접근 비허용
- isAnonymous() : 현재 사용자가 익명(비로그인)인 상태인 경우 true
- isRememberMe() : 현재 사용자가 RememberMe 사용자라면 true
- isAuthenticated() : 현재 사용자가 익명이 아니라면 (로그인 상태라면) true
- isFullyAuthenticated() : 현재 사용자가 익명이거나 RememberMe 사용자가 아니라면 true
@PostAuthorize("isAuthenticated() and (( returnObject.name == principal.name ) or hasRole('ROLE_ADMIN'))")
@RequestMapping( value = "/{seq}", method = RequestMethod.GET )
public User getuser( @PathVariable("seq") long seq ){
return userService.findOne(seq);
}
@PostAuthorize는 returnObject 예약어로 메서드의 리턴 객체에 접근할수 있다.
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
@PreAuthorize는 파라미터에 접근하기 위해 '#파라미터명'을 사용하여 객체에 접근할수 있다.
내가 생각한 결론은 간단한 인가에 대한 처리의 경우는 @Secure 를 사용하고, 복잡한 인가에 대한 처리는 @Pre/PostAuthorize 를 사용하는 것이다.
public abstract class AccessHandler {
public final boolean check(Long id) {
return hasRole(getRoleTypes()) || isResourceOwner(id);
}
abstract protected List<RoleType> getRoleTypes();
abstract protected boolean isResourceOwner(Long id);
private boolean hasRole(List<RoleType> roleTypes) {
return AuthHandler.extractMemberRoles().containsAll(roleTypes);
}
}
AccessHandler라는 abstract class를 선언하여 멤버, 게시글, 댓글 등 인가에 대한 처리를 손쉽게 할 수 있도록 한다.
@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BoardAccessHandler extends AccessHandler {
private final BoardRepository boardRepository;
private List<RoleType> roleTypes = List.of(RoleType.ROLE_ADMIN);
@Override
protected List<RoleType> getRoleTypes() {
return roleTypes;
}
@Override
protected boolean isResourceOwner(Long id) {
return boardRepository.findById(id)
.map(board -> board.getMember())
.map(member -> member.getId())
.filter(memberId -> memberId.equals(AuthHandler.extractMemberId()))
.isPresent();
}
}
AccessHandler 를 상속받은 BoardAccessHandler를 구현하여 인가에 대한 처리를 수행한다.
/**
* 게시글 수정
*/
@Transactional
@PreAuthorize("@boardAccessHandler.check(#boardId)")
public BoardDetailsResponse update(@Param("boardId")Long boardId, BoardUpdateRequest boardDTO) {
Board board = boardRepository.findById(boardId).orElseThrow(
() -> new BoardNotFoundException(boardId.toString()));
board.update(boardDTO.getTitle(), boardDTO.getContent());
return BoardDetailsResponse.fromEntity(board);
}
/**
* 게시글 삭제
*/
@Transactional
@PreAuthorize("@boardAccessHandler.check(#boardId)")
public void delete(@Param("boardId")Long boardId) {
Board board = boardRepository.findById(boardId).orElseThrow(
() -> new BoardNotFoundException(boardId.toString()));
board.getFiles().forEach(file -> fileService.deleteLocalFile(file));
boardRepository.delete(board);
}
@PreAuthorize 어노테이션을 이용하여 게시글 수정과 삭제에서 인가에 대한 처리를 수행할 수 있도록 한다. 해당 결과로 인가에 대한 처리와 비지니스에 대한 처리에 대한 코드가 분리되도록 구현하였다.