@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final int TOKEN_VALIDITY_TIME = 604800;
@NonNull
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@NonNull
private final UserService userService;
@Bean
public FilterRegistrationBean<Filter> getSpringSecurityFilterChainBindedToError(
@Qualifier("springSecurityFilterChain") Filter springSecurityFilterChain) {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
registration.setFilter(springSecurityFilterChain);
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
return registration;
}
@Bean
public AuthenticationFailureHandler customAuthenticationFailureHandler() {
return new CustomAuthenticationHandler();
}
@Bean
public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
return new CustomAuthenticationHandler();
}
@Bean
public LoginUrlAuthenticationEntryPoint ajaxAwareAuthenticationEntryPoint() {
return new AjaxAwareAuthenticationEntryPoint(SpringSecurity.LOGIN_URL);
}
@Bean
public AccessDeniedHandler customAccessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
/**
* 유저 DB의 DataSource와 Query 및 Password Encoder 설정.
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
}
/**
* Spring Security에서 인증받지 않아도 되는 리소스 URL 패턴을 지정해 줍니다.
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/static/**");
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
// setAllowCredentials(true) is important, otherwise:
// The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
configuration.setAllowCredentials(true);
// setAllowedHeaders is important! Without it, OPTIONS preflight request
// will fail with 403 Invalid CORS request
configuration.setAllowedHeaders(Collections.singletonList("Authorization"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
/**
* Spring Security에 의해 인증받아야 할 URL 또는 패턴을 지정해 줍니다.
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().sameOrigin()
.and().csrf().disable().authorizeRequests()
.antMatchers("/admin/**","/mypage/**","/comment/**").authenticated()
.anyRequest().permitAll()
.and().formLogin().loginPage(SpringSecurity.LOGIN_URL)
.loginProcessingUrl(SpringSecurity.LOGIN_PROCESS_URL)
.successHandler(customAuthenticationSuccessHandler())
.failureHandler(customAuthenticationFailureHandler())
.usernameParameter(SpringSecurity.PARAM_USERNAME)
.passwordParameter(SpringSecurity.PARAM_PASSWORD)
.and().logout()
.logoutRequestMatcher(new AntPathRequestMatcher(SpringSecurity.LOGOUT_URL))
.logoutSuccessUrl(SpringSecurity.LOGIN_URL)
.and().exceptionHandling().authenticationEntryPoint(ajaxAwareAuthenticationEntryPoint())
.accessDeniedHandler(customAccessDeniedHandler())
.and().rememberMe().disable();
}
}
config-MvcConfig
@Bean(name = "bCryptPasswordEncoder")
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
❗ MvcConfig에 @Bean으로 추가를 해야 사용이 가능하다!! ❗
config-PasswordConfig
@Getter
@Setter
@Configuration
@ConfigurationProperties("password")
@PropertySources({@PropertySource(value = "classpath:/config/password.properties")})
public class PasswordConfig {
private Validation validation;
@Getter
@Setter
@Configuration
@ConfigurationProperties("herbnet.password-validation")
public static class Validation {
private int length;
private boolean specialCharacters;
private boolean upperCases;
private boolean numbers;
private int maxAttemptsCount;
private int changeCycle;
}
}
❗ password.properties는 취향 것.. ❗
config-기타
//AjaxAwareAuthenticationEntryPoint
public class AjaxAwareAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public AjaxAwareAuthenticationEntryPoint(String loginUrl) {
super(loginUrl);
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String ajaxHeader = request.getHeader("X-Requested-With");
boolean isAjax = "XMLHttpRequest".equals(ajaxHeader);
if (isAjax) {
response
.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Ajax Request Denied (Session Expired)");
} else {
super.commence(request, response, authException);
}
}
}
//CustomAuthenticationHandler
public class CustomAuthenticationHandler
implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication auth) throws IOException {
response.sendRedirect(request.getContextPath() + SpringSecurity.LOGIN_SUCCESS_URL);
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String username = request.getParameter("username");
String dispatcherURL = SpringSecurity.LOGIN_URL;
request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
request.setAttribute("SPRING_SECURITY_LAST_USERNAME", username);
request.setAttribute("username", username);
request.setAttribute("error", getErrorMessage(exception));
request.getRequestDispatcher(dispatcherURL).forward(request, response);
}
/**
* Spring Security 가 반환하는 에러 메시지 얻어옴
*
* @return 에러메시지
*/
private String getErrorMessage(AuthenticationException exception) {
String error;
if (exception instanceof AccountExpiredException) {
error = "사용 기간이 만료된 계정입니다.";
} else if (exception instanceof BadCredentialsException) {
error = "아이디 또는 비밀번호를 확인해주시기 바랍니다.";
} else if (exception instanceof LockedException) {
error = "로그인 반복 실패 또는 관리자에 의해 잠겨있는 계정입니다.\n관리자에게 문의하세요.";
} else if (exception instanceof InternalAuthenticationServiceException) {
error = "존재하지 않는 계정입니다.";
} else if (exception instanceof DisabledException) {
error = "승인되지 않은 계정입니다. 관리자의 승인을 기다려주세요.";
} else if (exception instanceof CredentialsExpiredException) {
error = "만료된 비밀번호 입니다.";
} else {
error = exception.getLocalizedMessage();
}
return error;
}
}
//CustomAccessDeniedHandler
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof AnonymousAuthenticationToken) {
response.sendRedirect(request.getContextPath() + SpringSecurity.LOGIN_URL);
}
}
}
util-ApplicationContextProvider
public class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public static <T> T getEnvironmentProperty(String key, Class<T> targetClass, T defaultValue) {
if (key == null || targetClass == null) {
throw new NullPointerException();
}
T value = null;
if (applicationContext != null) {
value = applicationContext.getEnvironment().getProperty(key, targetClass, defaultValue);
}
return value;
}
public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
ApplicationContextProvider.applicationContext = applicationContext;
}
}
util-LoginAuth
public class LoginAuth {
/* 계정 정보 */
public static final String PASSWORD_SPECIAL_REGEXP = "(?=.*[!@#$%^&*()\\-_=+\\\\\\|\\[\\]{};:\\'\",.<>\\/?])";
public static final String PASSWORD_UPPER_REGEXP = "(?=.*[A-Z])";
public static final String PASSWORD_NUMBER_REGEXP = "(?=.*\\d)";
}
public class SpringSecurity {
public static final String LOGIN_URL = "/auth/login";
public static final String LOGIN_PROCESS_URL = "/auth/login/process";
public static final String LOGIN_SUCCESS_URL = "/";
public static final String LOGOUT_URL = "/auth/logout";
public static final String PARAM_USERNAME = "username";
public static final String PARAM_PASSWORD = "password";
public static final String PARAM_REMEMBER = "remember-me";
public static final String KEY_REMEMBER = "알아서";
}
exception-UserPasswordValidationException
@SuppressWarnings("serial")
public class UserPasswordValidationException extends Exception {
@Override
public String getMessage() {
return "비밀번호 규칙이 일치하지 않습니다";
}
}
exception-DataNotFoundException
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class DataNotFoundException extends HttpClientErrorException {
public DataNotFoundException() {
super(HttpStatus.NOT_FOUND, "데이터를 찾을 수 없습니다.");
}
public DataNotFoundException(String message) {
super(HttpStatus.NOT_FOUND, message);
}
}