[Spring] Spring Security - JWT
🤖 Spirng Security?
강력한 사용자 인증 및 Access 제어 framework
이다. 이는 Java 애플리케이션에 인증 및 권한 부여를 제공하는데 중점을 두었으며 다양한 필터를 사용하여 커스터마이징이 가능하다.
용어
'인증(Authentication)은 주체(Principal)의 신원(Identity)를 증명하는 과정입니다.' 라는 주장을 검증하는 과정이다.
-
주체(Principal)
보통 유저(사용자)를 가리키며 주체는 자신을 인증해달라고 신원 증명 정보, 즉 Credential을 제시한다. 주체가 유저일 경우 크레덴셜은 대개 패스워드이다. -
인가(Authorization, 권한 부여)
인증을 마친 유저에게 권한 Authority을 부여하여 대상 애플리케이션의 특정 리소스에 접근할 수 있게 허가하는 과정이다. 인가는 반드시 인증 과정 이후 수행돼야하며 권한은 롤 형태로 부여하는 것이 일반적이다. -
접근통제(Access Control, 접근 제어)
애플리케이션 리소스에 접근하는 행위를 제어하는 일이다. 따라서 어떤 유저가 어떤 리소스에 접근하도록 허락할지를 결정하는 행위, 즉 접근 통제 결정이 뒤따른다. 리소스의 접근 속성과 유저에게 부여된 권한 또는 다른 속성들을 견주어 결정한다.
🪴 FilterChain
-
Spring Security는 표준 서블릿 필터를 사용한다.
-
다른 요청들과 마찬가지로
HttpServletRequest
와HttpServletResponse
를 사용한다. -
Spring Security는 서비스 설정에 따라 필터를 내부적으로 구성한다. 각 필터는 각자 역할이 있고 필터 사이의 종속성이 있으므로 순서가 중요하다.
-
XML Tag를 이용한 네임스페이스 구성을 사용하는 경우 필터가 자동으로 구성되지만, 네임스페이스 구성이 지원하지 않는 기능을 써야하거나 커스터마이징된 필터를 사용해야 할 경우 명시적으로 빈을 등록 할 수 있다.
클라이언트가 요청을 하면 DelegatingFilterProxy
가 요청을 가로채고 Spring Security의 빈으로 전달한다. ( DispatcherServlet보다 먼저 실행 된다.)
이 DeletgatingFilterProxy
는 web.xml과 applicationContext 사이의 링크를 제공한다.
따라서 DelegatingFilterProxy
는 한 마디로 Spring의 애플리케이션 컨텍스트에서 얻은 Filter Bean을 대신 실행한다.
그러니 이 Bean은 javax.servlet.Filter를 구현해야한다. 이 포스팅에서는 jwtAuthenticationFilter가 되겠다. (밑에 소스 코드 참조)
Spring Security Filter Chain은 아래와 같이 다양하며 커스마이징이 가능하다.
-
SecurityContextPersistentFilter :
SecurityContextRepository에서 SecurityContext를 가져와서 SecurityContextHolder에 주입하거나 반대로 저장하는 역할을 합니다. -
LogoutFilter :
logout 요청을 감시하며, 요청시 인증 주체(Principal)를 로그아웃 시킵니다. -
UsernamePasswordAuthenticationFilter :
login 요청을 감시하며, 인증 과정을 진행합니다. -
DefaultLoginPageGenerationFilter :
사용자가 별도의 로그인 페이지를 구현하지 않은 경우, 스프링에서 기본적으로 설정한 로그인 페이지로 넘어가게 합니다. -
BasicAuthenticationFilter :
HTTP 요청의 (BASIC)인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장합니다. -
RememberMeAuthenticationFilter :
SecurityContext에 인증(Authentication) 객체가 있는지 확인하고 RememberMeServices를 구현한 객체 요청이 있을 경우, RememberMe를 인증 토큰으로 컨텍스트에 주입합니다. -
AnonymousAuthenticationFilter :
이 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면 익명 사용자로 취급합니다. -
SessionManagementFilter :
요청이 시작된 이후 인증된 사용자인지 확인하고, 인증된 사용자일 경우 SessionAuthenticationStrategy를 호출하여 세션 고정 보호 매커니즘을 활성화 하거나 여러 동시 로그인을 확인하는 것과 같은 세션 관련 활동을 수행합니다. -
ExceptionTranslationFilter :
필터체인 내에서 발생되는 모든 예외를 처리합니다. -
FilterSecurityInterceptor :
AccessDecisionManager로 권한부여처리를 위임하고 HTTP 리소스의 보안 처리를 수행합니다.
🏰 Spring Security 구조
-
사용자가 입력한 사용자 정보를 가지고 인증을 요청한다.(Request)
-
AuthenticationFilter가 이를 가로채 UsernamePasswordAuthenticationToken(인증용 객체)를 생성한다
-
필터는 요청을 처리하고 AuthenticationManager의 구현체 ProviderManager에 Authentication과 UsernamePasswordAuthenticationToken을 전달한다.
-
AuthenticationManager는 검증을 위해 AuthenticationProvider에게 Authentication과 UsernamePasswordAuthenticationToken을 전달한다.
-
이제 DB에 담긴 사용자 인증정보와 비교하기 위해 UserDetailsService에 사용자 정보를 넘겨준다.
-
DB에서 찾은 사용자 정보인 UserDetails 객체를 만든다.
-
AuthenticationProvider는 UserDetails를 넘겨받고 비교한다.
-
인증이 완료되면 권한과 사용자 정보를 담은 Authentication 객체가 반환된다.
-
AuthenticationFilter까지 Authentication정보를 전달한다.
-
Authentication을 SecurityContext에 저장한다.
Authentication 정보는 결국 SecurityContextHolder 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장한다. 세션에 사용자 정보를 저장한다는 것은 전통적인 세션-쿠키 기반의 인증 방식을 사용한다는 것을 의미한다.
🤡 JWT(Json Web Token)
JWT(Json Web Token)은 웹표준(RFC7519)로서 일반적으로 클라이언트와 서버, 서비스와 서비스 사이 통신 시 권한 인가(Authorization)을 위해 사용하는 토큰이다.
HEADER.PAYLOAD.SIGNATURE
일반적으로 헤더, 페이로드, 서명 세 부분을 점으로 구분하는 구조로 되어있다.
헤더에는 토큰 타입과 해싱 알고리즘을 저장하고
페이로드에는 실제로 전달하는 정보, 서명에는 위변조를 방지하기 위한 값이 들어가게 된다.
사용자 인증이 완료될 시 서버 측에서는 JWT 토큰을 Body에 담아 클라이언트에 전달하고 그 후 요청하는 API 서버에 JWT 토큰을 헤더에 담아 요청을 하게 되면 이를 확인하고 권한이 있는 사용자에게 리소스를 제공하게 된다.
🧞♂️ 예제
개발 환경
gradle
Java 8
build.gradle
dependencies {
.
.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
.
.
.
}
일단 필요한 라이브러리들을 받아주고, domain 객체부터 작성한다.
User.java
@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name="USERS")
public class User{
@Id
@Column(name = "user_id")
private String userId;
@Column(name = "user_name")
private String userName;
@Column(name = "user_pwd")
private String userPwd;
@Column
private String company;
@Column
private String position;
}
그 후, DB 접근 시 사용할 Repository와 Service를 만들어줬다. (Service의 경우 나중에 만들어도 된다.)
UserService.java
UserService의 경우 인증용 도메인과 API 용을 따로 만든다고 한다.
(여기선 하나만 만든다.)
@Service
public class UserService {
@Autowired
UserRepository userRepository;
.
.
.
public Optional<User> findByIdPw(String id) {
return userRepository.findById(id);
}
.
.
}
그 후 Spring Security Filter Chain을 사용한다는 것을 명시해줘야 한다.
WebSecurityConfigurerAdapter
를 상속 받은 Configuration 객체를 일단 만들어주고 거기에 @EnableWebSecurity 어노테이션을 달아주면 된다.
SecurityConfiguration.java
@Slf4j
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private final JwtAuthenticationEntryPoint unauthorizedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors() //(1)
.and()
.csrf() //(2)
.disable()
.exceptionHandling() //(3)
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement() //(4)
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests() // (5)
.antMatchers(RestControllerBase.API_URI_PREFIX + "/auth/**")
.permitAll()
.antMatchers(RestControllerBase.API_URI_PREFIX + "/**")
.authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.formLogin().disable().headers().frameOptions().disable();
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE"));
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
//비밀번호 암호화를 위한 Encoder 설정
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(1) 교차출처 리소스 공유(CORS) 설정이다. 만약 react나 vue.js를 클라이언트로 사용한다면 포트 넘버가 다르다.
그래서 corsConfigurationSource Bean을 통해
- addAllowedOrigin (Access-Control-Allow-Origin)에 모든 출처를 허용
- setAllowedMethods에 위와 같은 Request 방식을 허용
- addAllowedHeader (Access-Control-Allow-Methods)를 허용하도록 설정
(2) CSRF(Cross Site Request Forgery) 사이트 간 요청 위조 설정이다. 이건 설정이 복잡하기도하고 REST API 서버용으로만 사용할 것이기 때문에 disable로 처리했다.
(3) 인증, 허가 에러 시 공통적으로 처리해주는 부분이다.
🐝 Spring security JWT 처리 순서
1. JwtAuthFilter
- 해당 필터는 서버에 접근하는 모든 API의 JWT 토큰에 대해 유효성 검사를 진행한다. (ex. 토큰의 위변조, 유효 기간 등등)
- 검사 진행 후 컨트롤러로 보내게 된다.
- API 중 로그인 전에 허용되어야 하는 API에 대해 FilterSkipMatcher 가 예외 처리를 해준다. (로그인, 회원가입 페이지, CSS, ...)
- 로그인 처리만큼은 JWT가 생성되기 전이기 때문에 예외 처리가 되어 FormLoginFilter 로 접근하게 된다.
2. FormLoginFilter
- 전달 받은 username과 password를 DB 와 비교하여 맞는 경우 JWT를 생성하여 응답에 포함하여 응답해준다.
3. Client
- 클라이언트에서 응답 받은 JWT를 로컬이나 쿠키에 저장한다.
- 그 이후 서버에 요청하는 API에 JWT 토큰을 Header에 포함하여 요청하게 된다.
4. JwtAuthFilter
- 요청한 API 를 헤더에 포함한 JWT 에 대해 유효성 검사 후 허용해준다.
- 허용과 동시에 로그인 정보를 생성해서 컨트롤러로 넘어간다.(UserDetailsImpl)
5. Controller
- 로그인된 사용자의 관심 상품을 조회하여 반환한다.
Author And Source
이 문제에 관하여([Spring] Spring Security - JWT), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@gengar/Spring-Spring-Security-JWT저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)