티스토리 뷰
해당 예제의 github : https://github.com/digda5624/spring_security_study
인증에 대한 성공을 마쳤으므로 JWT 를 발급해보겠습니다.
저 같은 경우에는 Spring Security Context 객체를 통해서 JWT 를 발급했는데요, Security Context 의 경우 Session Login 의 핵심 역할을 하게 되므로 잘 알아 두어야 한다고 생각합니다.
잠깐 Security Context Holder 의 내용을 살펴 봅시다.
SecurityContextHolder 는 말 그대로 Context 를 저장하는 저장소 개념의 역할을 합니다.
내부에는 Authentication 정보를 포함하고 있다고 생각해보 무방해 보입니다.
Security Context Holder 는 이 인증 객체에 대한 정보를 저장할 수 있는 정책이 여러가지가 존재하게 됩니다.
기본 정책은 Thread Local 입니다.
정책 | 내용 |
MODE_THREADLOCAL | 스레드당 Context 객체를 할당. default |
MODE_INHERITABLETHREADLOCAL | 메인 스레드와 자식 스레드에 관하여 동일한 Context 유지 |
MODE_GLOBAL | Memory 에 단 하나의 Context 만을 가지고 있음 |
예제에서는 기본적으로 스레드 로컬 전략을 사용하고 있고, JWT의 경우에 StateLess 한 성질을 목표로 만들었기 때문에 별도로 SecurityContext 에 인증 객체를 넣을 필요는 없습니다. (Login api 에 한해서)
현재 핵심은 인증 완료된 객체를 가지고 JWT 를 만들어 클라이언트에게 발급하는 것 입니다.
그렇다면 JWT 란 무엇일까요?
JWT 란 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token 입니다.
JWT 는 Header, PayLoad, Signature의 구조로 각 부분은 Base64 로 인코딩 되어 표현 됩니다.
토큰의 헤더는 typ 와 alg 두 가지 정보로 구성이 되며 alg 의 경우 Signature 를 해싱하기 위한 알고리즘을 지정하는 것입니다.
토큰의 페이로드에는 토큰에서 사용할 정보의 조각들인 클레임(Claim) 이 담겨 있습니다. 클레임은 총 3가지로 나누어지며, Json(Key/Value) 형태로 다수의 정보를 넣을 수 있습니다.
1. Registered Claim - 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터
2. Public Claim - 사용자 정의 클레임, 공개용 정보를 위해 사용
3. Private Claim - 사용자 정의 클레임, 서버와 클라이언트 사이에 임의로 지정한 정보를 저장
토큰의 서명은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드입니다. 서명은 위에서 만든 헤더와 페이로드의 값을 각각 BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64로 인코딩하여 생성합니다.
자 이제 LoginSuccess Handler 에서 JWT 를 발급하는 로직을 작성해 봅시다.
손쉬운 JWT 작성을 위해 gradle 에 라이브러리를 추가합니다.
// JWT 의존성 추가
implementation group: 'com.auth0', name: 'java-jwt', version: '3.18.3'
JWT 와 관련된 설정 변수인 secretKey, 만료시간들의 경우에 application.yml 을 통해 따로 뺄 수 있지만 예제의 경우 static 변수를 통해 지정해 주었습니다.
@Slf4j
@Component
@RequiredArgsConstructor
public final class JwtUtils {
private static final String secretKey = "digda's secretKey";
private static final int jwtExpirationInMs = 1800;
public static String createAccessToken(Authentication authentication) {
User user = (User) authentication.getPrincipal();
return accessToken(user);
}
private static String accessToken(User user){
Map<String, Object> payload = createClaims(user);
return JWT.create()
.withSubject("Digda_Test_token")
.withExpiresAt(new Date(System.currentTimeMillis() + jwtExpirationInMs * 1000L))
.withPayload(Map.of("userInfo", payload))
.sign(Algorithm.HMAC512(secretKey));
}
private static Map<String, Object> createClaims(User user){
Map<String, Object> claims = new HashMap<>();
claims.put("id", user.getId());
claims.put("name", user.getName());
claims.put("role", user.getRole().toString());
return claims;
}
}
JWT 발급 유틸 클래스를 작성했으므로 LoginSuccessHandler 를 작성해 보겠습니다.
public class JwtLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
/**
* 들어온 Authentication 기반으로 토큰 설정하기
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
// Authentication 기반으로 토큰생성
System.out.println("request = " + request);
String accessToken = JwtUtils.createAccessToken(authentication);
response.setHeader("Access-Token", accessToken);
response.getWriter().println("login success");
}
}
마지막으로 spring security 에 대한 설정 파일 작성을 통해서 인증에 대한 포스팅을 마쳐보겠습니다.
@Configuration
@EnableWebSecurity
@Import(UserDetailsServiceImpl.class)
public class JwtSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
http.csrf().disable()
// jwt 토큰을 사용하므로 세션 저장소에 Context를 저장할 필요없다.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 스프링 시큐리티에서 제공하는 로그인 폼을 사용하지 않을 것이므로 disable
.formLogin().disable()
.addFilterBefore(jwtAuthenticationFilter(authenticationManager), LogoutFilter.class)
.authenticationManager(authenticationManager)
// 일단 현재에서는 Test를 위해서 모든 요청들에 대해 인가 검사를 하지 않게 적용했다.
.authorizeRequests()
.anyRequest()
.permitAll();
return http.build();
}
/**
* jwtAuthenticationFilter Bean 등록
*/
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(AuthenticationManager authenticationManager){
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(authenticationManager);
filter.setAuthenticationSuccessHandler(new JwtLoginSuccessHandler());
return filter;
}
/**
* AuthenticationManager 등록
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider){
return new ProviderManager(authenticationProvider);
}
@Bean
public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService){
return new JwtAuthenticationProvider(userDetailsService);
}
}
HttpSecurity.build() 를 통해서 스프링 시큐리티의 필터 체인을 빌드하고 이를 빈으로 등록합니다.
추가적으로 스프링 시큐리티 체인의 경우 우선순위가 사용자 정의 필터보다 선순위를 가지게 되므로 주의가 필요합니다.
'개발일지' 카테고리의 다른 글
엑셀 파일 유연하게 읽어보기 (0) | 2022.11.07 |
---|---|
batch insert 와 JPA 의 한계 (2) | 2022.11.04 |
선착순 동시성 문제 해결하기 낙관적 락, 비관적 락 (1) | 2022.11.03 |
다형성과 캡슐화, Spring DI 를 사용해 유연한 정책 변화 만들기 (0) | 2022.11.02 |
[Spring Security + AOP] 로그를 찍어보자 - 로그인 인증 (1) (0) | 2022.11.01 |
- Total
- Today
- Yesterday