카테고리 없음

[Spring Security] DB로 자원(권한) 관리하기

빅콜팝 2023. 2. 16. 23:27
728x90
반응형

SecurityConfig 설정

@Configuration
@EnableWebSecurity
@Order(0)
@Slf4j
public class AjaxSecurityConfig extends WebSecurityConfigurerAdapter {
    private String[] permitAllResources = {"/api/public/**","/sample","/aws/**","/sample/**", "/login/**", "/outer/**", "/fonts/**", "/landing/**", "/error/**", "/aws/health/check"};

    @Autowired
    private SecurityResourceService securityResourceService;

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 정적 파일은 보안 필터 거치지 않음
        web.ignoring()
                .antMatchers(permitAllResources)
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }

    @Bean
    private AuthenticationProvider authenticationProvider() {
        return new CustomAuthenticationProvider();
    }

    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception { // username, password
        http
                .authorizeRequests()
                .anyRequest().authenticated();

        http
                .addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class);

        http
                .addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class); // ajax 로그인

        http
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler())
        ;

        http
                .csrf().disable(); // post 방식일 때는 필수


    }

    @Override
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
        AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
        ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManager());
        ajaxLoginProcessingFilter.setAuthenticationSuccessHandler(ajaxAuthenticationSuccessHandler());
        ajaxLoginProcessingFilter.setAuthenticationFailureHandler(ajaxAuthenticationFailureHandler());
        return ajaxLoginProcessingFilter;
    }

    @Bean
    public AuthenticationSuccessHandler ajaxAuthenticationSuccessHandler(){
        return new AjaxAuthenticationSuccessHandler();
    }

    @Bean
    public AuthenticationFailureHandler ajaxAuthenticationFailureHandler(){
        return new AjaxAuthenticationFailureHandler();
    }
    @Bean
    private AccessDeniedHandler accessDeniedHandler() {
        AccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler();
        ((CustomAccessDeniedHandler) accessDeniedHandler).setErrorPage("/denied");
        return accessDeniedHandler;
    }

    //    db 동적 빈

    /**
     * DB 인가 처리
     * @return
     * @throws Exception
     */
    @Bean
    public PermitAllFilter customFilterSecurityInterceptor() throws Exception {
        PermitAllFilter permitAllFilter = new PermitAllFilter(permitAllResources);
        permitAllFilter.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
        permitAllFilter.setAccessDecisionManager(affimativeBased()); // 접근 결정 관리자 affimativeBased : ROLE 하나만 만족해도 인가됨
        permitAllFilter.setAuthenticationManager(authenticationManagerBean()); // 인가 전 인증 매니저
        return permitAllFilter;
    }

    private AccessDecisionManager affimativeBased() {
        return new AffirmativeBased(getAccessDecistionVoters());
    }

    private List<AccessDecisionVoter<?>> getAccessDecistionVoters() {
        // role 상위 계층 구현
        List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
        accessDecisionVoters.add(roleVoter());
        return accessDecisionVoters;
    }

    @Bean
    private AccessDecisionVoter<? extends Object> roleVoter() {
        RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
        return roleHierarchyVoter;
    }

    @Bean
    public RoleHierarchyImpl roleHierarchy() {
        return new RoleHierarchyImpl();
    }

    @Bean
    private FilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource() throws Exception {
        // db로 받은 Map 정보 전달
        return new UrlFilterInvocationSecurityMetadataSource(urlResourcesMapFactoryBean().getObject(), securityResourceService);
    }

    private UrlResourcesMapFactoryBean urlResourcesMapFactoryBean() {
        UrlResourcesMapFactoryBean urlResourcesMapFactoryBean = new UrlResourcesMapFactoryBean();
        urlResourcesMapFactoryBean.setSecurityResourceService(securityResourceService);
        return urlResourcesMapFactoryBean;
    }
}

 

CustomFilterSecurityInterceptor.class 가 FilterSecurityInterceptor.class 실행되기 전에 실행됨

http
        .addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class);

 

@Override
public AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManagerBean();
}
/**
 * DB 인가 처리
 * @return
 * @throws Exception
 */
@Bean
public PermitAllFilter customFilterSecurityInterceptor() throws Exception {
    PermitAllFilter permitAllFilter = new PermitAllFilter(permitAllResources);
    permitAllFilter.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
    permitAllFilter.setAccessDecisionManager(affimativeBased()); // 접근 결정 관리자 affimativeBased : ROLE 하나만 만족해도 인가됨
    permitAllFilter.setAuthenticationManager(authenticationManagerBean()); // 인가 전 인증 매니저
    return permitAllFilter;
}

private AccessDecisionManager affimativeBased() {
    return new AffirmativeBased(getAccessDecistionVoters());
}

private List<AccessDecisionVoter<?>> getAccessDecistionVoters() {
    // role 상위 계층 구현
    List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
    accessDecisionVoters.add(roleVoter());
    return accessDecisionVoters;
}

@Bean
private AccessDecisionVoter<? extends Object> roleVoter() {
    RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
    return roleHierarchyVoter;
}

@Bean
public RoleHierarchyImpl roleHierarchy() {
    return new RoleHierarchyImpl();
}

@Bean
private FilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource() throws Exception {
    // db로 받은 Map 정보 전달
    return new UrlFilterInvocationSecurityMetadataSource(urlResourcesMapFactoryBean().getObject(), securityResourceService);
}

private UrlResourcesMapFactoryBean urlResourcesMapFactoryBean() {
    UrlResourcesMapFactoryBean urlResourcesMapFactoryBean = new UrlResourcesMapFactoryBean();
    urlResourcesMapFactoryBean.setSecurityResourceService(securityResourceService);
    return urlResourcesMapFactoryBean;
}

 

UrlFilterInvocationSecurityMetadataSource

애플리케이션 구동 시 각 메뉴 url 리스트와 권한 리스트를 가져온 후 url 이동이 생길 때마다 권한이 존재하는지 체크

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

/**
 * db 동적 인가 체크
 */
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> requestMap = new LinkedHashMap<>();
    private SecurityResourceService securityResourceService;
    public UrlFilterInvocationSecurityMetadataSource(LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourcesMap
                                                    ,SecurityResourceService securityResourceService) {
        this.requestMap = resourcesMap;
        this.securityResourceService = securityResourceService;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        // 요청 url 객체
        HttpServletRequest request = ((FilterInvocation) object).getRequest();

        // test data
        // requestMap.put(new AntPathRequestMatcher("/admin"), Arrays.asList(new SecurityConfig("ROLE_ADMIN"),new SecurityConfig("ROLE_MAGAGER")));

        if(requestMap != null){
            // ex) key : /admin , value : ROLE_ADMIN, ROLE_MAGAGER
            for(Map.Entry<RequestMatcher, List<ConfigAttribute>> entry : requestMap.entrySet()){
                RequestMatcher matcher = entry.getKey(); // /admin
                // db 정보와 사용자 요청 정보 정보 일치 여부
                if(matcher.matches(request)) // /admin == /admin
                    return entry.getValue(); // Arrays.asList(new SecurityConfig("ROLE_ADMIN"),new SecurityConfig("ROLE_MAGAGER")
            }
        }
        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 자원 실시간 반영 (추후 권한 변경 화면 만들어지면 호출)
     */
    public void reload(){
        LinkedHashMap<RequestMatcher, List<ConfigAttribute>> reloadedMap = securityResourceService.getResourceList();
        Iterator<Map.Entry<RequestMatcher, List<ConfigAttribute>>> iterator = reloadedMap.entrySet().iterator();

        requestMap.clear();

        while (iterator.hasNext()){
            Map.Entry<RequestMatcher, List<ConfigAttribute>> entry = iterator.next();
            requestMap.put(entry.getKey(), entry.getValue());
        }
    }

}

 

애플리케이션이 구동되면 DB에서 권한 정보를 가져온 후 빈으로 등록함

import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.util.LinkedHashMap;
import java.util.List;

/**
 * db애서 가져온 권한/자원 정보를 빈으로 생성
 */
public class UrlResourcesMapFactoryBean implements FactoryBean<LinkedHashMap<RequestMatcher, List<ConfigAttribute>>> {

    private SecurityResourceService securityResourceService;
    private LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourceMap;

    public void setSecurityResourceService(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;
    }
}

 

각 메뉴에 대한 권한이 존재하는지 체크

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;


public class SecurityResourceService {

    private ResourceRepository resourceRepository;

    public SecurityResourceService(ResourceRepository resourceRepository) {
        this.resourceRepository = resourceRepository;
    }

    // db 자원 파싱
    public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getResourceList(){

        LinkedHashMap<RequestMatcher, List<ConfigAttribute>> result = new LinkedHashMap<>();
        List<Resocuces> resourceList = resourceRepository.findAllResources();

        // ex) /member , ROLE_ADMIN, ROME_USER
        //     /main , ROLE_MEMBER
        resourceList.forEach(r -> {
            List<ConfigAttribute> configAttributeList = new ArrayList<>();
            r.getRoleSet().forEach(role ->{
                configAttributeList.add(new SecurityConfig(role.getRoleName()));
                result.put(new AntPathRequestMatcher(re.getResourceName()), configAttributeList);
            });

        });

        return result;
    }
}

만약 url에 해당되는 권한이 없다면 전부 접근 가능

url에 해당되는 권한이 없을 때 접근 불가하도록 하려면 임의로 권한 부여 하면 됨!("ROLE_FALSE")

public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> getResourceList(){
    LinkedHashMap<RequestMatcher, List<ConfigAttribute>> result = new LinkedHashMap<>();
    List<Resources> resourcesList = adminMemberMapper.selectMenu();

    resourcesList.forEach(re -> {

      List<ConfigAttribute> configAttributeList = new ArrayList<>();
      List<String> authorityCode = adminMemberMapper.selectAuthorityCode(re.getMenuUrl());

      if(CollectionUtils.isEmpty(authorityCode)){
        configAttributeList.add(new SecurityConfig("ROLE_FALSE"));
        result.put(new AntPathRequestMatcher(re.getMenuUrl()), configAttributeList);
      }else{
        authorityCode.forEach(code -> {
          configAttributeList.add(new SecurityConfig(code));
          result.put(new AntPathRequestMatcher(re.getMenuUrl()), configAttributeList);
        });
      }

    });
    return result;
  }

빈으로 등록

@Configuration
public class AppConfig {

    @Bean
    public SecurityResourceService securityResourceService(ResourcesRepository resourcesRepository){
        return new SecurityResourceService(resourcesRepository);
    }

}

 

 

권한 체크 패스 시킬 경로 지정

private String[] permitAllResources = {"/api/public/**","/sample","/aws/**","/sample/**", "/login/**", "/outer/**", "/fonts/**", "/landing/**", "/error/**", "/aws/health/check"};
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * db 인가 처리 제외
 */
public class PermitAllFilter extends FilterSecurityInterceptor {
    private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
    private FilterInvocationSecurityMetadataSource securityMetadataSource;
    private boolean observeOncePerRequest = true;
    private List<RequestMatcher> permitAllRequestMatchers = new ArrayList<>();

    // 인가 처리가 필요 없는 url 매핑
    public PermitAllFilter(String...permitAllResources){
        for(String resource : permitAllResources){
            permitAllRequestMatchers.add(new AntPathRequestMatcher(resource));
        }
    }

    @Override
    protected InterceptorStatusToken beforeInvocation(Object object){
        boolean permitAll = false;
        HttpServletRequest request = ((FilterInvocation) object).getRequest();

        // 인가 처리 필요없는 url 과 사용자 요청 url 비교
        for(RequestMatcher requestMatcher : permitAllRequestMatchers){
            if(requestMatcher.matches(request)){
                permitAll = true;
                break;
            }
        }

        if(permitAll) return null;
        return super.beforeInvocation(object);
    }


    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }

    public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
        this.securityMetadataSource = newSource;
    }

    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if ((fi.getRequest() != null)
                && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                && observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        else {
            if (fi.getRequest() != null && observeOncePerRequest) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }

            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, null);
        }
    }

    public boolean isObserveOncePerRequest() {
        return observeOncePerRequest;
    }

    public void setObserveOncePerRequest(boolean observeOncePerRequest) {
        this.observeOncePerRequest = observeOncePerRequest;
    }
}
728x90
반응형