[Spring Security] Spring Security (Session) - Spring Security 6.1 이후
들어가며
Spring Security 6.1부터 기존에 사용하던 and()와 non-Lambda DSL Method가 Deprecated 되고, 필수적으로 Lambda DSL을 사용하도록 변경되었다.
변경된 내용으로 스프링 시큐리티 세션 로그인을 구현해보려 한다.
다만, 본 게시글은 스프링 시큐리티 위주의 내용만 작성하려고 한다. 로그인, 회원가입 등의 별도 비즈니스 로직 코드는 게시글 하단의 깃허브를 참고하길 바란다.
기술스택
- Spring Boot 3.3.1
- Spring Security 6.3.1
- JPA
- Session, Form Login 구현
- Spring Security 6.1 이후 lambda 문법을 이용한 코드 적용
스프링 시큐리티에 대한 설명은 해당 글을 참고하면 된다.
링크 : https://jangjjolkit.tistory.com/24
1. build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'com.jdh'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// log4jdbc
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
// thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}
test {
useJUnitPlatform()
}
compileJava {
options.compilerArgs << '-parameters'
}
2. SpringBootApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class SpringSecuritySessionApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecuritySessionApplication.class, args);
}
}
Spring Boot Security Auto Configuration을 비활성화하기 위해 @SpringBootApplication(exclude = SecurityAutoConfiguration.class)를 적용한다.
추가적으로, JPA를 사용하기 때문에 @EnableJpaAuditing 어노테이션을 사용했다.
3. Spring Security Configuration
/**
* Spring Security Config
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserGetService userGetService;
private final CustomLoginSuccessHandler customLoginSuccessHandler;
private final CustomLoginFailHandler customLoginFailHandler;
private final CustomLogoutSuccessHandler customLogoutSuccessHandler;
private final CustomAuthenticationEntryPointHandler customAuthenticationEntryPointHandler;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector);
}
@Bean
public SecurityFilterChain config(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspector);
// white list (Spring Security 체크 제외 목록)
MvcRequestMatcher[] permitAllWhiteList = {
mvc.pattern("/login-view"),
mvc.pattern("/login"),
mvc.pattern("/regist"),
mvc.pattern("/regist-action"),
mvc.pattern("/error/**"),
mvc.pattern("/favicon.ico")
};
// http request 인증 설정
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers(permitAllWhiteList).permitAll()
// 사용자 삭제는 관리자 권한만 가능
.requestMatchers("/delete").hasRole(RoleName.ROLE_ADMIN.getRole())
// 그 외 요청 체크
.anyRequest().authenticated()
);
// form login 중복 실행 방지
http.formLogin(login -> login
.loginProcessingUrl("/login")
.successForwardUrl("/dashboard")
.successHandler(customLoginSuccessHandler)
.failureHandler(customLoginFailHandler)
);
// logout 설정
http.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessHandler(customLogoutSuccessHandler)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
);
// csrf disable
http.csrf(AbstractHttpConfigurer::disable);
// session management
http.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
// entry point handler
http.exceptionHandling(conf -> conf
.authenticationEntryPoint(customAuthenticationEntryPointHandler)
.accessDeniedHandler(customAccessDeniedHandler)
);
// build
return http.build();
}
@Bean
public CustomAuthenticationProvider customAuthenticationProvider() {
return new CustomAuthenticationProvider(bCryptPasswordEncoder(), userGetService);
}
@Bean
public AuthenticationManager authenticationManager() {
CustomAuthenticationProvider authProvider = customAuthenticationProvider();
return new ProviderManager(authProvider);
}
}
기존에 and()를 사용하여 설정 옵션을 연결하는 방식이 아닌 Lambda DSL 방식을 사용하는 Security Configuration이다. Security, login, logout, Handler 설정 등을 담당한다. 또, 비밀번호 암/복호화에 사용할 BCryptPasswordEncoder와 특정 경로 매칭을 위한 MvcRequestMatcher를 bean으로 등록한다.
여기서 또 한 가지 체크해야 하는 부분은 requestMatcher 부분이다. Spring MVC를 사용 중이라면 requestMatchers(MvcRequestMatcher) 방식을 사용해야 하고, 아니라면 requestMatchers(AntPathRequestMatcher) 방식을 사용해야 한다.
4. Handler 구현
CustomLoginSuccessHandler.java
/**
* Custom Login Success Handler
*/
@Component
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// Spring Security Context Holder 인증 정보 set
SecurityContextHolder.getContext().setAuthentication(authentication);
response.sendRedirect("/dashboard");
}
}
AuthenticationSuccessHandler의 구현체 CustomLoginSuccessHandler를 구현한다. 이 핸들러는 로그인을 성공했을 때 동작하는 클래스이다. 해당 코드에선 Spring Security Context Holder에 인증정보를 set 하고, /dashboard로 리다이렉트 하도록 구현했다.
CustomLoginFailHandler.java
/**
* Custom Login Fail Handler
*/
@Slf4j
@Component
public class CustomLoginFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
log.info("[CustomLoginFailHandler] :: " + exception.getMessage());
response.sendRedirect("/login-view");
}
}
AuthenticationFailureHandler의 구현체 CustomLoginFailHandler를 구현한다. 이 핸들러는 로그인을 실패했을 때 동작하는 클래스이다. 해당 코드에선 로그인 화면으로 리다이렉트 하도록 구현했다.
CustomLogoutSuccessHandler.java
/**
* Custom Logout Success Handler
*/
@Slf4j
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
log.info("[CustomLogoutSuccessHandler] :: 로그아웃");
response.sendRedirect("/login-view");
}
}
LogoutSuccessHandler의 구현체 CustomLogoutSuccessHandler를 구현한다. 이 핸들러는 로그아웃을 성공했을 때 동작하는 클래스이다. 해당 코드에선 로그인 화면으로 리다이렉트 하도록 구현했다.
CustomAccessDeniedHandler.java
/**
* Custom Access Denied Handler
*/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.sendRedirect("/error/403");
}
}
AccessDeniedHandler의 구현체 CustomAccessDeniedHandler를 구현한다. 이 핸들러는 권한이 없는 사용자가 스프링 시큐리티에서 보호 중인 리소스에 접근할 때 동작하는 클래스이다. 해당 코드에선 403 에러 페이지로 리다이렉트 하도록 구현했다.
CustomAuthenticationEntryPointHandler.java
/**
* Custom Authentication Entry Point Handler
*/
@Slf4j
@Component
public class CustomAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
log.info("[CustomAuthenticationEntryPointHandler] :: {}", authException.getMessage());
log.info("[CustomAuthenticationEntryPointHandler] :: {}", request.getRequestURL());
log.info("[CustomAuthenticationEntryPointHandler] :: 로그인 인증 정보 없음");
response.sendRedirect("/error/401");
}
}
AuthenticationEntryPoint의 구현체 CustomAuthenticationEntryPointHandler를 구현한다. 이 핸들러는 사용자가 인증되지 않았거나 유효한 인증정보를 가지고 있지 않은 경우 동작하는 클래스이다. 한마디로 로그인을 하지 않은 사용자가 로그인이 필요한 리소스에 접근할 때 동작한다. 해당 코드에선 401 에러 페이지로 리다이렉트 하도록 구현했다.
5. AuthenticationProvider 구현
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final UserGetService userGetService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
// 사용자 정보 조회
UserGetResponseDTO userInfo = userGetService.getUserByUserId(username);
// password 일치 여부 체크
if(!bCryptPasswordEncoder.matches(password, userInfo.password()))
throw new LoginException(LoginExceptionResult.NOT_CORRECT);
// return UsernamePasswordAuthenticationToken
return new UsernamePasswordAuthenticationToken(
userInfo.id(),
userInfo.password(),
Collections.singleton(new SimpleGrantedAuthority(userInfo.roleName().name()))
);
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
AuthenticationProvider의 구현체 CustomAuthenticationProvider를 구현한다. AuthenticationProvider에선 Spring Security AuthenticationFilter가 전달해 준 인증정보(Authentication)를 토대로 사용자 인증을 진행한다. DB에서 사용자 정보를 가져와 실제 사용자의 입력정보와 비교한다. 인증이 완료되면 해당 사용자에 대한 UsernamePasswordAuthenticationToken 객체를 반환한다.
테스트
회원가입이 완료된 이후 기능부터 테스트를 진행하겠다.
1. 로그인
로그인에 실패할 경우 로그로 확인할 수 있다.
2. 대시보드
로그인에 성공할 경우 다음과 같은 대시보드 화면으로 넘어간다.
만약 로그인을 하지 않고 대시보드에 접근할 경우 401 에러페이지를 보여준다.
3. 로그아웃
로그아웃에 성공하면 로그인 화면으로 돌아온다.
4. 계정 삭제
스프링 시큐리티 설정에서 계정 삭제는 관리자 권한만 접근이 가능하도록 설정했다. 만약 권한이 없는 사용자가 계정 삭제를 시도하는 경우 403 에러페이지를 보여준다.
만약 권한이 있는 사용자(관리자)가 계정 삭제를 시도하면 정상적으로 처리된다.
DB의 del_yn이 true로 변경되어 삭제된 것을 볼 수 있다.
정리하며
위에서 언급했듯이 스프링 시큐리티 위주의 내용 설명만을 다뤘다. 자세한 코드는 깃허브를 참고하길 바란다.
링크 : https://github.com/JangDaeHyeok/SpringBoot-Security