Spring Security는 쉽게 말해 보안 담당자이다.

사이트에 누가 로그인했는지 확인하고, 접근해도 되는 페이지인지 판단해주는 경비원 역할을 한다. 

 

기본 기능만 사용해도 로그인/로그아웃, 세션 관리 등이 된다. 하지만 실무에서 사용하려면 100% 커스터마이징이 필수이다.

 

왜 커스터마이징이 필요할까?

요구사항 기본 Spring Security로 가능?  해결 방법
JWT 인증으로 로그인하고 싶다 커스텀 필터 필요
로그인 실패 시 로그 찍고 싶다 핸들러 오버라이드
/admin/** 경로는 관리자만 보게 하고 싶다 hasRole() 설정 필요
인증 실패 응답을 JSON으로 주고 싶다 AuthenticationEntryPoint 등록

 

즉, 기능은 있지만, 내가 원하는 방식은 아닐 것이다. 그래서 진짜 실무에선 필터, 핸들러, 서비스 다 바꿔서 쓰는게 기본이다.

 

 

실무에서 자주 쓰는 Spring Security 커스터마이징 TOP 5 에 대해서 알아보자

 

 

1. JWT 인증 필터 추가하기

Spring Security는 기본적으로 세션 기반인데, 요즘은 대부분 JWT 기반을 사용한다.

 

구현 흐름 : 

  1. UsernamePasswordAuthhenticationFilter 이전에 커스텀 필터 등록
  2. 요청 헤더에서 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);

+ Recent posts