๋ํ๊ต ์ปค๋ฎค๋ํฐ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ ๋์ค JWT ํํฐ ๊ด๋ จ ํ
์คํธ์์ ๋ฌด์ํ ์๋ฌ๋ฅผ ๋ง์ฃผํ๋ค..

์ ์ฒด ํ ์คํธ ์ฝ๋
package com.Sucat.global.jwt.service;
import com.Sucat.domain.user.model.User;
import com.Sucat.domain.user.repository.UserRepository;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class JwtFilterAuthenticationTest {
@Autowired
MockMvc mockMvc;
@Autowired
UserRepository userRepository;
@Autowired
EntityManager em;
@Autowired
JwtService jwtService;
PasswordEncoder delegatingPasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access.header}")
private String accessHeader;
@Value("${jwt.refresh.header}")
private String refreshHeader;
private static String KEY_USERNAME = "email";
private static String KEY_PASSWORD = "password";
private static String USERNAME = "test@naver.com";
private static String PASSWORD = "123456789";
private static String LOGIN_URL = "/login";
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String BEARER = "Bearer";
private ObjectMapper objectMapper = new ObjectMapper();
private void clear(){
em.flush();
em.clear();
}
@BeforeEach
private void init(){
userRepository.save(User.builder()
.email(USERNAME)
.password(delegatingPasswordEncoder.encode(PASSWORD))
.name("Member1")
.nickname("NickName1")
.build());
clear();
}
private Map getUsernamePasswordMap(String username, String password){
Map<String, String> map = new HashMap<>();
map.put(KEY_USERNAME, username);
map.put(KEY_PASSWORD, password);
return map;
}
private Map getAccessAndRefreshToken() throws Exception {
Map<String, String> map = getUsernamePasswordMap(USERNAME, PASSWORD);
MvcResult result = mockMvc.perform(
post(LOGIN_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(map)))
.andReturn();
String accessToken = result.getResponse().getHeader(accessHeader);
String refreshToken = result.getResponse().getHeader(refreshHeader);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put(accessHeader,accessToken);
tokenMap.put(refreshHeader,refreshToken);
return tokenMap;
}
/**
* AccessToken : ์กด์ฌํ์ง ์์,
* RefreshToken : ์กด์ฌํ์ง ์์
*/
@Test
public void Access_Refresh_๋ชจ๋_์กด์ฌ_X() throws Exception {
//when, then
mockMvc.perform(get(LOGIN_URL+"123"))//login์ด ์๋ ๋ค๋ฅธ ์์์ ์ฃผ์
.andExpect(status().isForbidden());
}
/**
* AccessToken : ์ ํจ,
* RefreshToken : ์กด์ฌํ์ง ์์
*/
@Test
public void AccessToken๋ง_๋ณด๋ด์_์ธ์ฆ() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
//when, then
mockMvc.perform(get(LOGIN_URL+"123").header(accessHeader,BEARER+ accessToken))//login์ด ์๋ ๋ค๋ฅธ ์์์ ์ฃผ์
.andExpectAll(status().isNotFound());
}
/**
* AccessToken : ์ ํจํ์ง ์์,
* RefreshToken : ์กด์ฌํ์ง ์์
*/
@Test
public void ์์ ํจํAccessToken๋ง_๋ณด๋ด์_์ธ์ฆX_์ํ์ฝ๋๋_403() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
//when, then
mockMvc.perform(get(LOGIN_URL+"123").header(accessHeader,accessToken+"1"))//login์ด ์๋ ๋ค๋ฅธ ์์์ ์ฃผ์
.andExpectAll(status().isForbidden()); // ์๋ ์ฃผ์๋ก ๋ณด๋์ผ๋ฏ๋ก NotFound
}
/**
* AccessToken : ์กด์ฌํ์ง ์์
* RefreshToken : ์ ํจ
*/
@Test
public void ์ ํจํRefreshToken๋ง_๋ณด๋ด์_AccessToken_์ฌ๋ฐ๊ธ_200() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
// refreshToken์ด null์ธ์ง ํ์ธ
assertThat(refreshToken).isNotNull();
//when, then
MvcResult result = mockMvc.perform(get("/login123").header(refreshHeader, BEARER + refreshToken))
.andExpect(status().isOk())
.andReturn();
String accessToken = result.getResponse().getHeader(accessHeader);
// accessToken์ด null์ธ์ง ํ์ธ
assertThat(accessToken).isNotNull();
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken).getSubject();
// subject๊ฐ ์์ํ ๊ฐ๊ณผ ์ผ์นํ๋์ง ํ์ธ
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
}
/**
* AccessToken : ์กด์ฌํ์ง ์์
* RefreshToken : ์ ํจํ์ง ์์
*/
@Test
public void ์์ ํจํRefreshToken๋ง_๋ณด๋ด๋ฉด_403() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
mockMvc.perform(get(LOGIN_URL + "123").header(refreshHeader, refreshToken))//Bearer์ ๋ถ์ด์ง ์์
.andExpect(status().isForbidden());
mockMvc.perform(get(LOGIN_URL + "123").header(refreshHeader, BEARER+refreshToken+"1"))//์ ํจํ์ง ์์ ํ ํฐ
.andExpect(status().isForbidden());
}
/**
* AccessToken : ์ ํจ
* RefreshToken : ์ ํจ
*/
@Test
public void ์ ํจํRefreshToken์ด๋_์ ํจํAccessToken_๊ฐ์ด๋ณด๋์๋_AccessToken_์ฌ๋ฐ๊ธ_200() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL+"123")
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken))
.andExpect(status().isOk())
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(responseAccessToken).getSubject();
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
assertThat(responseRefreshToken).isNull();//refreshToken์ ์ฌ๋ฐ๊ธ๋์ง ์์
}
/**
* AccessToken : ์ ํจํ์ง ์์
* RefreshToken : ์ ํจ
*/
@Test
public void ์ ํจํRefreshToken์ด๋_์์ ํจํAccessToken_๊ฐ์ด๋ณด๋์๋_AccessToken_์ฌ๋ฐ๊ธ_200() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL + "123")
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken + 1))
.andExpect(status().isOk())
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(responseAccessToken).getSubject();
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
assertThat(responseRefreshToken).isNull();//refreshToken์ ์ฌ๋ฐ๊ธ๋์ง ์์
}
/**
* AccessToken : ์ ํจ
* RefreshToken : ์ ํจํ์ง ์์
*/
@Test
public void ์์ ํจํRefreshToken์ด๋_์ ํจํAccessToken_๊ฐ์ด๋ณด๋์๋_์ํ์ฝ๋200_ํน์404_RefreshToken์_AccessToken๋ชจ๋_์ฌ๋ฐ๊ธ๋์ง์์() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL + "123")
.header(refreshHeader, BEARER + refreshToken+1)
.header(accessHeader, BEARER + accessToken ))
.andExpect(status().isNotFound())//์๋ ์ฃผ์๋ก ๋ณด๋์ผ๋ฏ๋ก NotFound
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
assertThat(responseAccessToken).isNull();//accessToken์ ์ฌ๋ฐ๊ธ๋์ง ์์
assertThat(responseRefreshToken).isNull();//refreshToken์ ์ฌ๋ฐ๊ธ๋์ง ์์
}
/**
* AccessToken : ์ ํจํ์ง ์์
* RefreshToken : ์ ํจํ์ง ์์
*/
@Test
public void ์์ ํจํRefreshToken์ด๋_์์ ํจํAccessToken_๊ฐ์ด๋ณด๋์๋_403() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL + "123")
.header(refreshHeader, BEARER + refreshToken+1)
.header(accessHeader, BEARER + accessToken+1 ))
.andExpect(status().isForbidden())//์๋ ์ฃผ์๋ก ๋ณด๋์ผ๋ฏ๋ก NotFound
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
assertThat(responseAccessToken).isNull();//accessToken์ ์ฌ๋ฐ๊ธ๋์ง ์์
assertThat(responseRefreshToken).isNull();//refreshToken์ ์ฌ๋ฐ๊ธ๋์ง ์์
}
@Test
public void ๋ก๊ทธ์ธ_์ฃผ์๋ก_๋ณด๋ด๋ฉด_ํํฐ์๋_X() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(post(LOGIN_URL) //get์ธ ๊ฒฝ์ฐ config์์ permitAll์ ํ๊ธฐ์ notFound
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken))
.andExpect(status().isBadRequest())
.andReturn();
}
}
์์ธํ ์ฝ๋๋ ์๋ ๊นํ๋ธ ๋งํฌ ์ฐธ๊ณ !
https://github.com/pp8817/Sucat
์ฐ์ ํ๋ํ๋ ํด๊ฒฐํด๋ณด๊ธฐ๋ก ํ๋ค.
์ ํจํ RefreshToken๊ณผ ์ ํจํ AccessToken์ ๊ฐ์ด ๋ณด๋ธ ๊ฒฝ์ฐ
@Test
public void ์ ํจํRefreshToken์ด๋_์ ํจํAccessToken_๊ฐ์ด๋ณด๋์๋_AccessToken_์ฌ๋ฐ๊ธ_200() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_URL+"123")
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken))
.andExpect(status().isOk())
.andReturn();
String responseAccessToken = result.getResponse().getHeader(accessHeader);
String responseRefreshToken = result.getResponse().getHeader(refreshHeader);
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(responseAccessToken).getSubject();
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
assertThat(responseRefreshToken).isNull();//refreshToken์ ์ฌ๋ฐ๊ธ๋์ง ์์
}
๋ณธ๋ ์๋ํ ๋ชฉ์ ์ ์ ํจํ RefreshToken, ์ ํจํ AccessToken์ด ๊ฐ์ด ๋์ด๊ฐ ๊ฒฝ์ฐ RefreshToken์ ์ ํจ์ฑ ๊ฒ์ฌ ํ AccessToken์ ์ฌ๋ฐ๊ธํ๋๋ก ๊ฐ๋ฐํ๋ค.
๊ทธ๋ฌ๋ ํ
์คํธ ์ฝ๋ ๊ฒฐ๊ณผ ๋ก๊ทธ๋ฅผ ๋ณด๋ฉด

์ด์ฒ๋ผ 403 ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
403(Forbidden) ์๋ฌ?
์๋์ค์ธ ์๋ฒ์ ํด๋ผ์ด์ธํธ์ ์์ฒญ์ด ๋๋ฌํ์ง๋ง, ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์ ์ ๊ทผ์ ๊ฑฐ๋ถํ ๋ ๋ฐ์ํ๋ ์๋ฌ ์ฝ๋์ด๋ค. ์ฆ, ์ฌ์ฉ์์ ๊ถํ ๊ฒ์ฆ ๋จ๊ณ์์ ๋ฌธ์ ๊ฐ ์๊ธด๊ฒ์ด๋ค.
์ฝ๋์ ํ๋ฆ์ ๋ณด๋ฉด '/login123'์ผ๋ก ์์ฒญ์ด ๊ฐ์ ์ฐ์ JWTAuthenticationProcessingFilter๋ก ๋์ด๊ฐ์ JWT ํ ํฐ์ ๋ํ ๊ฒ์ฆ์ ํ๋ค.
JWTAuthenticationProcessingFilter
/**
* OncePerRequestFilter: ๋ชจ๋ ์๋ธ๋ฆฟ ์ปจํ
์ด๋์์ ์์ฒญ ๋์คํจ์น๋น ๋จ์ผ ์คํ์ ๋ณด์ฅํ๋ ๊ฒ์ ๋ชฉํ๋ก ํ๋ ํํฐ ๊ธฐ๋ณธ ํด๋์ค
*/
@RequiredArgsConstructor
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserRepository userRepository;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
private static final String NO_CHECK_URL = "/login";
/**
* 1. RefreshToken์ด ์ค๋ ๊ฒฝ์ฐ -> ์ ํจํ๋ฉด AccessToken ์ฌ๋ฐ๊ธํ, ํํฐ ์งํ X
* 2. RefreshToken์ ์๊ณ AccessToken๋ง ์๋ ๊ฒฝ์ฐ -> ์ ์ ์ ๋ณด ์ ์ฅ ํ ํํฐ ์งํ, RefreshToken ์ฌ๋ฐ๊ธX
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("Received request URI: " + request.getRequestURI());
if (request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response);
return;
}
String refreshToken = jwtService
.extractRefreshToken(request)
.filter(jwtService::isTokenValid)
.orElse(null);
if (refreshToken != null) { // Request ์์ฒญ์ผ๋ก ์จ ์ฌ์ฉ์๊ฐ refreshToken์ ๊ฐ์ง๊ณ ์๋ค๋ฉด AccessToken์ ์ฌ๋ฐ๊ธ
checkRefreshTokenAndReIssueAccessToken(response, refreshToken);
return;
}
checkAccessTokenAndAuthentication(request, response, filterChain);
}
private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
jwtService.extractAccessToken(request).filter(jwtService::isTokenValid).ifPresent(
accessToken -> jwtService.extractEmail(accessToken).ifPresent(
email -> userRepository.findByEmail(email).ifPresent(
user -> saveAuthentication(user)
)
)
);
filterChain.doFilter(request,response);
}
private void saveAuthentication(User users) {
UserDetails user = org.springframework.security.core.userdetails.User.builder()
.username(users.getEmail())
.password(users.getPassword())
.roles(users.getRole().name())
.build();
Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, authoritiesMapper.mapAuthorities(user.getAuthorities()));
SecurityContext context = SecurityContextHolder.createEmptyContext();//5
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
private void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
userRepository.findByRefreshToken(refreshToken).ifPresent(
user -> jwtService.sendAccessToken(response, jwtService.createAccessToken(user.getEmail()))
);
}
}
์ฐ์ ๋ฌด์ํ๊ฒ ๋๋ฒ๊น
์ ์ฐ์ด๋ดค๋ค.

๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ง์ ์ ์ฐพ์๋ค. ๋ถ๋ช
ํ
์คํธ์ฝ๋์์ ์๋ํ ๋ฐ์ ๋ฐ๋ฅด๋ฉด RefreshToken์ด ์ ํจํ ์ํ๋ก ๋์ด๊ฐ๊ฒ ๋๋๋ฐ RefreshToken์ ์ถ์ถํ๋ ๊ณผ์ ์์ null์ด ๋์๋ค.
RefreshToken ์ถ์ถ
@Override
public Optional<String> extractRefreshToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(refreshHeader)).filter(
refreshToken -> refreshToken.startsWith(BEARER)
).map(refreshToken -> refreshToken.replace(BEARER, ""));
}
์ด์ ์ด ์ฝ๋๋ฅผ ๋๋ฒ๊น ํด๋ณด๋ฉด ์ค๋ฅ์ ์์ธ์ ์ฐพ์ ์ ์์๊ฑฐ ๊ฐ๋ค.

์... ์ด์ ๋ต์ 2๊ฐ ์ค ํ๋์ด๋ค.
1. refreshToken -> refreshToken.startsWith(BEARER) ๊ณผ์ ์ ๋ฌธ์ ๊ฐ ์๋ค.
2. request.getHeader(refreshHeader)์์ ๊ฐ์ ๊ฐ์ ธ์ค์ง ๋ชปํ๋ค.
1๋ฒ ๋จผ์ ํ์ธํด๋ณด์.

์.. ๋ณด์ด๋ ๊ฒ์ฒ๋ผ ์์๋ Constants๋ผ๋ ํ์ผ์ ๋ง๋ค์ด ๋ฐ๋ก ๋ณด๊ดํ๋๋ฐ BEARER์์ ์ค๋ฅ๊ฐ ์๋ ๊ฒ์ ํ์ผํ ์ ์๋ค. ๊ฝค๋ ํ๋ฌดํ์ง๋ง ์ฌ์ํ ์ค์ ๋๋ฌธ์ ํฐ ๋ฌธ์ ๊ฐ ์๊ธฐ๋ ๊ฒ์ ์ ์๊ณ ์๊ธฐ ๋๋ฌธ์ ์ผ์ฐ ๋ฐ๊ฒฌํด์ ๋คํ์ด๋ผ๊ณ ์๊ฐํ๋ค.
์ค๋ฅ๋ฅผ ์์ ํ๊ณ ๋ค์ ํ ์คํธ ์ฝ๋๋ฅผ ์คํํด๋ณด์.

๋ฌธ์ ํด๊ฒฐ
์์งํ ๋จ์ํ ๋์ด์ฐ๊ธฐ๋ฅผ ์๋ชปํ ์ฌ์ํ ์ค์์ธ๋ฐ ๊ตณ์ด ๊ธ๋ก ์ ๋ฆฌํด์ผ ๋๋? ๋ผ๋ ์๊ฐ์ด ๋ค์๋ค.
ํ์ง๋ง ๋ชจ๋ ๋ฌธ์ ๋ ์ฌ์ํ ๋ฌธ์ ์์ ์์ํ๋ค๋ ๊ฒ์ ์๊ณ ์๊ธฐ ๋๋ฌธ์ ๊ฒฝ๊ฐ์ฌ์ ๊ฐ์ง์๋ ์๋ฏธ์ ์ด๋ํ ์์คํ ๊ฒฝํ์ด๋ผ๊ณ ์๊ฐํด ๊ธ๋ก ์ ๋ฆฌํด๋ดค๋ค.