프로젝트를 진행하는데 인증(Authentication)과 인가(Authorization)에 대한 처리 설정해야했다.
스프링 시큐리티라는 프레임워크를 알게되었고 자연스레 이에대한 공부를 할 수 있었다.
스프링 시큐리티는 "인증"과 "인가" 등의 애플리케이션 보안을 담당하는 스프링 하위 프레임워크이다.
스피링 시큐리티는 Filter의 흐름에 따라 "인증"과 "인가"를 처리하고있다.
개발자는 스프링시큐리티를 사용해서 보안관련 로직을 편하게 사용할 수 있는 장점이 있다.
인증(Authentication) : 'A'라고 주장하는 주체(user, subject, principal)가 'A'가 맞는지 확인하는 절차인가(Authorization) : 인증된 사용자가 요청한 자원에 접근 가능한지를 결정하는 절차 Spring Security에서는 이러한 인증과 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용한다.
Spring Security 적용방법은 아래에 있으니 이론부터 확인해보자
바로 적용을 하고싶다면 목록의Spring Security 적용을 확인하면 된다.
아래 사진은 SpringSecurity 문서에서 가져왔다.

우선 클리이언트의 Request가 오면 Filter chain을 거쳐 servlet(DispatcherServlet)에 도착한다.
이때 Spring Security는 메인 Filter Chain에 DelegatingFilterProxy 라는 필터를 만든다. (DispatcherServlet에 도착하기 전 상태)
조금 더 정확히는 SecurityFilterAutoConfiguration에서 DelegatingFilterProxyRegistrationBean을 만들고 여기서 DelegtingFilterProxy라는 filter를 만들어준다.
DelegatingFiterProxy의 내부에 FilterChainProxy라는 위임대상을 가지고 있다.
FilterChainProxy는 SpringSecurity에서 제공되는 특수 필터로 SpringSecurityFilterChain이라는 이름을 가진 Bean을 호출하여 SecurityFilter의 역할을 수행한다.
위 내용을 정리하면 아래와 같다.
사진과 같이 보며 이해하면 좋을 것이다.
Clinet -> FilterChain -> DelegatingFilterProxy (위임처리) -> FilterChainProxy -> Security Filter
이렇게 Spring Security가 적용되는 과정을 알아보았다.
이제 SecurityFilterChain가 어떻게 동작하는이 알아보자.
SpringSecurityFilterChain은 List의 형태로 구성되어있다.
이 리스트를 AuthenticationFilter라고 부른다.
WebAsyncManagerIntegrationFilterSecurityContextPersistenceFilterHeaderWriterFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterConcurrentSessionFilterBearerTokenAuthenticationFilter RequestCacheAwareFilterSecurityContextHolderAwareRequestFilter RememberMeAuthenticationFilter AnonymousAuthenticationFilter SessionManagementFilter ExceptionTranslationFilter -> 예외처리에서 사용할 예정이다.FilterSecurityInterceptor 여러 필터를 거치면서 앞 선 어떠한 필터에서 인증이 완료되면 해당 요청(Request)은 "인증된 요청"이 되는 것이다.
모든 필터를 거쳤는데 전부 다 인증에 실패하면 어떻게 될까? "인증되지 않은 요청"이 되는 것뿐이다.
그러면 인증이 안 됐으니까 해당 요청이 접근 권한이 없으므로 그에 따른 처리를 해주면 된다. 예를 들어 회원가입 페이지로 :redirect 하거나 Http Error Code : 403에 대한 처리 등을 하면 된다.
WebSecurityConfigurerAdapter을 상속받아 Filter Chain을 만드는 Class위에
@EnableWebSecurity(debug = true)어노테이션을 붙여주면 현재 실행되는 Security Fiter들을 확인할 수 있다.
아래 사진과 같이 나온다.

여기서 중요한것은 UsernamePasswordAuthenticationFilter 라는 필터이다.
이 필터가 아이디, 패스워드를 이용한 인증을 담당 하는 필터다.
이제 UsernamePasswordAuthenticationFilter가 어떻게 작동하는지 알아보자.
UsernamePasswordAuthenticationFilter 클래스 내의 attemptAuthentication(request, response) 메서드이다.
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
우선 Request로 넘어온 username,password를 통해 UsernamePasswordAuthenticationToken(Authentication)을 생성한다.
그 다음에 참조하고 있던 AuthenticationManager(구현체인 ProviderManager) 에게 인증을 진행하도록 위임한다.
UsernamePasswordAuthenticationToken 은 Authentication 인터페이스의 구현체다.
참고로 Authentication(Interface)을 구현한 구현체여야만 AuthenticationManager에서 인증 과정을 수행할 수 있다.
UsernamePasswordAuthenticationToken은 userName을 principal로 Password를 credentials로 받는다.
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
이제 AuthenticationManager가 인증을 해주는 데까지 왔다.
이제 AuthenticationManager가 무엇인지를 먼저 알아보고 동작을 살펴볼 것이다.
UsernamePasswordAuthenticationFilter -> UsernamePasswordAuthenticationToken(Authentication) -> AuthenticationManager(Interface) -> ProviderManager(Class) -> AuthenticationProvider(Interface)
AuthenticationManager(Interface)
Authentication 객체를 돌려주는 메서드를 구현하도록 하는 인터페이스다. (isAuthenticated(boolean)값을 TRUE로 바꿔준다.)ProviderManager(Class)
AuthenticationProvider들을에게 인증을 위임처리 후 결과를 AuthenticationManager에게 returnAuthenticationProvider (Interface)
ProviderManager에게 전달Authentication 인터페이스를 구현한 구현체 클래스(ex. UsernamePasswordAuthenticationToken)의 객체가 SecurityContext에 들어가고 SecurityContextHolder에 저장이 된다.

SecurityContextHolder를 통해 확인이 가능하다.자 이제 이론적인부분을 봤으니 Spring Security를 적용해보자.
Security dependency를 추가해주자.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
}
스프링부트에서는 @EnableAutoConfiguration을통해 SecurityFilterAutoConfiguration클래스를 로드하고 디폴트로 이름이 "springSecurityFilterChain" 빈을 등록해준다.
그리고 @Bean SecurityFilterChain을 등록하면 적용이 된다.
@RequiredArgsConstructor
@EnableWebSecurity(debug = true) // request가 올 떄마다 어떤 filter를 사용하고 있는지 출력을 해준다.
public class SecurityConfig {
private final UserService userService;
@Value("${jwt.secret}")
private String secretKey;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll() // join, login은 언제나 가능
.antMatchers(HttpMethod.POST, "/api/v1/posts/**").authenticated()
.antMatchers(HttpMethod.POST, "/api/v1/users/{id}/role/change").access("hasRole('ROLE_ADMIN')")
.antMatchers(HttpMethod.PUT, "/api/**").authenticated()
.antMatchers(HttpMethod.DELETE, "/api/**").authenticated()
.antMatchers(HttpMethod.GET, "/api/v1/posts/my").authenticated()
.and()
.exceptionHandling()// 예외처리기능 작동
.authenticationEntryPoint(new CustomAuthenticationEntryPointHandler()) // 인증처리 실패시 처리
.accessDeniedHandler(new CustomAccessDeniedHandler())// 인가처리 실패시 처리
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // STATELESS = jwt사용하는 경우 씀 : 매번 토큰을 사용하는 개념?
.and()
.addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
.addFilterBefore(new ExceptionHandlerFilter(), JwtFilter.class)
.build();
}
}
HttpSecurity를 이용해 스프링 시큐리티 관련 설정을 한다.
현재는 permitAll(), hasRole(), authenticated() 만 사용해서 권한 설정을 해주고 있는데 권한 설정 관련 메소드들은 다음과 같다.
authenticated() : 인증된 사용자의 접근을 허용permitAll() : 모든 사용자 허용denyAll() : 모든 사용자 거부hasRole(Role) : Role 에 해당하는 사용자만 허용hasAnyRole(Roles...): Role 중 하나라도 해당하면 허용hasIpAddress : 해당 IP 를 가지고 있는 사용자인 경우 허용이제 시큐리티 적용이 된것을 확인할 수 있다.
그런데 위의 코드에는 JWT의 내용도 들어있는것을 확인 할 수 있다.
다음에는 JWT를 Security Filter에 적용하는 방법을 알아보자.
제가 쓰고 있는 시큐리티 적용은 극히 일부만 적용되어 있는 것 같습니다..
좀 더 깊이 공부하여 여러 시도를 해봐야겠습니다 !
정리를 잘 해주셔서 많은 도움을 받고 갑니다 ^_^