1. 계층 권한 적용 : RoleHierarchy, RoleHierarchyVoter 개념
User, Manager, Admin 권한이 있을 때, 보통은 Admin 권한이 있다면 User, Manager 권한을 갖도록 설계하는 것이 보통이다. 이런 권한 계층 관리를 해주는 것이 RoleHierarchy 객체이다.
Rolehierarchy 객체는 String 구문으로 작성된다. 예를 들면 다음과 같다.
ROLE_ADMIN > ROLE_MANAGER
ROLE_ADMIN > ROLE_USER
부등호와 줄바꿈(\N) 기호를 통해서 구분하며, 하나의 String으로 작성되어야 한다. 부등호가 큰 쪽이 상위 계층이라는 의미가 된다.
이렇게 작성된 RoleHierarchy 객체를 RoleHierarchyVoter에 적용할 것이다. 그리고 RoleHierarchyVoter를 SecurityConfig에서 기존에 사용하던 Voter에 대신 주입해줄 것이다. 맨 아래 DB 단에서부터 시작해본다.
2. RoleHierarchy Entity 작성 : JPA 계층 구조 Entity
우선 DB에 테이블로 계층 구조를 저장하기 위해서 RoleHierarchy Entity를 작성해본다. 여기서 JPA에서 계층 구조 엔티티를 만들기 위한 연관관계를 적용한다. 이에 대해서는 JPA 학습을 하면서 정리했었다.
https://whitepro.tistory.com/429
@Entity
@Table(name = "role_hierarchy")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleHierarchy {
@Id
@GeneratedValue
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "child_name")
private String childName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_name")
private RoleHierarchy parentName;
@OneToMany(mappedBy = "parentName", fetch = FetchType.LAZY)
private List<RoleHierarchy> roleHierarchy = new ArrayList<>();
}
여기서 childName은 각 RoleHierarchy의 이름, parentName은 해당 RoleHierarchy의 부모 인스턴스의 이름이 된다.
* 강의에서는 parentName의 @JoinColumn 속성에 referencedColumnName = "child_name" 속성을 지정해줬었다. 그래서 아래 표와 같이 테이블상에 childName, parentName이 각 Role의 이름값으로 지정됬다.
그러나 보통 정규화를 위해서 referencedColumnName 값을 사용하는 것은 지양된다. 이 값은 fk로 작동해서 다른 테이블의 pk값을 자동으로 가져오는데, 보통의 경우 pk인 id값 대신 특정 값을 지정해주기 위해서 사용된다. 예를 들어 team-memeber 1:N 관계에서 member객체의 id값에 @JoinColumn(referecedColumnName="name") 값을 적용하면 member-team 조인 시 team의 name 값을 기준으로 조인하게 된다. 보통은 따로 지정하지 않고, 이런 경우에는 자동으로 pk값을 지정하여 team.teamId(pk)를 기준으로 조인하게 된다. 더 자세한 설명은 참조2.
3. RoleHierarchyService, 기초 데이터 setUp
RoleHierarchyService
이제 SecurityConfig에서 권한 계층을 설정해주기 위해서 RoleHierarchyService를 만들어준다. 앞서 언급한 내용대로 spring security에서 지정한 규칙대로 roleHierarchy 리스트를 정규화된 String으로 표현해주었다.
@RequiredArgsConstructor
@Service
public class RoleHierarchyServiceImpl implements RoleHierarchyService {
private final RoleHierarchyRepository roleHierarchyRepository;
@Transactional
@Override
public String findAllHierarchy() {
List<RoleHierarchy> roleHierarchies = roleHierarchyRepository.findAll();
Iterator<RoleHierarchy> itr = roleHierarchies.iterator();
StringBuilder concatedRoles = new StringBuilder();
while(itr.hasNext()) {
RoleHierarchy roleHierarchy = itr.next();
if (!Objects.isNull(roleHierarchy.getParentName())) {
concatedRoles.append(roleHierarchy.getParentName().getChildName());
concatedRoles.append(" > "); //spring security 규칙
concatedRoles.append(roleHierarchy.getChildName());
concatedRoles.append("\n");
}
}
return concatedRoles.toString();
}
}
기초 데이터 Setup
다음으로 기초 데이터 setup을 해준다. 앞서도 다뤘지만, 이 부분은 DB상에 데이터가 없을 때 초기값으로 데이터를 넣어주기 위한 부분일 뿐이며, 만약 DB상에 데이터가 있다면 필수적인 부분은 아니다. 각 parent, child RoleHierarchy가 있는지 확인하고 insert 해준다. child의 경우 parent도 set 해준다.
//생략
//DB상 초기 데이터를 넣어주기 위한 부분일 뿐, 만약 DB상에 데이터가 있다면 필수적인 코드는 아님
private void setupSecurityResources() {
Role adminRole = createRoleIfNotFound("ROLE_ADMIN");
//중략
createUserIfNotFound("user", "pass", "user@gmail.com", 12, userRole);
createRoleHierarchyIfNotFound(userRole, adminRole);
createRoleHierarchyIfNotFound(managerRole, adminRole);
//중략...
@Transactional
public void createRoleHierarchyIfNotFound(Role childRole, Role parentRole) {
//parent
RoleHierarchy roleHierarchy = roleHierarchyRepository.findByChildName(parentRole.getRoleName());
if (roleHierarchy == null) {
roleHierarchy = RoleHierarchy.builder().childName(parentRole.getRoleName()).build();
}
RoleHierarchy parentRoleHierarchy = roleHierarchyRepository.save(roleHierarchy);
//child
roleHierarchy = roleHierarchyRepository.findByChildName(childRole.getRoleName());
if (roleHierarchy == null) {
roleHierarchy = RoleHierarchy.builder().childName(childRole.getRoleName()).build();
}
RoleHierarchy childRoleHierarchy = roleHierarchyRepository.save(roleHierarchy);
childRoleHierarchy.setParentName(parentRoleHierarchy);
}
4. Initializer 작성
ApplicationRunner에서 권한 계층 정보 전달
권한 계층 정보를 RoleHierarchy 인스턴스에 전달해야한다. 어떤 타이밍이든 상관없지만, 강의에서는 서버가 뜰 때 ApplicationRunner의 run 메서드를 이용해서 권한 계층 정보를 RoleHierarchy 객체로 전달한다.
//ApplicationRunner를 implemenets 받아와서 run 메서드를 Override 해주면, 서버 기동 시 해당 메서드를 실행한다.
@Component
@RequiredArgsConstructor
public class SecurityInitializer implements ApplicationRunner {
private final RoleHierarchyService roleHierarchyService;
private final RoleHierarchyImpl roleHierarchy;
@Override
public void run(ApplicationArguments args) throws Exception {
String allHierarchy = roleHierarchyService.findAllHierarchy();
roleHierarchy.setHierarchy(allHierarchy);
}
}
여기서 유의할 점은 RoleHierarchy를 주입받을 때 RoleHierarchyImpl 구현체로 주입받는다는 점이다. 구현체의 setHierarchy 메서드를 사용하기 위해서 구현체를 직접 주입받았다.
5. SecurityConfig 설정
마지막으로 SecurityConfig 설정 부분을 추가한다. 기존에 getAccessDecisionVoters에서 단순히 new RoleVoter()를 지정해주던 부분을, 이제 직접 지정한 RoleHierarchyVoter가 주입되도록 바꾸어준다.
@Bean
public FilterSecurityInterceptor customFilterSecurityInterceptor() throws Exception {
// FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
// //3가지 속성을 설정해주어야 한다.
// filterSecurityInterceptor.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
// filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
// filterSecurityInterceptor.setAuthenticationManager(authenticationManagerBean());
//
// return filterSecurityInterceptor;
//permitAllFilter 적용
PermitAllFilter permitAllFilter = new PermitAllFilter(permitAllResources);
permitAllFilter.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
permitAllFilter.setAccessDecisionManager(affirmativeBased());
permitAllFilter.setAuthenticationManager(authenticationManagerBean());
return permitAllFilter;
}
private AccessDecisionManager affirmativeBased() {
return new AffirmativeBased(getAccessDecisionVoters());
}
private List<AccessDecisionVoter<?>> getAccessDecisionVoters() {
List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
accessDecisionVoters.add(roleVoter());
// return Arrays.asList(new RoleVoter());
return accessDecisionVoters;
}
@Bean
public AccessDecisionVoter<? extends Object> roleVoter() {
return new RoleHierarchyVoter(roleHierarchy());
}
@Bean
public RoleHierarchyImpl roleHierarchy() { //RoleHierarchyImpl 구현체의 메서드를 활용하기 위해서 Impl 구현체를 리턴
return new RoleHierarchyImpl();
}
@Bean
public FilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource() throws Exception {
return new UrlFilterInvocationMetadataSource(urlResourcesMapFactoryBean().getObject(), securityResourceService);
}
private UrlResourceMapFactoryBean urlResourcesMapFactoryBean() {
UrlResourceMapFactoryBean urlResourceMapFactoryBean = new UrlResourceMapFactoryBean();
urlResourceMapFactoryBean.setSecurityResourceService(securityResourceService);
return urlResourceMapFactoryBean;
}
전체 과정을 다시 한 번 살펴보자. SecurityConfig 설정 시 Filter 목록에 FilterSecurityInterceptor를 추가하고자 했었다. 여기서 해당 Filter의 속성값 중 하나인 AccessDecisionManager를 설정하는 부분에서 어떤 전략을 취할지 결정 했었고, AffirmativeBased로 정했다. 그리고 여기에 주입받은 AccessDecisionVoter를 RoleHierarchy를 이용한 구조로 주입하도록 바꿔주었다.
-> 이제 부모 권한인 Admin으로 로그인해도, User 권한이 있어야하는 /mypage나 Manager 권한이 있어야하는 /messsages 자원에 접근이 가능하다.
참조
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
2) 인프런 - JPA 김영한님 강의 - 질문과 답변
https://www.inflearn.com/questions/113969
'Programming-[Backend] > Spring Security' 카테고리의 다른 글
[스프링 시큐리티]23. Method 방식 : 동작방식 및 구조 알아보기 (0) | 2022.02.27 |
---|---|
[스프링 시큐리티]22. 아이피 접속 제한 ; AccessDecisionVoter 추가 (0) | 2022.02.25 |
[스프링 시큐리티]20. 실시간 권한 업데이트, 허용필터 (0) | 2022.02.20 |
[스프링 시큐리티]19. DB 연동 인가처리 -FilterInvocationSecurityMetadataSource 구현 (0) | 2022.02.16 |
[스프링 시큐리티]18. DB 연동 인가처리 - 스프링 MVC 복습, FilterInvocationSecurityMetadataSource 기초 구현 (0) | 2022.01.25 |