package com.its.op.security; import com.its.op.security.interceptor.UserLogoutHandler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.session.HttpSessionEventPublisher; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j @Configuration @EnableWebSecurity @RequiredArgsConstructor //@EnableGlobalMethodSecurity(securedEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final WebLoginService loginService; private final WebLoginSuccessHandler webLoginSuccessHandler; private final WebLoginFailureHandler webLoginFailureHandler; @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/favicon.ico"); // static 디렉터리의 하위 파일 목록은 인증 무시 ( = 항상통과 ) web.ignoring().antMatchers("/js/**", "/images/**", "/libs/**", "/css/**", "/application/fonts/**"); //web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations()); // 정적 리소스 접근 가능하게 web.ignoring().antMatchers(HttpMethod.GET, "/api/**"); // GET Method 는 모두 통과 // cs-api web.ignoring().antMatchers(HttpMethod.GET, "/cs-api/**"); // GET Method 는 모두 통과 web.ignoring().antMatchers(HttpMethod.POST, "/cs-api/**"); // GET Method 는 모두 통과 web.ignoring().antMatchers(HttpMethod.PUT, "/cs-api/**"); // GET Method 는 모두 통과 web.ignoring().antMatchers(HttpMethod.DELETE, "/cs-api/**"); // GET Method 는 모두 통과 } @Override protected void configure(HttpSecurity http) { // URL 권한 설정 //setAntMatchers(http, "ROLE_"); try { http.csrf() .disable() ; // REST API 호출 유효하게(POST...) http .authorizeRequests() // SWAGGER 권한 설정 .antMatchers("/swagger-ui.html", "/swagger/**", "/swagger-resources/**", "/webjars/**", "/v2/api-docs").permitAll() // 웹소켓 권한 설정하지 .antMatchers("/ws/**").permitAll() .antMatchers("/api/**").permitAll() // API 권한 설정하지 //.antMatchers("/api/**").permitAll() // 지도 URI 권한 설정하지 .antMatchers("/MAPDATA/**").permitAll() .antMatchers("/download/**").permitAll() // 페이지 권한 설정 // .antMatchers("/application/facility/**", "/facility/**").permitAll() .antMatchers("/application/**", "/facility/**").permitAll() .antMatchers("/application/wall/**", "/wall/**").permitAll() .antMatchers("/application/login/**").permitAll() .antMatchers("/api/auth/**").permitAll() // .antMatchers("/api/**").permitAll() .anyRequest().authenticated() .and() .formLogin() // .loginPage("/application/login/login.html") .loginPage("/application/op/00.main/main.html") .loginProcessingUrl("/api/auth/login.do") .defaultSuccessUrl("/application/op/00.main/main.html", true) .usernameParameter("username") .passwordParameter("password") .successHandler(this.webLoginSuccessHandler) .failureHandler(this.webLoginFailureHandler) .permitAll() .and() .logout() //.logoutUrl("/api/auth/logout.do") //.logoutRequestMatcher(new AntPathRequestMatcher("/api/auth/logout.do")) .addLogoutHandler(new UserLogoutHandler()).permitAll() .logoutSuccessUrl("/application/login/login.html").permitAll() //.logoutSuccessUrl("/api/auth/login.do").permitAll() .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .deleteCookies(WebMvcConfig.USER_UUID) .deleteCookies(WebMvcConfig.USER_TIME) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 스프링 시큐리티가 필요 시 생성 (default) // 인증에 성공할 때 마다 세션 ID나 세션을 변경해서 발급해줌으로써 // 세션을 중간에서 가로채더라도 해당 세션이 유효하지 않게 하는 기능 .invalidSessionUrl("/application/login/login.html") // 세션이 유효하지 않을 경우 이동 할 페이지 //.invalidSessionUrl("/api/auth/login.do") // 세션이 유효하지 않을 경우 이동 할 페이지 .sessionFixation().changeSessionId() // changeSessionId : 새로운 세션 ID를 발급해서 전달(default) // none : 아무 동작 안함 // migrateSession : 새로운 세션을 생성해서 전달 (속성값 유지) // newSession : 새로운 세션 전달 (속성값 유지 안됨) .maximumSessions(20) // 최대 허용 가능 세션 수, -1인 경우 무제한 세션 허용 .maxSessionsPreventsLogin(true) // 동시 로그인 차단, false 인 경우 기존 세션 만료(default) .expiredUrl("/application/login/login.html") // 세션이 만료된 경우 이동 할 페이지 //.expiredUrl("/api/auth/login.do") // 세션이 만료된 경우 이동 할 페이지 .sessionRegistry(sessionRegistry()) // .and() // .exceptionHandling() // .accessDeniedPage("/login.do") // .and() // .headers() // .defaultsDisabled() // .frameOptions() // .sameOrigin() // .cacheControl(); // .and() // 로그아웃 설정 // .logout() // .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // .logoutSuccessUrl("/login") // .invalidateHttpSession(true) // .deleteCookies("JSESSIONID") // .and() // // 403 예외처리 핸들링 // .exceptionHandling().accessDeniedPage("/login"); ; } catch (IOException e) { // FOR KISA Secure Coding pass log.error("{configure: IOException}"); } catch (Exception e) { log.error("{configure: Exception}"); } } public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(this.loginService); authenticationProvider.setPasswordEncoder(passwordEncoder()); // loadUserByUsername 의 UsernameNotFoundException 이 BadCredentialsException 로 발생함. // Exception 을 catch 하기 위해서는 아래를 false 로 설정하면 됨. authenticationProvider.setHideUserNotFoundExceptions(true); return authenticationProvider; } @Override public void configure(AuthenticationManagerBuilder auth) { // loadUserByUsername 의 UsernameNotFoundException 를 처리하기 위해 // AuthenticationProvider 를 빈으로 등록해서 사용자 로그인 처리를 수행한다. //auth.userDetailsService(this.loginService).passwordEncoder(passwordEncoder()); auth.authenticationProvider(daoAuthenticationProvider()); } @Bean public PasswordEncoder passwordEncoder() { return new WebPasswordEncoder(); } @Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } /** * 로그아웃을 했기 때문에 세션의 개수가 0이라 생각했는데 로그인이 안되었다. * 이에 대한 해결책으로 SessionRegistry 빈을 생성 후 sessionManagement 에 DI 시킨다. * @return */ @Bean public static ServletListenerRegistrationBean httpSessionEventPublisher() { return new ServletListenerRegistrationBean(new HttpSessionEventPublisher()); } protected List> getAuthReq() { Map roll = new HashMap<>(); // roll.put("id", "id"); // roll.put("url", "url"); // roll.put("hasAuthority", "auth"); // roll.put("date", "date"); return (List>) roll; } protected void setAntMatchers(HttpSecurity http, String rolePrefix) { List> list = getAuthReq(); for(Map m : list) { // 쉼표(,)로 구분된 권한 정보를 분리 후 배열로 저장 String[] roles = m.get("hasAuthority").toString().split(","); // 권한 앞에 접두사(rolePrefix) 붙임 for(int ii = 0; ii < roles.length; ii++) { roles[ii] = rolePrefix + roles[ii].toUpperCase(); } String url = m.get("url").toString(); if(url.charAt(0) != '/') { url = "/" + url; } // url, 권한 정보를 넣는다. try { http.authorizeRequests() .antMatchers(url) .hasAnyAuthority(roles); } catch (IOException ie) { // FOR KISA Secure Coding pass log.error("setAntMatchers: IOException"); } catch (Exception e) { log.error("setAntMatchers: Exception"); } } try { http.authorizeRequests() .antMatchers("/**").permitAll() .anyRequest().authenticated(); } catch (IOException ie) { // FOR KISA Secure Coding pass log.error("setAntMatchers: IOException, permitAll"); } catch (Exception e) { log.error("setAntMatchers: Exception, permitAll"); } } }