본문 바로가기
spring/spring security

[Spring Security] Multiple HttpSecurity, Multiple Login, accessDenied 설정

by moonsiri 2020. 10. 31.
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]

https://cnpnote.tistory.com/entry/SPRING-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-accessDeniedHandler%EA%B0%80-%EC%9E%91%EB%8F%99%ED%95%98%EC%A7%80-%EC%95%8A%EC%8A%B5%EB%8B%88%EB%8B%A4

sup2is.github.io/2020/03/10/spring-security-exception-translation-filter-and-authentication-entry-point.html

728x90
반응형

댓글