RoleHierarchy를 통해 상위 계층 Role은 하위 계층 Role 자원에 접근이 가능하므로 Resources와 Role은 다대일 단방향으로 구현했다.
스프링 시큐리티6 부터는 FilterSecurityInterceptor이 deprecated 되었다. 따라서 AuthorizationManager를 상속받아 DB로부터 자원 및 권한을 가져와 인가처리하도록 UrlAuthorizationManager
를 구현했다.
public class UrlAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private LinkedHashMap<RequestMatcher, String> requestMap = new LinkedHashMap<>();
private RoleHierarchy roleHierarchy = new NullRoleHierarchy();
public UrlAuthorizationManager(LinkedHashMap<RequestMatcher, String> requestMap) {
this.requestMap = requestMap;
}
public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
@Override
public AuthorizationDecision check(Supplier<Authentication> supplier, RequestAuthorizationContext requestAuthorizationContext) {
Authentication authentication = supplier.get();
HttpServletRequest request = requestAuthorizationContext.getRequest();
if (requestMap != null) {
for (Map.Entry<RequestMatcher, String> entry : requestMap.entrySet()) {
RequestMatcher requestMatcher = entry.getKey();
if (requestMatcher.matches(request)) {
String authority = entry.getValue();
boolean isGranted = isGranted(authentication, authority);
return new AuthorityAuthorizationDecision(isGranted, AuthorityUtils.createAuthorityList(authority));
}
}
}
return new AuthorizationDecision(true);
}
private boolean isGranted(Authentication authentication, String authority) {
return authentication != null && isAuthorized(authentication, authority);
}
private boolean isAuthorized(Authentication authentication, String authority) {
Iterator iter = getGrantedAuthorities(authentication).iterator();
GrantedAuthority grantedAuthority;
do {
if (!iter.hasNext()) {
return false;
}
grantedAuthority = (GrantedAuthority)iter.next();
} while(!authority.equals(grantedAuthority.getAuthority()));
return true;
}
private Collection<? extends GrantedAuthority> getGrantedAuthorities(Authentication authentication) {
return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
}
}
requestMap
에 DB로 부터 가져온 자원/권한 정보가 Map형식으로 담겨있다.entry.getKey()
를 통해 RequestMatcher를 꺼내 사용자 요청 URI와 일치하는지 판단한다.entry.getValue()
를 통해 해당 자원에 해당하는 권한 정보를 꺼낸다. 그리고 해당 자원에 필요한 권한을 사용자가 가졌는지 isAuthorized()
메서드를 통해 확인한다.isGranted = true
, 그렇지 않다면 isGranted = false
AuthroizationDecision(true)
를 반환한다. getGrantedAuthorities()
메서드를 통해 계층@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class SecurityResourceService {
private final ResourcesRepository resourcesRepository;
public LinkedHashMap<RequestMatcher, String> getResourceList() {
LinkedHashMap<RequestMatcher, String> result = new LinkedHashMap<>();
List<Resources> resourcesList = resourcesRepository.findAll();
resourcesList.forEach(re -> {
result.put(
new AntPathRequestMatcher(re.getResourceName()),
re.getRole().getRoleName());
});
return result;
}
}
public interface ResourcesRepository extends JpaRepository<Resources, Long> {
}
public class UrlResourcesMapFactoryBean implements FactoryBean<LinkedHashMap<RequestMatcher, String>> {
private SecurityResourceService securityResourceService;
private LinkedHashMap<RequestMatcher, String> resourceMap;
public void setSecurityResourceService(SecurityResourceService securityResourceService) {
this.securityResourceService = securityResourceService;
}
@Override
public LinkedHashMap<RequestMatcher, String> getObject() {
if(resourceMap == null) {
init();
}
return resourceMap;
}
private void init() {
resourceMap = securityResourceService.getResourceList();
}
@Override
public Class<?> getObjectType() {
return LinkedHashMap.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final SecurityResourceService securityResourceService;
//..생략
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers(new AntPathRequestMatcher("/**")).access(urlAuthorizationManager())
.anyRequest().authenticated()
);
http
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
.usernameParameter("loginId")
.passwordParameter("password")
.loginProcessingUrl("/login")
.permitAll()
);
http
.csrf(CsrfConfigurer::disable);
return http.build();
}
@Bean
public UrlAuthorizationManager urlAuthorizationManager() {
UrlAuthorizationManager urlAuthorizationManager = new UrlAuthorizationManager(urlResourcesMapFactoryBean().getObject());
urlAuthorizationManager.setRoleHierarchy(roleHierarchy());
return urlAuthorizationManager;
}
private UrlResourcesMapFactoryBean urlResourcesMapFactoryBean() {
UrlResourcesMapFactoryBean urlResourcesMapFactoryBean = new UrlResourcesMapFactoryBean();
urlResourcesMapFactoryBean.setSecurityResourceService(securityResourceService);
return urlResourcesMapFactoryBean;
}
@Bean
static RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return hierarchy;
}
}
.requestMatchers(new AntPathRequestMatcher("/**")).access(urlAuthorizationManager())
모든 url을 UrlAurthorizationManagaer를 통해 인가처리한다. UrlAurthorizationManagaer
도 빈으로 등록하다.hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER")
에 원하는 계층구조를 세팅하면된다. >
는 포함한다는 의미로 ROLE_ADMIN이 ROLE_USER 권한도 가진다는 의미한다. 이제 DB를 통해 자원/권한을 설정하고 제어할 수 있게되었다. 추가하고 싶은 자원/권한은 DB에 추가하면 된다. 또한 계층 구조도 >
으로 추가하면된다. 계층 구조는 아래 링크를 참고
Role Hierarchy
참고로 권한을 계층적으로 구성하지 않는다면 ROLE, RESOURCES 가 다대다 관계가 되므로 ROLE_RESOURCES 연결테이블을 추가해서 일대다, 다대일 관계로 풀면된다. 그리고 LinkedHashMap<RequestMatcher, String>을LinkedHashMap<RequestMatcher, List<String>> 으로 바꿔 구현하면 된다.