Kpop Generation 프로젝트를 시작하면서 로그인 기능 구현을 위해 SpringSecurity 프레임워크를 사용하기로 결정했다.
AWS 상에 서버를 배포한다는 가정 하에 보안성 강화를 위해 스프링에서 제공하는 프레임워크를 사용하는 것이 좋다고 판단
간단한 토이 프로젝트로 향후 서버를 scale-out 하지는 않을 것으로 예상되므로 Session-Cookie 방식을 사용하는 것이 효과적이라고 판단
프론트엔드 개발자 없이 Thymeleaf를 사용하여 서버 사이드 렌더링 방식으로 프론트엔드단을 구현하므로, JWT 토큰 방식보다 Session-Cookie 방식이 구현하기 더 유리하고 편하다고 판단
새로운 프레임워크 학습
스프링 시큐리티와 관련된 전체 설정입니다.
<주요 특징>
1. 해당 서비스를 운용하는 데 적합한 방식으로 SpringSecurity가 제공하는 인터페이스, 클래스 약 13개를 재설정하여 빈으로 등록
2. Success, Failure Handler를 설정해 사용자가 손쉽게 웹페이지를 탐색할 수 있도록 유도
3. Naver 로그인과 같은 Oauth2 인증 기능 제공
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public AuthenticationProvider authenticationProvider() {
return new CustomAuthenticationProvider(userDetailsService(), passwordEncoder());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Bean
public AuthenticationDetailsSource formAuthenticationDetailSource() {
return new FormAuthenticationDetailSource();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new CustomAuthenticationSuccessHandler();
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
CustomAccessDeniedHandler customAccessDeniedHandler = new CustomAccessDeniedHandler();
customAccessDeniedHandler.setErrorPage("/denied");
return customAccessDeniedHandler;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
web
.ignoring()
.antMatchers("/resources/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.authorizeRequests()
.anyRequest().permitAll();
http
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/loginProc")
http
.addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class);
http
.exceptionHandling()
.authenticationEntryPoint(new Http403ForbiddenEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
http
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(customLogoutSuccessHandler());
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
http
.oauth2Login()
.userInfoEndpoint()
.userService(oauth2UserService())
;
}
@Bean
public CustomOauth2UserService oauth2UserService(){
return new CustomOauth2UserService();
}
@Bean
public LogoutSuccessHandler customLogoutSuccessHandler(){
return new CustomLogoutSuccessHandler();
}
@Bean
public FilterSecurityInterceptor customFilterSecurityInterceptor() throws Exception {
FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
filterSecurityInterceptor.setSecurityMetadataSource(urlFilterInvocationSecurityMetadatasource());
filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
filterSecurityInterceptor.setAuthenticationManager(authenticationManagerBean());
return filterSecurityInterceptor;
}
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public FilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadatasource() throws Exception {
return new UrlFilterInvocationSecurityMetadatasource(urlResourcesMapFactoryBean().getObject(), securityResourceService());
}
@Bean
public SecurityResourceService securityResourceService(){
SecurityResourceService securityResourceService = new SecurityResourceService();
return securityResourceService;
}
private UrlResourcesMapFactoryBean urlResourcesMapFactoryBean(){
UrlResourcesMapFactoryBean urlResourcesMapFactoryBean = new UrlResourcesMapFactoryBean(securityResourceService());
return urlResourcesMapFactoryBean;
}
private AccessDecisionManager affirmativeBased() {
return new AffirmativeBased(getAccessDecisionVoters());
}
private List<AccessDecisionVoter<?>> getAccessDecisionVoters() {
return Arrays.asList(new RoleVoter());
}
}
PasswordEncoder
DB에 비밀번호를 저장 시 암호화한 저장한다
또한 PasswordEncoder를 통해서 비밀번호 일치 여부를 판단할 수 있다
CustomUserDetailsService
UserDetailsService를 상속받아 구현한 클래스로, 로그인 인증 시도 시 DB에 접근해 해당하는 username을 가진 member를 조회해온다
CustomAuthenticationProvider
AuthenticationProvider를 상속받아 구현한 클래스로, 실제 인증 과정을 담당한다.
UserDetailsService가 username을 통해 조회해 온 회원 정보(member)의 password를 검증한다.
인증 성공 시 AuthenticationToken을 만들며, 해당 토큰에는 member 객체와 member 객체의 role이 담겨 있다
FormAuthenticationDetailSource
AuthenticationDetailsSource를 상속받아 구현한 클래스로, 인증 과정에서 HttpServletRequest에서 필요한 정보를 추출하여 인증 과정에서 사용하는 등 인증에 사용되는 세부 정보 등을 사용할 수 있게 도와준다
CustomAuthenticationSuccessHandler
SimpleUrlAuthenticationSuccessHandler를 상속받아 구현한 클래스로, 인증에 성공했을 때 사용되는 handler이다.
CustomAuthenticationFailureHandler
SimpleUrlAuthenticationFailureHandler를 상속받아 구현한 클래스로, 인증에 실패했을 때 사용되는 handler이다
CustomAccessDeniedHandler
AccessDeniedHandler를 상속받아 구현한 클래스로, 권한 평가(인가 과정)에 실패했을 때 사용되는 handler이다.
최종적인 필터인 FilerSecurityInterceptor가 최종적으로 사용자의 권한을 평가할 때, 해당 리소스에 접근할 권한이 없을 때 사용된다.
UrlFilterInvocationSecurityMetadatasource
FilterInvocationSecurityMetadataSource를 상속받아 구현한 클래스이다.
FilterSecurityInterceptor는 SecurityMetaDatasource로부터 DB에 저장된 각 url의 권한 정보를 조회해오며, AccessDecisionHandler에게 이 url에게 설정된 권한 정보와 현재 접근자의 권한 정보를 비교하도록 요청함으로써 권한 평가를 수행한다
다시 말해, 최종적으로 권한 평가를 수행함으로써 해당 접근을 허용할지 거부할지 결정하는 FitlerSecurityInterceptor에게 DB에 저장되어 있는 url 권한 설정 정보를 넘겨주는 역할을 한다. 이 정보를 바탕으로 인터셉터는 현재 사용자의 권한 정보와 사용자가 접근하고자 하는 url에 설정되어 있는 권한 정보를 비교해 이 접근을 허용할지 결정한다
UrlResourcesMapFactoryBean
어플리케이션 최초 동작 시 , DB에 저장되어 있는 url 권한 정보를 추출하여 제공한다