Spring Security에 대해 설명하기 전에 숙지해야 할 두 가지 개념이 있습니다. 바로 authentication(권한)과 authorization(인증)입니다. 해당 라이브러리의 구조에서는 이 두 가지를 분리해서 사용할 수 있게 했는데요. 개념을 보자면 다음과 같습니다.
- 서버에 접근하려는 주체(user, subject, principal)가 올바른 주체인지 확인하는 과정입니다.
- 인증 과정에서는 ID/PASSWORD, jwt-token, OAuth 등이 인증에 필요한 값을 전달합니다.
- session과 cookie 같은 수단들 역시 인증 방식을 이루는 하나의 개념이라 볼 수 있습니다.
- 인증 절차가 끝난 뒤, 해당 주체가 접근할 수 있는 범위를 설정하는 과정입니다.
- 사용자는 부여된 authorization를 기준으로 서버 자원에 달리 접근할 수 있습니다.
- 정회원, 준회원, 관리자 회원 등과 같은 등급 분류가 권한에 속합니다.
- 개인 정보와 게시물의 공개/비공개 여부 설정과 회원가입 과정 또한 하나의 권한 부여과정이라 볼 수 있습니다.
- 위에 언급된 인증과 권한 과정에서, 해당 resource에 접근하는 시스템 혹은 사용자를 뜻합니다.
- 자바 스프링 시큐리티에서는 '인증되는 주체의 ID'가 이에 속합니다.
- 인증 과정 중, 주체가 본인을 인증하기 위해 서버에 제공하는 ID/Password 같은 정보들을 포괄합니다.
- token에 담긴 값 역시 credential이며, 보안을 위해서는 해당 정보가 암호화 되어야 합니다.
사실 어려운 개념은 아니지만, 영단어 기준으로 각각 'auth-'으로 시작해서 '-ation'으로 끝나다보니, 비영어권 화자들에겐 조금 혼란스러울 때가 있긴 합니다.
해당 개념을 풀어쓰기 어렵다면, 그냥 이렇게 생각하면 됩니다.

로그인 로그아웃은 authentication이며, 회원가입과 탈퇴는 authorization이고, 해당 과정은 credential에 담긴 정보를 확인하여 이루어집니다.
여튼 위에 언급된 개념들은 spring security에서 지원하는 라이브러리를 통해 인터페이스로 구현할 수 있습니다.

웹 개발에 있어서 비즈니스 로직을 호출 하기 이전과 이후에 공통적으로 처리해야 하는 프로세스들이 있습니다. 관점 지향 프로그래밍(Aspect-Oriented Programming, AOP)에서는 이를 두 가지로 구분합니다.
핵심 관심사(Core Concern) - 각 객체가 가져야 할 본래의 기능을 뜻합니다.
공통 관심사(Cross-cutting Concern) - 여러 객체에서 공통적으로 사용되는 코드를 말합니다.
해당 관심사를 두 가지로 나눈 이유는 간단합니다. 각 클래스 마다 중복될 수 있는 소스 코드를 줄임으로서, 로깅, 보안, 트랜잭션 관리 등과 같은 공통적인 관심사를 모듈화 하여 코드 중복을 줄이고 유지 보수성을 향상하기 위해서 입니다.
스프링에서는 위에서 언급한 공통 관심사를 처리하기 위한 방법으로 크게 Filter, Interceptor, Spring AOP가 있는데요, 일단 filter와 Intercepter의 차이를 알아보겠습니다.

필터는 J2EE표준에 의해 제공되는 클래스입니다. 인증, 로그인, 이미지 전송, 암호화 등의 기능을 지원하며, Dispatcher Servlet에 요청이 전달되기 전 / 후에 url 패턴에 맞는 모든 요청에 대해 부가 작업을 처리할 수 있는 기능을 제공 합니다.
필터는 tomcat과 같은 웹 컨테이너에서 제공되는 기능으로서 스프링 바깥에서 실행됩니다. 모든 필터들은 아래의 인터페이스를 공통적으로 구현합니다.
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
init()doFilter()destroy()Interceptor는 SpringContext, 즉 스프링 내부에서 동작합니다. 인터셉터의 사용 단계는 Dispatcher Sevlet이 특정 URI로 요청하여 Controller로 호출하기 전후로서, 인터셉터는 이 과정에 끼어들어 request 혹은 response를 참조하거나 가공하는 기능을 제공합니다.org.springframework.web.servlet의 HandlerInterceptor 인터페이스를 구현(implements)하며, 해당 인터페이스는 다음과 같은 메서드를 가집니다.public interface HandlerInterceptor {
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler);
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mav);
void afterCompletion(HttpServletRequest request, HttpServeletResponse response, Object handler, Exception ex);
}
preHandle() postHandle() ModelAndView 타입의 정보가 제공됩니다. @RestController 의 도입 이후 활용도가 줄었다고 합니다.afterCompletion() filter
- java에서 지원합니다.
- dispatch-servlet의 호출 전후에 실행합니다.
- sevlret request/response의 교체가 가능합니다.
- filter에서 발생하는 예외처리는 web Application에서 이루어집니다.
interceptor
- SpringContext에서 지원합니다.
- sevlet의 요청이 controller로 전송될 때 실행 됩니다.
- interceptor는 해당 request와 response를 받아서 처리합니다.

Spring Security는 위에서 언급한 기능들을 제공하는 프레임워크입니다. Spring Security의 인증 절차는 일련의 FilterChain을 거친 뒤, 해당 요청이 dispatcher-servlet으로 가기 전 적용이 됩니다.
스프링 시큐리티가 제공하는 모듈들은 아래와 같습니다.
SecurityContextHolder
보안 주체의 세부 정보를 포함하는 응용프로그램의, SecurityContext에 대한 현재 세부 정보가 저장되는 인터페이스입니다.
SecurityContext
Authentication를 보호하는 모듈입니다. Authentication 객체를 꺼내올 수 있습니다.
Authentication
Authentication는 현재 접근하는 주체의 정보와 권한을 담는 인터페이스로서, Security Context에 저장됩니다. 해당 객체에 접근하기 위해서는 다음과 같은 방법을 사용할 수 있습니다.
SecurityContextHolder.getContext().getAuthentication()
UsernamePasswordAuthenticationToken
Username과 Password의 파라미터를 받아 각각 Principal과 Credential의 역할을 부여합니다.
AuthenticationProvider
인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 하는 인터페이스 입니다. 해당 인터페이스는 AuthenticationProvider를 구현하여 개발자의 의도에 따라 custom한 뒤, AuthenticaitonManager에 등록하여 사용할 수 있습니다.
AuthenticationManager
SpringSecurity에서 인증 프로세스를 통괄하는 모듈입니다.
해당 구현체에 등록된 각각의 AuthenticationProvider들의 로직에 따라 그 과정이 실행됩니다.
UserDetails
일반적으로 개발자가 직접 생성한 사용자 정보, 이를테면 DTO, VO, Entity와 같은 모델 클래스에 implements하여 사용하는 인터페이스입니다.
인증 프로세스를 성공하여 생성된 UserDetails 객체는 Authentication 객체와 함께 UsernamePasswordAuthenticationToken에 구현할 수 있습니다.
UserDetailsService
이름과 같이 Spring MVC 패턴에서 Service와 같은 역할을 합니다.
UserRepository의 주입을 받아 DB와 연결해 처리됩니다.
PasswordEncoding
패스워드 암호화에 사용 될 PasswordEncoder의 구현체를 지정하는 모듈입니다.
해당 모듈은 사용자 패스워드 뿐만이 아닌 모든 암호화 프로세스에서 사용 될 수 있습니다.
GrantedAuthority
사용자(principal)이 지닌 권한(authority)의 인증 여부를 반환하는 인터페이스 입니다.
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthentificationFilter extends OncePerRequestFilter
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
private final TokenRepository tokenRepository;
private final UserRepository userRepository;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if(!isAuthHeaderValid(authHeader)){
filterChain.doFilter(request, response);
}else{
jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt);
if(userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
var isTokenValid = tokenRepository.findByToken(jwt)
.map(t -> !t.isExpired() && !t.isRevoked())
.orElse(false);
if((jwtService.isTokenValid(jwt, userDetails) && isTokenValid)){
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
filterChain.doFilter(request, response);
}
}
}
private boolean isAuthHeaderValid(String authHeader){
if(authHeader!=null && authHeader.startsWith("Bearer ")) return true;
return false;
}
}
Spring Security로 처리하는 필터입니다.OncePerRequestFilter를 상속 받아 해당 필터는 요청 과정에서 단 한번만 실행하도록 합니다.isAuthHeaderValid()를 통해 서블릿 헤더 값에서 Bearer 여부를 반환한 뒤, 이를 통과하면 jwtService를 통해 DB에서 사용자 정보 유무를 확인합니다. SecurityContextHolder에서 context 내부에서 보호되는 authentication 객체를 호출합니다. 인증 절차가 끝나면, 해당하는 request에 따라 토큰의 유효성을 판별합니다. @Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@RequiredArgsConstructor
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class SecurityConfiguration {
private final JwtAuthentificationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeHttpRequests()
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/user/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement(management -> management
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout
.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext()));
return http.build();
}
@Configuration(proxyBeanMethods = '')
어노테이션을 통해 해당 클래스를 컴포넌트로 설정하여, 스프링 시큐리티와 관련된 빈의 의존성 주입을 자동화 하게 합니다.
@EnableWebSecurity
스프링 시큐리티를 사용할 수 있도록 선언하는 어노테이션입니다.
@RequiredArgsConstructor
lombok에서 생성자 주입을 자동으로 설정하는 어노테이션입니다. 스프링시큐리티와는 직접적인 관련이 없습니다.
@ConditionalOnDefaultWebSecurity
해당 클래스를 스프링 시큐리티의 기본값으로 설정합니다. 일반적으로 스프링 시큐리티의 의존성을 주입할 경우, 스프링에서 자체적으로 필터 체인을 생성하므로 사용자가 정의한 값과 충돌이 일어나기 때문에 이를 어노테이션으로 선언합니다.
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
해당 클래스의 웹 어플리케이션 기반을 설정합니다.
SecurityFilterChain
HttpSecurity 객체를 받아 해당 구조의 필터를 설정하는 bean입니다.
csrf() 사이트 간 요청 위조(Cross Site Request Forger)의 사용 여부를 체크합니다.
위에서 직접 설정한 JwtAuthentificationFilter객체를 .addFilterBefore()에 주입합니다. UsernamePasswordAuthenticationFilter.class 보다 이전 순위에 넣음으로서, 해당 필터가 SpringSecurityFilter에서 ID/비밀번호 인증 전에 실행하도록 합니다.
로그아웃 요청을 처리하는 컴포넌트의 url은 별도로 작성했습니다. 로그아웃 처리는 해당 url을 통해 인터셉트 합니다.
https://jeong-pro.tistory.com/205
https://mangkyu.tistory.com/173
https://getinthere.tistory.com/29
https://winter1396love.tistory.com/78
https://mangkyu.tistory.com/18
https://haneepark.github.io/2018/04/21/authentication-authorization/
https://www.baeldung.com/spring-bean
https://toneyparky.tistory.com/6
https://jaehoney.tistory.com/174
https://catsbi.oopy.io/9ed2ec2b-b8f3-43f7-99fa-32f69f059171
https://meetup.nhncloud.com/posts/151
https://6161990src.tistory.com/94
https://code-lab1.tistory.com/193
https://velog.io/@hsgin11/HongSpring%EC%97%90%EC%84%9C-Handler
비문, 오탈자와 코드 오류 및 잘못된 지식에 대한 지적 및 질문은 언제나 환영합니다.