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
반응형