spring/spring security
[Spring Security5] Multiple HttpSecurity, Multiple Login, accessDenied 설정
moonsiri
2020. 10. 31. 02:42
728x90
반응형
한 프로젝트에 여러 개 서비스가 있을 경우, 로그인 화면이 두 개 이상인 경우 다음과 같이 구성합니다.
MultipleSecurityConfiguration.java (Spring docs 기반으로 작성.)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Deprecated
public class MultipleSecurityConfiguration {
// spring security에서 허용할 web 리소스 path
public static final String[] SECURITY_EXCLUDE_PATTERN_ARR = {
"/"
// resource
, "/error/**"
, "/favicon.ico"
, "/resources/**"
// api
, "/api/**" // */
// User 관련
, "/user/login*"
// Admin 관련
, "/admin/login*"
};
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(customAuthenticationProvider); // 인증
}
@Order(1)
@Configuration
public static class UserSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserLoginSuccessHandler loginSuccessHandler;
@Autowired
private UserLoginFailureHandler loginFailureHandler;
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(SECURITY_EXCLUDE_PATTERN_ARR); // 제외 패턴
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/user/**") // */
.csrf().disable().anonymous() // CSRF OFF
.and()
.addFilterAfter(filterSecurityInterceptor, FilterSecurityInterceptor.class)
.authorizeRequests().anyRequest().authenticated() // 사용자 인증이 된 요청에 대해서만 요청 허용
.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")
// .logoutSuccessHandler(logoutSuccessHandler)
.logoutSuccessUrl("/user/login")
.and()
.exceptionHandling()
.accessDeniedPage("/error/notAuth");
}
}
@Order(2)
@Configuration
public static class AdminSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private AdminLoginSuccessHandler loginSuccessHandler;
@Autowired
private AdminLoginFailureHandler loginFailureHandler;
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(SECURITY_EXCLUDE_PATTERN_ARR);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/admin/**") // */
.csrf().disable().anonymous() // CSRF OFF
.and()
.addFilterAfter(filterSecurityInterceptor, FilterSecurityInterceptor.class)
.authorizeRequests().anyRequest().authenticated() // 사용자 인증이 된 요청에 대해서만 요청 허용
.and()
.formLogin()
.loginPage("/admin/login")
.defaultSuccessUrl("/admin/main")
.loginProcessingUrl("/admin/login/loginUser")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.usernameParameter("empId")
.passwordParameter("passWd")
.and()
.logout()
.logoutUrl("/admin/logout")
// .logoutSuccessHandler(logoutSuccessHandler)
.logoutSuccessUrl("/admin/login")
.and()
.exceptionHandling()
.accessDeniedPage("/error/notAuth");
}
}
// 각종 Bean 추가
}
위 구조를 분리해보겠습니다.
- CommonSecurityConfiguration.java
- UserSecurityConfiguration.java
- AdminSecurityConfiguration.java
@Configuration
@RequiredArgsConstructor // final이나 @notnull인 필드값만 파라미터로 받는 생성자 생성
public class CommonSecurityConfiguration {
// spring security에서 허용할 web 리소스 path
public static final String[] SECURITY_EXCLUDE_PATTERN_ARR = {
"/"
// resource
, "/error/**"
, "/favicon.ico"
, "/resources/**"
// api
, "/api/**" // */
// User 관련
, "/user/login*"
// Admin 관련
, "/admin/login*"
};
// 각종 Bean 추가
/**
* 접근 권한 검사
* - 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(name = "filterSecurityInterceptor")
public FilterSecurityInterceptor getFilterSecurityInterceptor() throws Exception {
FilterSecurityInterceptor interceptor = new FilterSecurityInterceptor();
interceptor.setAccessDecisionManager(getAffirmativeBased()); // AccessDecisionManager에 권한검사 위임
interceptor.setSecurityMetadataSource(getReloadableFilterInvocationSecurityMetadataSource());
return interceptor;
}
/**
* 권한 설정용
* - 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(name = "accessDecisionManager")
public AffirmativeBased getAffirmativeBased() {
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;
}
/**
* 사용자가 수동으로 권한 갱신하기 위해 securedObjectService 처리
*
* @return
*/
@Bean
public ReloadableFilterInvocationSecurityMetadataSource getReloadableFilterInvocationSecurityMetadataSource() {
List<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : SECURITY_EXCLUDE_PATTERN_ARR) {
matchers.add(new AntPathRequestMatcher(pattern));
}
return new ReloadableFilterInvocationSecurityMetadataSource(matchers);
}
/**
* (여러개의 슬래시(//)가 포함되어있는) 경로의 패턴 불일치 방지
* - spring security Request Matching 내용 참고
* - https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/reference/htmlsingle/#request-matching
* @return
*/
@Bean
public HttpFirewall defaultHttpFirewall() {
return new DefaultHttpFirewall();
}
/**
* 패스워드 암호화
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
@Order(1)
@Configuration
@RequiredArgsConstructor
public class UserSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final UserLoginSuccessHandler loginSuccessHandler;
private final UserLoginFailureHandler loginFailureHandler;
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(SECURITY_EXCLUDE_PATTERN_ARR);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/user/**") // */
// filterSecurityInterceptor 미적용 시, Attributes: [authenticated] 로 권한 상관없이 Authorization successful 한다.
// filterSecurityInterceptor 적용 시, Attributes: [ROLE_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")
// .logoutSuccessHandler(logoutSuccessHandler)
.logoutSuccessUrl("/user/login")
.and()
.exceptionHandling()
.accessDeniedPage("/error/notAuth");
}
}
@Order(2)
@Configuration
@RequiredArgsConstructor
public class AdminSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final AdminLoginSuccessHandler loginSuccessHandler;
private final AdminLoginFailureHandler loginFailureHandler;
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(SECURITY_EXCLUDE_PATTERN_ARR);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/admin/**") // */
.addFilterAfter(filterSecurityInterceptor, FilterSecurityInterceptor.class)
.authorizeRequests().anyRequest().authenticated() // 사용자 인증이 된 요청에 대해서만 요청 허용
.and()
.csrf().disable().anonymous() // CSRF OFF
.and()
.formLogin()
.loginPage("/admin/login")
.defaultSuccessUrl("/admin/main")
.loginProcessingUrl("/admin/login/loginUser")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.usernameParameter("empId")
.passwordParameter("passWd")
.and()
.logout()
.logoutUrl("/admin/logout")
// .logoutSuccessHandler(logoutSuccessHandler)
.logoutSuccessUrl("/admin/login")
.and()
.exceptionHandling()
.accessDeniedPage("/error/notAuth");
}
}
(+) 번외
accessDenidPage, accessDeniedHandler는 인증된 사용자가 접근할 수 없는 페이지에 접근했을 때 작동합니다.
.formLogin().loginPage(...) 설정 시 인증하지 않은 사용자는 기본 동작인 로그인 페이지로 이동하게됩니다. (LoginUrlAuthenticationEntryPoint)
만약 해당 부분을 사용하지 않고 변경하려면 인증하지 않은 사용자가 보호자원에 액세스 하려고 할 때 호출하는 AuthenticationEntryPoint(Http403ForbiddenEntryPoint)를 구성해야 합니다.
public class CustomHttp403ForbiddenEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
if (this.isAjaxReq(request)) {
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
} else {
response.sendError(...);
// response.sendRedirect(...);
}
}
/**
* Ajax요청인지 판단
*/
public static boolean isAjaxReq(HttpServletRequest servletRequest) {
String chkStr = servletRequest.getHeader("X-Requested-With");
if (StringUtils.isEmpty(chkStr) == false && StringUtils.equals("xmlhttprequest", StringUtils.lowerCase(chkStr))) {
return true;
} else {
return false;
}
}
}
@Configuration
@RequiredArgsConstructor
public class UserSecurityConfiguration extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http
.formLogin().disable()
.and()
.exceptionHandling()
.authenticationEntryPoint(new CustomHttp403ForbiddenEntryPoint())
;
}
// @Bean
// public AuthenticationEntryPoint authenticationEntryPoint() {
// return new CustomHttp403ForbiddenEntryPoint();
// }
}
[Reference]
728x90
반응형