728x90
반응형
개요
스프링 부트 3.0부터 스프링 시큐리티 6 버전이 적용되었습니다. 삭제되거나 deprecated된 코드가 많아서 마이그레이션 시 주의할 부분에 대해 알려드리겠습니다.
기존 버전 : 5.3.3.RELEASE
최신 버전 : 6.2.4
요약
- 기존 WebSecurityConfigurationAdapter를 상속받아 세팅하던 방식은 삭제되었고 SecurityFilterChain bean을 스프링 컨테이너에 등록해줘야함.
- authorizeRequests() → authorizeHttpRequests()로 변경
- antMatchers() → requestMatchers()로 변경
- 로그인 페이지 리다이렉트 반복 접근 이슈 발생
- .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() 추가
- https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html#match-by-dispatcher-type
- @EnableRedisHttpSession → @EnableRedisIndexedHttpSession로 변경
- spring.redis 에서 spring.data.redis로 properties 변경
- https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html
- FilterSecurityInterceptor → AuthorizationFilter로 변경
상세
AS-IS)
@Configuration
@RequiredArgsConstructor
public class UserSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final UserLoginSuccessHandler loginSuccessHandler;
private final UserLoginFailureHandler loginFailureHandler;
private final RoleHierarchyService roleHierarchyService; //권한 처리
@PostConstruct
public void init() {
// spring hierarchy 관련 셋팅
roleHierarchyService.resetRoleHierarchyData();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(SECURITY_EXCLUDE_PATTERN_ARR);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/user/**")
.addFilterAfter(filterSecurityInterceptor(), FilterSecurityInterceptor.class) // https://blog.naver.com/myh814/221934064615
.authorizeRequests().anyRequest().authenticated() // 사용자 인증이 된 요청에 대해서만 요청 허용
.and()
.csrf().disable().anonymous() // CSRF OFF
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/user/main")
.loginProcessingUrl("/user/login/loginUser")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.usernameParameter("empId")
.passwordParameter("passWd")
.and()
.logout()
.logoutUrl("/user/logout")
.logoutSuccessUrl("/user/login")
.and()
.exceptionHandling()
.accessDeniedPage("/error/notAuth")
;
http
.headers()
.frameOptions()
.sameOrigin()
.httpStrictTransportSecurity()
.includeSubDomains(true)
.maxAgeInSeconds(3600)
;
http
.sessionManagement()
.sessionFixation()
.changeSessionId()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredSessionStrategy(sessionInformationExpiredStrategy)
.sessionRegistry(springSessionBackedSessionRegistry)
;
}
/**
* 접근 권한 검사
* - https://docs.spring.io/spring-security/site/docs/4.1.5.RELEASE/reference/html/core-web-filters.html#filter-security-interceptor
* - https://blog.naver.com/myh814/221934064615
*
* @return
* @throws Exception
*/
@Bean
public FilterSecurityInterceptor filterSecurityInterceptor() throws Exception {
FilterSecurityInterceptor interceptor = new FilterSecurityInterceptor();
interceptor.setAccessDecisionManager(accessDecisionManager()); // AccessDecisionManager에 권한검사 위임
interceptor.setSecurityMetadataSource(filterInvocationSecurityMetadataSource());
return interceptor;
}
/**
* 사용자가 수동으로 권한 갱신하기 위해 securedObjectService 처리
*
* @return
*/
@Bean
public ReloadableFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource() {
List<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : SECURITY_EXCLUDE_PATTERN_ARR) {
matchers.add(new AntPathRequestMatcher(pattern));
}
return new ReloadableFilterInvocationSecurityMetadataSource(matchers);
}
}
더보기
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import java.util.ArrayList;
import java.util.List;
@Configuration
@RequiredArgsConstructor
public class RoleHierarchyConfiguration {
/**
* role hierarchy singleton
*
* @return
*/
@Bean
public RoleHierarchy roleHierarchy() {
return new RoleHierarchyImpl();
}
/**
* 권한 설정용
* - spring security 권한 필터 개념 내용 참고
* - https://docs.spring.io/spring-security/site/docs/3.0.x/reference/technical-overview.html#tech-intro-access-control
* - https://docs.spring.io/spring-security/site/docs/4.1.5.RELEASE/reference/html/ns-config.html#ns-access-manager
*
* @return
*/
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<AccessDecisionVoter<?>>();
RoleVoter roleVoter = new RoleVoter();
roleVoter.setRolePrefix("");
decisionVoters.add(roleVoter);
AffirmativeBased affirm = new AffirmativeBased(decisionVoters);
affirm.setAllowIfAllAbstainDecisions(false);
return affirm;
}
}
public class ReloadableFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private RedisRequestMapService redisRequestMapService;
private final List<RequestMatcher> matchers;
/**
* 생성자 내 requestMap 초기화
*
* @param matchers
*/
public ReloadableFilterInvocationSecurityMetadataSource(List<RequestMatcher> matchers) {
this.matchers = matchers;
}
/**
* break문 처리하여 매칭되는 권한이 있으면 바로 리턴
* 즉, DB에서 권한을 얻어올 때 순서가 의미가 있다
*
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
HttpServletRequest request = ((FilterInvocation)object).getRequest();
Collection<ConfigAttribute> result = null;
// 아래 설정에 대한 참고문서 https://github.com/spring-projects/spring-session/issues/244
// set excluding uri
for (RequestMatcher matcher : matchers) {
if (matcher.matches(request)) {
request.setAttribute("org.springframework.session.web.http.SessionRepositoryFilter.FILTERED", Boolean.TRUE);
return result;
}
}
Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = redisRequestMapService.getRequestMap();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
if (entry.getKey().matches(request)) {
result = entry.getValue();
break;
}
}
return result;
}
/**
* @return
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet<>();
Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = redisRequestMapService.getRequestMap();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
allAttributes.addAll(entry.getValue());
}
return allAttributes;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
TO-BE)
import com.biz.config.security.custom.AccessDecisionManagerAuthorizationManagerAdapter;
import com.biz.config.security.handler.LoginFailureHandler;
import com.biz.config.security.handler.LoginSuccessHandler;
import jakarta.servlet.DispatcherType;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@RequiredArgsConstructor
public class UserSecurityConfiguration {
private final UserLoginSuccessHandler loginSuccessHandler;
private final UserLoginFailureHandler loginFailureHandler;
private final AccessDecisionManagerAuthorizationManagerAdapter authorizationManager;
private final RoleHierarchyService roleHierarchyService; //권한 처리
@PostConstruct
public void init() {
// spring hierarchy 관련 셋팅
roleHierarchyService.resetRoleHierarchyData();
}
@Bean
public WebSecurityCustomizer configure() {
return web -> web.ignoring().requestMatchers(SECURITY_EXCLUDE_PATTERN_ARR);
}
@Bean
public SecurityFilterChain userFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/user/**") // */
.authorizeHttpRequests(authorize -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.anyRequest().access(authorizationManager)
// .anyRequest().authenticated()
)
.csrf(AbstractHttpConfigurer::disable) // CSRF OFF
.formLogin(formLogin -> formLogin
.loginPage("/user/login").permitAll()
.defaultSuccessUrl("/user/main")
.loginProcessingUrl("/user/login/loginUser")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.usernameParameter("empId")
.passwordParameter("passWd")
)
.logout(logout -> logout
.logoutUrl("/user/logout")
.logoutSuccessUrl("/user/login")
)
.exceptionHandling(e -> e.accessDeniedPage("/error/notAuth"))
;
http.headers(header -> header
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.includeSubDomains(true)
.maxAgeInSeconds(3600)
)
);
http.sessionManagement(session -> session
.sessionFixation()
.changeSessionId()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredSessionStrategy(sessionInformationExpiredStrategy)
.sessionRegistry(springSessionBackedSessionRegistry)
);
return http.build();
}
}
더보기
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
@Configuration
public class RoleHierarchyConfiguration{
/**
* role hierarchy singleton
*
* @return
*/
@Bean
public RoleHierarchy roleHierarchy(){
return new RoleHierarchyImpl();
}
}
import com.biz.config.security.service.RedisRequestMapService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Map;
import java.util.function.Supplier;
/**
* 사용자 권한 - url 인가
*
* https://docs.spring.io/spring-security/reference/servlet/authorization/architecture.html#authz-voter-adaptation
*/
@Component
@RequiredArgsConstructor
public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager<RequestAuthorizationContext> {
public static final String FILTER_SECURITY_INTERCEPTOR_APPLIED = "__spring_security_authorizationManager_filterApplied"; // SecurityContextHolderFilter.class.getName() + ".APPLIED";
private final RedisRequestMapService redisRequestMapService;
private final RoleHierarchy roleHierarchy;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
try {
HttpServletRequest request = object.getRequest();
if (request == null) {
throw new AccessDeniedException("The request is null.");
}
if (request.getAttribute(FILTER_SECURITY_INTERCEPTOR_APPLIED) != null) {
return new AuthorizationDecision(true);
}
Collection<ConfigAttribute> attributes = this.getAttributes(request);
if (attributes == null || attributes.isEmpty()) {
throw new AccessDeniedException("ConfigAttributes is blank.");
}
Authentication auth = authentication.get();
if (auth == null || auth instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("No authentication.");
}
Collection<? extends GrantedAuthority> authorities = this.getAuthorities(auth);
for (ConfigAttribute attribute : attributes) {
if (attribute.getAttribute() == null) {
continue;
}
// Attempt to find a matching granted authority.
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
request.setAttribute(FILTER_SECURITY_INTERCEPTOR_APPLIED, Boolean.TRUE);
return new AuthorizationDecision(true);
}
}
}
throw new AccessDeniedException("No matching granted authority.");
} catch (AccessDeniedException ex) {
return new AuthorizationDecision(false);
}
}
/**
* 이전에 FilterInvocationSecurityMetadataSource 로 처리했던 로직
*
* @param request
* @return
* @throws IllegalArgumentException
*/
private Collection<ConfigAttribute> getAttributes(HttpServletRequest request) throws IllegalArgumentException {
Collection<ConfigAttribute> result = null;
Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = redisRequestMapService.getRequestMap();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
if (entry.getKey().matches(request)) {
result = entry.getValue();
break;
}
}
return result;
}
private Collection<? extends GrantedAuthority> getAuthorities(Authentication authentication) {
return roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
}
}
@Service
@Slf4j
public class RoleHierarchyServiceImpl implements RoleHierarchyService {
@Resource
private AuthorityHierarchyMappingRepositoryCustom authorityHierarchyMappingRepositoryCustom;
@Resource
private RoleHierarchy roleHierarchy;
/**
* 권한 계층 구조를 만듬
*
*/
@Override
public void resetRoleHierarchyData() {
List<AuthorityHierarchyMappingDTO.GetRes> dbInfo = authorityHierarchyMappingRepositoryCustom.findAll(); // 전체 데이터 조회
try{
((RoleHierarchyImpl)roleHierarchy).setHierarchy(this.makeRoleHierarchyStringRepresentation(dbInfo));
} catch (CycleInRoleHierarchyException e){
log.error("##### 권한 계층 구조에 Cycle이 존재함. 관리자 확인 반드시 필요!!! ##### | error msg : {}", e.getMessage());
throw e;
}
}
/**
* 권한 계층 구조를 만듬(overloading)
*
*/
@Override
public void resetRoleHierarchyData(List<AuthorityHierarchyMappingDTO.GetRes> dbInfo){
try{
((RoleHierarchyImpl)roleHierarchy).setHierarchy(this.makeRoleHierarchyStringRepresentation(dbInfo));
} catch (CycleInRoleHierarchyException e){
log.error("##### 권한 계층 구조에 Cycle이 존재함. 관리자 확인 반드시 필요!!! ##### | error msg : {}", e.getMessage());
throw e;
}
}
/**
* cycle hierarchy인지를 검사함.
* - cycle이 존재하면 true / 존재하지 않으면 false
*
* @param parentAuthorityCd
* @param childAuthorityCds
* @return
*/
@Override
public boolean existCycleHierarchy(String parentAuthorityCd, List<String> childAuthorityCds) {
Collection<GrantedAuthority> searchParamList = new HashSet<>();
childAuthorityCds.forEach(childAuthorityCd -> searchParamList.add(new SimpleGrantedAuthority(childAuthorityCd)));
Collection<? extends GrantedAuthority> childAuthorityCdsHasRoleInfoList = roleHierarchy.getReachableGrantedAuthorities(searchParamList); // 추가 하려는 자식권한이 가지는 모든 권한 리스트
Optional<? extends GrantedAuthority> filteredRoleInfo = childAuthorityCdsHasRoleInfoList.stream().filter(info -> StringUtils.equals(parentAuthorityCd, info.getAuthority())).findFirst();
return filteredRoleInfo.isPresent();
}
/**
* spring hierarchy에서 읽을 수 있는 1차원 계층 형태 스트링 데이터를 만듬.
*
* @param hierarchyList
* @return
*/
private String makeRoleHierarchyStringRepresentation(List<AuthorityHierarchyMappingDTO.GetRes> hierarchyList) {
if (hierarchyList.isEmpty()) {
return "";
}
List<String> hierarchyNotationStrList = new ArrayList<>();
hierarchyList.forEach(hierarchyVo -> hierarchyNotationStrList.add(hierarchyVo.getParentAuthorityCd() + " > " + hierarchyVo.getChildAuthorityCd()));
return String.join("\n", hierarchyNotationStrList);
}
}
ㅅ
[Reference]
https://docs.spring.io/spring-security/reference/6.0/migration/servlet/index.html
https://velog.io/@doublive/Spring-Security-6-버전-URL-요청-인가-흐름
728x90
반응형
'spring > spring security' 카테고리의 다른 글
[Spring Security] DaoAuthenticationProvider (0) | 2023.05.09 |
---|---|
[Spring Security] DelegatingPasswordEncoder와 BCryptPasswordEncoder strength에 따른 수행시간 (0) | 2022.03.20 |
[Spring Security5] 권한 계층구조(roleHierarchy) 설정 (0) | 2021.08.13 |
[Spring Security] redis 세션에서 SecurityContext 수정 (0) | 2021.07.12 |
[Spring] Chrome 양식 다시 제출 확인 ERR_CACHE_MISS 해결방법 (0) | 2020.10.31 |
댓글