1. Voter에 의한 인가 처리 방식
Voter에 의한 인가처리는 AccessDecisionManager에 의해 접근을 검사하는 것이고, 앞선 글의 계층 권한 처리에서 살펴본 것과 같은 방식으로 처리한다. 따라서 추가적인 Voter 구현체로 IpAddressVoter를 만들어주면 된다. 다시 말해 RoleHierarchyVoter 외에 IpAddressVoter를 추가하여 접속자의 Ip도 검사하는 방식이 된다.
Voter의 결과 리턴값
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}
AccessDecisionManager에 여러 개의 AccessDecisionVoter를 넣어주고 순차적으로 작동하도록 만든다. 결과값 중 하나인ACCESS_GRANTED는 후순위 Voter를 무시하고 인가처리를 하는 방식이다. 그런데 ACCESS_ABSTAIN을 만들면 일단 해당 Voter에서는 인가를 승인하되, 다음 Voter로 승인을 보류할 수 있다. 따라서 선순위 Voter에서는 ACCESS_ABSTAIN을 리턴하도록 하고, Voter들간 순서도 잘 구성하도록 해주는 것이 중요하다.
반대로 ACCESS_DENIED는 일단 해당 Voter에서는 인가를 거부하되, 다른 Voter로 판단을 넘기게 된다. 만약 Ip가 맞지 않으면 접근을 제한하고 싶은 경우, 다음 Voter로 넘기는 것이 아니라 AccessDeniedException을 발생시키도록 해주어야 한다. 물론 AccessDecisionManager의 인가 처리 전략을 AffirmativeBased가 아닌 다른 전략으로 바꿔주는 것도 방법일 수 있다.
2. IpAddressVoter 추가
IpAddressVoter를 추가한다. 이전 글의 RoleHierarchyVoter를 추가하는 방식과 유사하다.
AccessIp 엔티티 생성 및 테이블 추가
AccessIp 엔티티를 생성하고 JPA ddl-auto 전략을 create로 하여 테이블을 생성한다.
@Entity
@Table(name = "ACCESS_IP")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class AccessIp implements Serializable {
@Id
@GeneratedValue
@Column(name = "IP_ID", unique = true, nullable = false)
private Long id;
@Column(name = "IP_ADDRESS", nullable = false)
private String ipAddress;
}
IpAddress data setup
SetupDataLoader 파일에 DB에 추가될 기본 ip 주소 데이터를 추가해준다. Local로 접속 시 ipAddress는 0:0:0:0:0:0:0:1 이 된다.
@Component
@RequiredArgsConstructor
public class SetupDataLoader implements ApplicationListener<ContextRefreshedEvent> {
private boolean alreadySetup = false;
private final AccountRepository accountRepository;
private final RoleRepository roleRepository;
private final ResourcesRepository resourcesRepository;
private final RoleResourceRepository roleResourceRepository;
private final AccountRoleRepository accountRoleRepository;
private final PasswordEncoder passwordEncoder;
private final RoleHierarchyRepository roleHierarchyRepository;
private final AccessIpRepository accessIpRepository;
@Override
@Transactional
public void onApplicationEvent(final ContextRefreshedEvent event) {
if (alreadySetup) {
return;
}
setupSecurityResources();
setupAccessIpData();
alreadySetup = true;
}
//...중략
public void setupAccessIpData() {
AccessIp byIpAddress = accessIpRepository.findByIpAddress("0:0:0:0:0:0:0:1");
if (byIpAddress == null) {
AccessIp accessIp = AccessIp.builder()
.ipAddress("0:0:0:0:0:0:0:1")
.build();
accessIpRepository.save(accessIp);
}
}
IpAddressVoter 작성
IpAddressVoter를 작성한다. 계층 권한에서 살펴본 RoleHierarchyImpl과 같은 구현체가 있는 것이 아니기 때문에 AccessDecisionVoter를 상속받는 구현체를 만드는 것이다. Override하는 supports 메서드들은 true값을 리턴하도록 만들고, voter 메서드를 작성한다.
authentication 객체에서 .getDetails() 메서드를 활용하여 사용자의 ip 주소를 얻어올 수 있다. 다만 Object 타입으로 조회되기 때문에 get 메서드 활용을 위해서 WebAuthenticationDetails 타입으로 캐스팅이 필요하다.
DB에 저장된 IP라면 ACCESS_ABSTAION을 반환하여 인가 결정을 미루고, 그렇지 않다면 AccessDeniedException을 던지도록 한다.
@RequiredArgsConstructor
public class IpAddressVoter implements AccessDecisionVoter {
private final SecurityResourceService securityResourceService;
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class aClass) {
return true;
}
@Override
public int vote(Authentication authentication, Object o, Collection collection) {
//authentication.getDetails에서 사용자 정보를 얻을 수 있으며, WebAuthenticationDetails 객체로 캐스팅 해줘야 다양한 메서드 사용 가능
WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
String remoteAddress = details.getRemoteAddress();
List<String> accessIpList = securityResourceService.getAccessIpList();
for (String accessIp : accessIpList) {
if (accessIp.equals(remoteAddress)) {
return ACCESS_ABSTAIN;
}
}
throw new AccessDeniedException("Invalid Ip Address");
}
}
SecurityConfig에 추가
마지막으로 SecurityConfig에 accessDecisionVoters 목록에 추가한다. 인가 결정을 보류하는 로직이 있기 때문에, 반드시 roleVoter 앞에 위치하도록 해야한다.
private List<AccessDecisionVoter<?>> getAccessDecisionVoters() {
List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
accessDecisionVoters.add(new IpAddressVoter(securityResourceService));
accessDecisionVoters.add(roleVoter());
// return Arrays.asList(new RoleVoter());
return accessDecisionVoters;
}
3. 실습 결과, AffirmativeBased
실습을 위해서 AffirmativeBased 클래스에서 break point를 잡는다. 내용을 살펴보면 AccessDecisionVoter에서 반환하는 ACCESS_GRANTED 등의 int 값에 따라 인가처리가 분기되어 있음을 확인할 수 있다. 그리고 아래 콘솔에서 authentication 객체 및 접근 경로, details 내부의 remoteAddress 정보 등을 확인하는 것을 볼 수 있다.
resources로 등록된 /mypage, /messages에 다른 ip로 접근 시, Exception이 발생하여 exceptionHandler에 의해 처리가 된 것을 확인할 수 있었다.
참조
1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의
https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%E
'Programming-[Backend] > Spring Security' 카테고리의 다른 글
[스프링 시큐리티]24. Method 방식 : 어노테이션 API 이해 (0) | 2022.02.28 |
---|---|
[스프링 시큐리티]23. Method 방식 : 동작방식 및 구조 알아보기 (0) | 2022.02.27 |
[스프링 시큐리티]21. 계층 권한 (0) | 2022.02.25 |
[스프링 시큐리티]20. 실시간 권한 업데이트, 허용필터 (0) | 2022.02.20 |
[스프링 시큐리티]19. DB 연동 인가처리 -FilterInvocationSecurityMetadataSource 구현 (0) | 2022.02.16 |