코드를 작성하기 전에 Spring Security가 무엇이고 왜 사용해야 하는지 알아보았다.
Spring Security란?
Spring 기반으로 어플리케이션 보안(인증, 권한, 인가 등) 기능을 제공하는 스프링 하위 프레임워크.
Spring Security는 인증과 권한에 대한 부분을 Filter 흐름에 따라 처리한다.
이 Filter는 Dispatcher Servlet으로 가기 전에 적용되기 때문에 가장 먼저 URL의 요청을 받는다.
Interceptor와는 다른 점이 Interceptor는 Dispatcher Servlet과 Controller 사이에 위치하기 때문에 적용되는 시점이 다른 차이가 있다.
Spring Security는 보안과 관련하여 쳬계적으로 많은 옵션을 제공하기 때문에 개발자의 입장에서는 이런 보안 로직들을 하나하나 작성할 필요가 없다는 장점이 있다.
Spring Security 주요 모듈
SecurityContextHolder
보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 컨텍승트에 대한 세부 정보가 저장된다.
SecurityContext
Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다.
Authentication
현재 접근하는 주체의 정보와 권한을 담은 인터페이스. Authentication 객체는 Security Context에 저장되며 SecurityContextHolder를 통해 SecurityContext에 접근하고 SecurityContext를 통해 Authentication에 접근할 수 있다.
AuthenticationProvider
실제 인증에 대해 처리하는 부분이다. 인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다. AuthenticationProvider 인터페이스를 구현해서 Custom 한 AuthenticationProvider를 작성하여 AuthenticationManager에 등록하면 된다.
Authentication Manager
인증에 대한 부분을 처리하는 곳이다. 실질적으론 AuthenticationMnager에 등록된 AuthenticationProvider에 의해서 처리된다. 인증이 성공하면 인증이 성공한 객체를 생성하여 Security Context에 저장한다. 세션을 사용할 경우 인증 상태를 유지하기 위해 세션에 보관한다.
Password Encoding
암호화에 사용될 PasswordEncoder구현체를 지정할 수 있다. AuthenticationManagerBuilder.userDetailService().passwordEncoder()를 통해 지정한다.
GrantedAuthority
사용자가 가지고 있는 권한이 있는지를 검사하고 접근 허용 여부를 결정한다.
Spring Security 아키텍처
사실 이 부분은 봐도 자세히는 모르겠지만 흐름은 대충 알아야 할 것 같아 정리했다.
- 사용자가 Form형태로 로그인 정보를 입력 후 인증 HttpRequest를 서버로 전송
- 전송이 오면 AuthenticationFilter로 요청이 먼저 오고 아이디와 비밀번호를 기반으로 UserPasswordAuthenticationToken을 발급 해주어야 한다.
- UserNamePasswordToken을 Authentication Manager에게 전달한다. Authentication Manager는 인증을 처리할 여러개의 AuthenticationProvider를 가지고 있다.
- UsernamePasswordToken을 Authentication Provider에게 전달한다. Authentication Manager는 전달 받은 토큰을 순차적으로 AuthenticationProvider들에게 전달하여 실제 인증 과정을 수행해야 한다. 실제 인증에 대한 부분은 authenticate함수에 작성을 해야 하고 Spring Security에서는 DB에서 데이터를 조회할 때 Username으로 조회하기 때문에 전달받은 토큰으로부터 아이디를 조회하고 비밀번호가 일치한 지 확인한다.
- UserDetailsService로 조회할 아이디를 전달한다. UserDetailsService는 인터페이스라서 이를 구현할 클래스를 작성해야하며 여기서는 아이디를 기반으로 데이터를 조회해야 한다.
- 아이디를 기반으로 DB에서 데이터를 조회한다
- 조회한 결과를 반환한다.
- 인증 처리 후 토큰을 AuthenticationManager에 반환 AuthenticationProvider에서 UserDetalisService를 통해 조회한 정보와 입력받은 비밀번호가 일치하는지 확인이 일어나고 일치한다면 인증된 토큰을 반환해야 한다. DB에 비밀번호는 보통 암호화 되어있기 때문에 똑같이 암호화 후 비교를 한다.
- 인증된 토큰을 AuthenticationFilter에 전달 인증이 완료된 UsernamePasswordAuthenticationToken을 AuthenticationFilter로 반환하고 AuthenticationFiler는 LoginSuccessHandler로 전달한다.
- 인증된 토큰을 SecurityContoextHolder에 저장한다.
이제 직접 만들어보면서 이해해 보자.
auth-api에 Spring Security를 붙여보겠다.
시작하기에 앞서 회원 가입, 로그인 틀을 준비해 오자. security 적용 후에 jwt 토큰과 함께 로그인을 완성할 것이다.
그리고 member-api처럼 필요한 의존성들을 추가하고 예외처리, JpaRepository 등 필요한 기능들을 추가해 주자.
Spring Security 적용
우선 각 모듈마다 Security를 적용할지 공통으로 뺄지 고민을 하였다.
공통으로 사용하니 빼서 인증 인가 모듈을 새로 만들면 좋다고 생각했지만, 내 생각대로 잘 동작할까?라는 생각과 공통으로 뺐다가 의도치 않게 모듈끼리 의존을 많이 하진 않을까?라는 생각도 들었다.
하지만 공통되는 코드들을 복붙 하며 여러 곳에서 보는 것이 더 싫었고 우선 공통 모듈로 빼고 문제가 발생하면 다시 고민하고 처리하기로 했다.
인증 인가 모듈 생성
위와 같이 새로운 공통 모듈을 생성한다.
다음으로는 Security 의존성을 추가해 준다.
build.gradle(common-authentication 모듈)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security' //Spring Security
}
이제 바로 Security가 적용되는지 확인해 보겠다.
config/AuthenticationConfig (common-authentication 모듈)
package com.seungh1024.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@EnableWebSecurity
public class AuthenticationConfig {
}
@EnableWebSecurity 어노테이션을 달아주면 Security가 활성화된다.
Security가 활성화되면 회원 가입, 로그인 틀을 만든 Controller가 작동하기 전에 Security가 가로채서 확인할 것이다.
이제 새로 만든 이 모듈을 api모듈에 추가한 후 웹에서 테스트해보면 아래와 같은 화면이 나오는 것을 확인할 수 있다.
이제 AuthenticationConfig에 기본적인 설정들을 추가해 보자.
package com.seungh1024.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class AuthenticationConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
return httpSecurity
.httpBasic().disable() // ------------------------------- 1
.csrf().disable() // ------------------------------------ 2
.cors().and()
.authorizeHttpRequests() // ------------------------------3
.requestMatchers("/api/v1/auth/signup","/api/v1/auth/signin").permitAll()
.anyRequest().authenticated()//---------------------------4
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)//-5
.and()
// .addFilterBefore()
.build();
}
}
- 인증은 UI가 아니라 jwt를 사용하여 토큰으로 인증할 계획이기 때문에 httpBasic()은 비활성화시켰다.
- csrf()의 경우 Cross Site Request Forgery로 사이트 간 위조 요청인데 정상적인 사용자가 의도치 않은 위조요청을 보내는 것을 의미한다. CSRF protection은 Spring Security에서 default로 설정되며 protection을 통해 상태를 변화할 수 있는 POST, PUT, DELETE 요청으로부터 보호한다. 이 기능은 브라우저를 통해 Request를 받을 때 사용하라고 한다. disable()을 하는 이유는 Rest API를 이용한 서버라면 세션과 다르게 stateless 하다. 즉 서버에 인증정보를 보관하지 않기 때문에 굳이 불필요한 csrf 코드를 작성할 필요가 없게 된다.
- authorizeHttpRequests() → 여기서부터 요청에 대해 authorize를 하는 것. 기존에 authorizeRequests() 였는데 deprecated 됐다.
- requestMatchers()로 어디로 들어오는 요청인지, 뒤에 어떻게 처리할 건지 정한 것이다. permitAll()로 모두 허용을 하였다. 로그인의 경우 인증이 필요 없기 때문이다. 얘도 antMatchers가 deprecated 되어 requestMatchers()로 바뀌었다. anyRequest() 나머지 모든 요청에 대해서는 authenticated() → 인증이 필요하게 했다. anyRequest() 말고 requestMatchers(HttpMethod.POST, “/api/v1/**")와 같이 특정 메소드에 대한 End Point 접근을 막을 수도 있다.
requestMatchers()로 모든 요청을 막으려면 "/api/v1/**"과 같이 모든 요청 경로를 적용하면 된다. - sessionManagement() → 세션 설정을 하는 것으로 sessionCreationPolicy(SessionCreationPolicy.STATELESS)를 하면 세션을 사용하지 않는다. JWT로 구현할 것이기 때문에 굳이 세션이 필요가 없다.
이제 회원가입, 로그인은 인증 없이 가능하니 테스트를 해보면
잘 되는 것을 볼 수 있다.
추가로 테스트를 위해 만든 API에서는
오른쪽 아래를 보면 403 에러가 발생한 것을 볼 수 있다.
다음엔 jwt를 연동하여 토큰을 발급하고 인증 계층을 추가해보겠다.
'Java > Spring boot 프로젝트' 카테고리의 다른 글
Spring Security + JWT 적용하기(3) - 게시판 만들기(12) (0) | 2023.04.15 |
---|---|
Spring Security + JWT 적용하기(2) - 게시판 만들기(11) (0) | 2023.04.06 |
Entity 모듈 분리(멀티 모듈 프로젝트) - 게시판 만들기(9) (0) | 2023.03.22 |
@Valid를 이용한 객체 유효성 검증 - 게시판 만들기(8) (0) | 2023.03.17 |
Spring Entity와 DTO 구분하기 -게시판 만들기(7) (0) | 2023.03.16 |