Spring Security는 쉽게 말해 보안 담당자이다.
사이트에 누가 로그인했는지 확인하고, 접근해도 되는 페이지인지 판단해주는 경비원 역할을 한다.
기본 기능만 사용해도 로그인/로그아웃, 세션 관리 등이 된다. 하지만 실무에서 사용하려면 100% 커스터마이징이 필수이다.
왜 커스터마이징이 필요할까?
요구사항 | 기본 Spring Security로 가능? | 해결 방법 |
JWT 인증으로 로그인하고 싶다 | ❌ | 커스텀 필터 필요 |
로그인 실패 시 로그 찍고 싶다 | ❌ | 핸들러 오버라이드 |
/admin/** 경로는 관리자만 보게 하고 싶다 | ❌ | hasRole() 설정 필요 |
인증 실패 응답을 JSON으로 주고 싶다 | ❌ | AuthenticationEntryPoint 등록 |
즉, 기능은 있지만, 내가 원하는 방식은 아닐 것이다. 그래서 진짜 실무에선 필터, 핸들러, 서비스 다 바꿔서 쓰는게 기본이다.
실무에서 자주 쓰는 Spring Security 커스터마이징 TOP 5 에 대해서 알아보자
1. JWT 인증 필터 추가하기
Spring Security는 기본적으로 세션 기반인데, 요즘은 대부분 JWT 기반을 사용한다.
구현 흐름 :
- UsernamePasswordAuthhenticationFilter 이전에 커스텀 필터 등록
- 요청 헤더에서 JWT 호출 → 유효성 검사 → 인증객체 생성
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
String token = request.getHeader("Authorization");
if (token != null && jwtUtil.validateToken(token)) {
Authentication auth = jwtUtil.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
등록하기
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
2. 로그인 성공/실패 핸들러 커스터마이징
로그인 성공/실패 시, 특정 페이지로 리디렉션하거나 로그를 남기고 싶을 때 사용한다.
@Component
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
System.out.println("로그인 성공: " + authentication.getName());
response.sendRedirect("/dashboard");
}
}
등록은 .successHandler() / .failuerHandler() 에서 하면 된다.
3. URL 별로 접근 권한 다르게 설정
기본 설정만으로는 URL마다 세세한 권한 제어가 어렵다.
http.authorizeHttpRequests()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").authenticated()
.anyRequest().permitAll();
- hasRole("ADMIN") : 관리자만 접근 가능
- authenticated() : 로그인한 사람만 가능
- permitAll() : 아무나 접근 가능
4. 사용자 인증 커스터마이징
로그인 시 사용자를 어떻게 검증할지 우리가 직접 정할 수 있다.
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
Member member = memberRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자 없음"));
return new User(member.getEmail(), member.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_" + member.getRole())));
}
}
등록은 Security 설정에서 이렇게 한다.
.auth.userDetailsService(customUserDetailsService)
5. 예외 응답을 JSON으로 주기
기본적으로 인증 실패하면 403 페이지가 뜬다. API 서버는 JSON으로 에러 응답을 줘야 한다.
* 인증 실패(401 Unauthorized)
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"인증이 필요합니다\"}");
}
}
* 권한 없음(403 Forbidden)
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"접근 권한이 없습니다\"}");
}
}
등록은 마찬가지로 Security 설정에서 한다.
http.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler);
'Back-End > Spring Boot + Kotlin' 카테고리의 다른 글
OAuth2 로그인 시 커스터마이징 포인트 (0) | 2025.06.18 |
---|---|
Spring Bean? (5) | 2025.06.12 |
DB Connection Pool 고갈이 되면 무슨 일이 벌어질까? (0) | 2025.05.05 |
Kotlin val 은 왜 값이 변하는가?? (0) | 2025.04.28 |
Spring Boot 의존성 주입 (Dependency Injection) (2) | 2025.04.16 |