db와 연동하여 자원 및 권한을 설정하고 제어함으로 동적 권한 관리가 가능하도록 한다.
antMatcher("/user").hasRole("USER") 없앰
웹 게반 인가처리 DB 연동 - 주요 아키텍처 이해
이제 db를 사용하니 http.anMatchers를 사용안할거지만 , 원래는 저렇게 configure해놓으면 Map으로 들고 있다가 AccessDecisionManager에게 비교할 수있게 ref를 제공해준다.
접근을 시도하려는 request's authentication의 authority를 확인하여 허용해줄지 말지를 결정하게 된다.
MapBasedMethodSecurityMetadataSource 를 통해서 db에서 authority를 추출하는 방식을 사용할 수 있다.
위에 3개는 이미 구현완료된 annotation을 처리해주는 클래스이다.
웹 기반 인가처리 DB 연동 - FilterInvocationSecurityMetadataSource - 1
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap = new LinkedHashMap<>();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
HttpServletRequest request = ((FilterInvocation) object).getRequest();
requestMap.put(new AntPathRequestMatcher("/mypage"), Arrays.asList(new SecurityConfig("ROLE_USER")));
if(requestMap != null){
for(Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap.entrySet()){
RequestMatcher matcher = entry.getKey();
if(matcher.matches(request)){
return entry.getValue();
}
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet<>();
for (Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap
.entrySet()) {
allAttributes.addAll(entry.getValue());
}
return allAttributes;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
사용자가 접근하고자 하는 URL 자원에 대한 권한 정보를 추출하고 AccessDecisionManager에게 전달하여 인가처리 수행
DB로부터 자원 및 권한정보를 매핑하여 맵으로관리하고 매 요청마다 확인하는 용도로 사용됨
FilterSecurityInterceptor를 내것을 하나만들어서 기존 FilterSecurityInterceptor앞에 놓으면 내 필터만 처리하고 2번쨰 기존것으로 넘어갈때는 이미 한번 처리했으니 바로 다음으로 넘어가게 된다.
public class UrlResourcesMapFactoryBean implements FactoryBean<LinkedHashMap<RequestMatcher, List<ConfigAttribute>>> {
private SecurityResourceService securityResourceService;
private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourceMap;
public UrlResourcesMapFactoryBean(SecurityResourceService securityResourceService) {
this.securityResourceService = securityResourceService;
}
@Override
public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getObject() throws Exception {
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;
}
}
보통 Map 자료구조 자체를 bean으로 등록하지 않는다. requestMap에 채워질 resourceMap을 db에서 가져와야하기 때문에 securityResourceService를 별도로 구성한다.
public class SecurityResourceService {
private ResourcesRepository resourcesRepository;
public SecurityResourceService(ResourcesRepository resourcesRepository) {
this.resourcesRepository = resourcesRepository;
}
@Transactional
public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getResourceList() {
LinkedHashMap<RequestMatcher,List<ConfigAttribute>> result = new LinkedHashMap<>();
List<Resources> resourceList = resourcesRepository.findAllResources();
resourceList.forEach(re -> {
List<ConfigAttribute> configAttributesList = new ArrayList<>();
re.getResourcesRoles().forEach(resourcesRole -> {
Role role = resourcesRole.getRole();
configAttributesList.add(new SecurityConfig(role.getRoleName()));
result.put(new AntPathRequestMatcher(re.getResourceName()),configAttributesList);
});
});
return result;
}
}
db 에 저장되어있는 resource 에 대한 authority 정보들을 파싱해서 map에 담는 과정이다.
@Bean
public SecurityResourceService securityResourceService(ResourcesRepository resourcesRepository) {
return new SecurityResourceService(resourcesRepository);
}
http.apply(new CustomFilterSecurityInterceptorDsl(securityResourceService(resourcesRepository)));
강의처럼 AppConfig를 따로만들어서 SecurityResouceService를 생성하려 하니 injection 순서가 꼬이게 된다.
하나의 Config파일에 같이 넣어줬다.
웹 기반 인가처리 실시간 반영하기
controller 단에서 admin 계정으로 권한 정보 update요청이 들어오면 런타임에 requestMap을 비워주고 새로 반영된 정보를 db에서 다시 읽어온다.
인가처리 허용 필터 - PermitAllFilter 구현
FilterSecurityInterceptor를 상속한 PermitAllFilter를 만들어서 beforeInvcation을 오버라이드 한다.
@Override
protected InterceptorStatusToken beforeInvocation(Object object) {
boolean permitAll = false;
HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (RequestMatcher requestMatcher : permitAllRequestMatcher) {
if (requestMatcher.matches(request)) {
permitAll = true;
break;
}
}
if (permitAll) {
return null;
}
return super.beforeInvocation(object);
}
기존에는 super.beforeInvocation으로 부모 클래스로 가서 기존 access처리를 했었지만 해당 필터에서 request matching을 먼저하고 null을 리턴함으로 권한 처리 페이즈 까지 안가게 할 수있다.
FilterSecurityInterceptor 애는 한번만 실행되기 때문이다.
permitall 인 만큼 , "/", "/login", "/user/login/**" 과 같은 url들은 굳이 권한 처리 로직을 거치지 않게 할 수 있는것이다.
계층 권한 적용하기 - RoleHierarchy
원래 권한은 독립적으로 존재하지만 계층형으로도 만들 수 있다.
ROLE_ADMIN > ROLE_MANAGER
ROLE_MANAGER > ROLE_USER
해당 포맷으로 만들고 RoleHierarchyImpl.setHierarchy를 호출하면 계층형을 만들어 준다.
@Component
public class SetupDataLoader implements ApplicationListener<ContextRefreshedEvent> {
@Transactional
public void createRoleHierarchyIfNotFound(Role childRole, Role parentRole) {
RoleHierarchy roleHierarchy = roleHierarchyRepository.findByChildName(parentRole.getRoleName());
if (roleHierarchy == null) {
roleHierarchy = RoleHierarchy.builder()
.childName(parentRole.getRoleName())
.build();
}
RoleHierarchy parentRoleHierarchy = roleHierarchyRepository.save(roleHierarchy);
roleHierarchy = roleHierarchyRepository.findByChildName(childRole.getRoleName());
if (roleHierarchy == null) {
roleHierarchy = RoleHierarchy.builder()
.childName(childRole.getRoleName())
.build();
}
RoleHierarchy childRoleHierarchy = roleHierarchyRepository.save(roleHierarchy);
childRoleHierarchy.setParentName(parentRoleHierarchy);
}
해당 클래스에서 ApplicationContext가 초기화될때 = AbstractApplicationContext의 refresh 메소드에서 Bean들을 다 초기화하고 제일 마지막에 finishRefresh를 호출할 때 이벤트를 발생시키는 시점이다.
이때 createRoleHierarchyIfNotFound를 호출하여 db에 저장해둔다.
@RequiredArgsConstructor
@Component
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);
}
}
db에 저장해둔것을 꺼낼때 위 사진의 Format처럼 꺼내와서 setting한다.
아이피 접속 제한하기 - CustomIpAddressVoter
기존의 vote와 다르게 여러 voter들 중에 가장 먼저 심사하게 해야하고 심사를 통과하더라도 ACCESS_ABSTAIN을 주어서 나머지 추가 심사를 진행하도록 해야한다.
@RequiredArgsConstructor
public class IpAddressVoter implements AccessDecisionVoter<Object> {
private final SecurityResourceService securityResourceService;
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
WebAuthenticationDetails details = (WebAuthenticationDetails)authentication.getDetails();
String remoteAddress = details.getRemoteAddress();
List<String> accessIpList = securityResourceService.getAccessIpList();
int result = ACCESS_DENIED;
for (String ipAddress : accessIpList) {
if (remoteAddress.equals(ipAddress)) {
return ACCESS_ABSTAIN;
}
}
if (result == ACCESS_DENIED) {
throw new AccessDeniedException("Invalid IpAddress");
}
return result;
}
}
AccessDecsionVoter를 구현하고 허용 가능한 ip가 존재할 경우에만 ACCESS_ABSTAIN을 return하여 추가적인 Voter에게 검사를 받을 수 있도록한다.
private List<AccessDecisionVoter<?>> getAccessDecisionVoters() {
List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
accessDecisionVoters.add(new IpAddressVoter(securityResourceService));
accessDecisionVoters.add(roleVoter());
return accessDecisionVoters;
}
@Override
public void configure(HttpSecurity http) throws Exception {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
// here we lookup from the ApplicationContext. You can also just create a new instance.
FilterSecurityInterceptor filterSecurityInterceptor = new PermitAllFilter(permitAllResources);
filterSecurityInterceptor.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
filterSecurityInterceptor.setSecurityMetadataSource(new UrlFilterInvocationSecurityMetadataSource(urlResourcesMapFactoryBean().getObject(), securityResourceService));
filterSecurityInterceptor.setAccessDecisionManager(new AffirmativeBased(getAccessDecisionVoters()));
http.addFilterBefore(filterSecurityInterceptor, FilterSecurityInterceptor.class);
}
security dsl에서 (init,configure 해주는 클래스) , 예를들면 filter설정을 할때 setAccessDecisionManager 에서 해당 voter를 추가한 다음 넣어준다.
'WEB > Security' 카테고리의 다른 글
실전프로젝트 - 인가 프로세스 DB 연동 서비스 계층 구현 (0) | 2023.01.20 |
---|---|
실전프로젝트 - 인증 프로세스 Ajax 인증 구현 (0) | 2023.01.15 |
스프링 시큐리티 주요 아키텍처 이해 (0) | 2023.01.10 |
스프링 시큐리티 기본 API 및 Filter 이해 (안겹치는것만) (0) | 2023.01.08 |
OAuth 2.0 Client + Resource Server + Authorization Server 연동 (0) | 2022.12.28 |