프로젝트를 진행하는데 인증(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
라고 부른다.
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
ConcurrentSessionFilter
BearerTokenAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
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에 적용하는 방법을 알아보자.
제가 쓰고 있는 시큐리티 적용은 극히 일부만 적용되어 있는 것 같습니다..
좀 더 깊이 공부하여 여러 시도를 해봐야겠습니다 !
정리를 잘 해주셔서 많은 도움을 받고 갑니다 ^_^