์ด์ ์ด์ ์ ์์ฑํ JwtService๋ฅผ ๊ฐ์ง๊ณ ๋ก๊ทธ์ธ ํ JWT๋ฅผ ๋ฐ๊ธํ๋ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์ดํ JWT๋ง์ผ๋ก ์ธ์ฆ์ ํ๋ ์ฝ๋๋ ์์ฑํ๋๋ก ํ๊ฒ ์ต๋๋ค.
- ์ํ๋ฆฌํฐ๋ฅผ ์ด์ฉํ JSON ๋ฐ์ดํฐ๋ก ๋ก๊ทธ์ธ (์๋ฃ)
- JWT๋ฅผ ์ด์ฉํ ์ธ์ฆ (์งํ ์ค)
- ๋๋ฉ์ธ, ํ ์ด๋ธ ์ค๊ณ, ์ํฐํฐ ์์ฑ
- ๋๊ธ ์ญ์ ๋ก์ง ๊ตฌํ
- ํ์๊ฐ์ + ์ ๋ณด์์ ๋ฑ ํ์ ์๋น์ค ๊ตฌํ
- ๊ฒ์ํ ์๋น์ค ๊ตฌํ
- ๋๊ธ ์๋น์ค ๊ตฌํ (1๋๊ธ -> *(๋ฌดํ) ๋๋๊ธ ๊ตฌ์กฐ)
- ์์ธ ์ฒ๋ฆฌ
- ์์ธ ๋ฉ์ธ์ง ๊ตญ์ ํ
- ์นดํ ๊ณ ๋ฆฌ๋ณ ๊ฒ์ํ ๋ถ๋ฅ
- ๊ฒ์๊ธ ํ์ด์ง
- ๋์ ์ธ ๊ฒ์ ์กฐ๊ฑด์ ์ฌ์ฉํ ๊ฒ์
- ์ฌ์ฉ์ ๊ฐ ์ชฝ์ง ๊ธฐ๋ฅ
- ๋ฌดํ ์ชฝ์ง ์คํฌ๋กค
- ๊ฒ์๋ฌผ & ๋๊ธ์ ๋ํ ์๋
- ์ชฝ์ง์ ๋ํ ์๋
- ์ ์ํ ์ฌ์ฉ์ ๊ฐ ์ค์๊ฐ ์ฑํ
- ํ์๊ฐ์ ์ ๊ฒ์ฆ(์: XX๋ํ๊ต XX๊ณผ๊ฐ ์๋๋ฉด ๊ฐ์ ํ ์ ์๊ฒ)
- Swagger๋ฅผ ์ฌ์ฉํ API ๋ฌธ์ ๋ง๋ค๊ธฐ
- ์ ๊ณ & ๋ธ๋๋ฆฌ์คํธ ๊ธฐ๋ฅ
- AOP๋ฅผ ํตํ ๋ก๊ทธ
- ์ด๋๋ฏผ ํ์ด์ง
- ์บ์
- ๋ฐฐํฌ (+ ๋ฌด์ค๋จ ๋ฐฐํฌ)
- ๋ฐฐํฌ ์๋ํ
- ํฌํธ์ ์ด๋ํฐ ์ค๊ณ๋ฅผ ๋ฐ๋ฅด๋ ํจํค์ง ๊ตฌ์กฐ ์ค๊ณํ๊ธฐ
- ...
์์ํ๊ธฐ ์ ์ JwtService๋ฅผ ์กฐ๊ธ ๋ฐ๊พธ๊ณ ๊ฐ๊ฒ ์ต๋๋ค.
์ ๋ ์์ฑํ๊ณ ํฌ์คํ ์ ์ด๋๊ฒ ์๋๋ผ ํฌ์คํ ํ๋ฉฐ ๊ตฌํ์ค์ด๊ธฐ ๋๋ฌธ์ ์ฝ๋์ ๊ณ์ ๋ณ๊ฒฝ์ฌํญ์ด ์๋ ๊ฒ์ ๋ํด ์ํด ๋ถํ๋๋ฆฝ๋๋ค.
JwtService ๋ณ๊ฒฝ
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Optional;
public interface JwtService {
String createAccessToken(String username);
String createRefreshToken();
void updateRefreshToken(String username, String refreshToken);
void destroyRefreshToken(String username);
void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken);
void sendAccessToken(HttpServletResponse response, String accessToken);
Optional<String> extractAccessToken(HttpServletRequest request);
Optional<String> extractRefreshToken(HttpServletRequest request);
Optional<String> extractUsername(String accessToken);
void setAccessTokenHeader(HttpServletResponse response, String accessToken);
void setRefreshTokenHeader(HttpServletResponse response, String refreshToken);
boolean isTokenValid(String token);
}
1. sendToken์ sendAccessAndRefreshToken๊ณผ sendAccessToken์ผ๋ก ๋ณ๊ฒฝํ์์ต๋๋ค.
AccessToken๊ณผ RefreshToken, Username์ Optional๋ก ๋ฐํํ๋๋ก ํ์๊ณ , ์์ธ๋ฅผ ๋ชจ๋ ์ ๊ฑฐํ์์ต๋๋ค.
2. isTokenValid ๊ฐ ์ถ๊ฐ๋์์ต๋๋ค.
์ฌ์ค ์๋ช ์์ ์กฐ๊ธ ๊ณ ๋ฏผํ์ต๋๋ค. jwtService์ธ๋ฐ, isValid๋ง ํด๋ ๋์ง ์์๊น??? ํ๊ณ ๋ง์ด์ฃ ..
๋ง๋๋ก ํ์ธ์! ์ ๋ ์ธ์ ๋ฐ๊ฟ์ง ๋ชจ๋ฆ ๋๋ค!
ํ ํฐ์ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํ ๋ ์ฌ์ฉํฉ๋๋ค.
3. extractRefreshToken ๊ณผ extractAccessToken์ ๋ก์ง์ด ์์ ๋์์ต๋๋ค.
๊ธฐ์กด ๋ก์ง์ Bearer๊ฐ ๋ถ์ด์์ง ์์๋ ์๋ํฉ๋๋ค. ์ด๋ฅผ ๋ฐ๊พธ์์ต๋๋ค.
JwtServiceImpl ๋ณ๊ฒฝ
package boardexample.myboard.global.jwt.service;
import boardexample.myboard.domain.member.repository.MemberRepository;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Transactional
@Service
@RequiredArgsConstructor
@Setter(value = AccessLevel.PRIVATE)
@Slf4j
public class JwtServiceImpl implements JwtService{
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access.expiration}")
private long accessTokenValidityInSeconds;
@Value("${jwt.refresh.expiration}")
private long refreshTokenValidityInSeconds;
@Value("${jwt.access.header}")
private String accessHeader;
@Value("${jwt.refresh.header}")
private String refreshHeader;
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String USERNAME_CLAIM = "username";
private static final String BEARER = "Bearer ";
private final MemberRepository memberRepository;
@Override
public String createAccessToken(String username) {
return JWT.create()
.withSubject(ACCESS_TOKEN_SUBJECT)
.withExpiresAt(new Date(System.currentTimeMillis() + accessTokenValidityInSeconds * 1000))
.withClaim(USERNAME_CLAIM, username)
.sign(Algorithm.HMAC512(secret));
}
@Override
public String createRefreshToken() {
return JWT.create()
.withSubject(REFRESH_TOKEN_SUBJECT)
.withExpiresAt(new Date(System.currentTimeMillis() + refreshTokenValidityInSeconds * 1000))
.sign(Algorithm.HMAC512(secret));
}
@Override
public void updateRefreshToken(String username, String refreshToken) {
memberRepository.findByUsername(username)
.ifPresentOrElse(
member -> member.updateRefreshToken(refreshToken),
() -> new Exception("ํ์์ด ์์ต๋๋ค")
);
}
@Override
public void destroyRefreshToken(String username) {
memberRepository.findByUsername(username)
.ifPresentOrElse(
member -> member.destroyRefreshToken(),
() -> new Exception("ํ์์ด ์์ต๋๋ค")
);
}
@Override
public void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken){
response.setStatus(HttpServletResponse.SC_OK);
setAccessTokenHeader(response, accessToken);
setRefreshTokenHeader(response, refreshToken);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put(ACCESS_TOKEN_SUBJECT, accessToken);
tokenMap.put(REFRESH_TOKEN_SUBJECT, refreshToken);
}
@Override
public void sendAccessToken(HttpServletResponse response, String accessToken){
response.setStatus(HttpServletResponse.SC_OK);
setAccessTokenHeader(response, accessToken);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put(ACCESS_TOKEN_SUBJECT, accessToken);
}
@Override
public Optional<String> extractAccessToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(accessHeader)).filter(
accessToken -> accessToken.startsWith(BEARER)
).map(accessToken -> accessToken.replace(BEARER, ""));
}
@Override
public Optional<String> extractRefreshToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(refreshHeader)).filter(
refreshToken -> refreshToken.startsWith(BEARER)
).map(refreshToken -> refreshToken.replace(BEARER, ""));
}
@Override
public Optional<String> extractUsername(String accessToken) {
try {
return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken).getClaim(USERNAME_CLAIM).asString());
}catch (Exception e){
log.error(e.getMessage());
return Optional.empty();
}
}
@Override
public void setAccessTokenHeader(HttpServletResponse response, String accessToken) {
response.setHeader(accessHeader, accessToken);
}
@Override
public void setRefreshTokenHeader(HttpServletResponse response, String refreshToken) {
response.setHeader(refreshHeader, refreshToken);
}
@Override
public boolean isTokenValid(String token){
try {
JWT.require(Algorithm.HMAC512(secret)).build().verify(token);
return true;
}catch (Exception e){
log.error("์ ํจํ์ง ์์ Token์
๋๋ค", e.getMessage());
return false;
}
}
}
JwtService ํ ์คํธ์ฝ๋ ์ฌ์์ฑ
์ด๋ฅผ ๋ฐํ์ผ๋ก ๋ค์ ์์ฑํ ํ ์คํธ์ฝ๋๋ฅผ ๋ค์ ์์ฑํ๊ฒ ์ต๋๋ค.
1. sendToken -> sendAccessAndRefreshToken ์ผ๋ก ๋ฐ๊ฟ์ค๋๋ค.
2. username, accessToken, refreshToken์ ๋ฐํํ๋ ์ฝ๋๋ ์๋์ ๋น์ทํ๊ฒ ๋ฐ๊ฟ์ฃผ๋ฉด ๋ฉ๋๋ค.
jwtService.extractAccessToken(httpServletRequest);
jwtService.extractAccessToken(httpServletRequest).orElseThrow(()-> new Exception("ํ ํฐ์ด ์์ต๋๋ค"));
3. ์ด์ jwtService์์ json์ผ๋ก Body์ ํ ํฐ์ ๋ฃ์ด์ฃผ์ง ์์ต๋๋ค.
sendAccessAndRefreshToken_ํ ํฐ_์ ์ก์์ ์๋ ์ฝ๋๊ฐ ์ฌ๋ผ์ง๋๋ค.
/*Map<String, String> tokenMap = objectMapper.readValue(mockHttpServletResponse.getContentAsString(), Map.class);
String bodyAccessToken = tokenMap.get(ACCESS_TOKEN_SUBJECT);
String bodyRefreshToken = tokenMap.get(REFRESH_TOKEN_SUBJECT);*/
4. ํ ํฐ์ ์ ํจ์ฑ์ ๊ฒ์ฌํ๋ ํ ์คํธ๊ฐ ์ถ๊ฐ๋์์ต๋๋ค.
@Test
public void ํ ํฐ_์ ํจ์ฑ_๊ฒ์ฌ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
//when, then
assertThat(jwtService.isTokenValid(accessToken)).isTrue();
assertThat(jwtService.isTokenValid(refreshToken)).isTrue();
assertThat(jwtService.isTokenValid(accessToken+"d")).isFalse();
assertThat(jwtService.isTokenValid(accessToken+"d")).isFalse();
}
์ ์ฒด ํ ์คํธ ์ฝ๋
import boardexample.myboard.domain.member.Member;
import boardexample.myboard.domain.member.repository.MemberRepository;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
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.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
class JwtServiceTest {
@Autowired JwtService jwtService;
@Autowired MemberRepository memberRepository;
@Autowired EntityManager em;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access.header}")
private String accessHeader;
@Value("${jwt.refresh.header}")
private String refreshHeader;
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String USERNAME_CLAIM = "username";
private static final String BEARER = "Bearer ";
private String username = "username";
@BeforeEach
public void init(){
Member member = Member.builder().username(username).password("1234567890").name("Member1").nickName("NickName1").age(22).build();
memberRepository.save(member);
clear();
}
private void clear(){
em.flush();
em.clear();
}
@Test
public void createAccessToken_AccessToken_๋ฐ๊ธ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
DecodedJWT verify = JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken);
String subject = verify.getSubject();
String findUsername = verify.getClaim(USERNAME_CLAIM).asString();
assertThat(findUsername).isEqualTo(username);
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
}
@Test
public void createRefreshToken_RefreshToken_๋ฐ๊ธ() throws Exception {
//given
String refreshToken = jwtService.createRefreshToken();
DecodedJWT verify = JWT.require(Algorithm.HMAC512(secret)).build().verify(refreshToken);
String subject = verify.getSubject();
String username = verify.getClaim(USERNAME_CLAIM).asString();
assertThat(subject).isEqualTo(REFRESH_TOKEN_SUBJECT);
assertThat(username).isNull();
}
@Test
public void updateRefreshToken_refreshToken_์
๋ฐ์ดํธ() throws Exception {
//given
String refreshToken = jwtService.createRefreshToken();
jwtService.updateRefreshToken(username, refreshToken);
clear();
Thread.sleep(3000);
//when
String reIssuedRefreshToken = jwtService.createRefreshToken();
jwtService.updateRefreshToken(username, reIssuedRefreshToken);
clear();
//then
assertThrows(Exception.class, () -> memberRepository.findByRefreshToken(refreshToken).get());//
assertThat(memberRepository.findByRefreshToken(reIssuedRefreshToken).get().getUsername()).isEqualTo(username);
}
@Test
public void destroyRefreshToken_refreshToken_์ ๊ฑฐ() throws Exception {
//given
String refreshToken = jwtService.createRefreshToken();
jwtService.updateRefreshToken(username, refreshToken);
clear();
//when
jwtService.destroyRefreshToken(username);
clear();
//then
assertThrows(Exception.class, () -> memberRepository.findByRefreshToken(refreshToken).get());
Member member = memberRepository.findByUsername(username).get();
assertThat(member.getRefreshToken()).isNull();
}
@Test
public void ํ ํฐ_์ ํจ์ฑ_๊ฒ์ฌ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
//when, then
assertThat(jwtService.isTokenValid(accessToken)).isTrue();
assertThat(jwtService.isTokenValid(refreshToken)).isTrue();
assertThat(jwtService.isTokenValid(accessToken+"d")).isFalse();
assertThat(jwtService.isTokenValid(accessToken+"d")).isFalse();
}
@Test
public void setAccessTokenHeader_AccessToken_ํค๋_์ค์ () throws Exception {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
jwtService.setAccessTokenHeader(mockHttpServletResponse, accessToken);
//when
jwtService.sendAccessAndRefreshToken(mockHttpServletResponse,accessToken,refreshToken);
//then
String headerAccessToken = mockHttpServletResponse.getHeader(accessHeader);
assertThat(headerAccessToken).isEqualTo(accessToken);
}
@Test
public void setRefreshTokenHeader_RefreshToken_ํค๋_์ค์ () throws Exception {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
jwtService.setRefreshTokenHeader(mockHttpServletResponse, refreshToken);
//when
jwtService.sendAccessAndRefreshToken(mockHttpServletResponse,accessToken,refreshToken);
//then
String headerRefreshToken = mockHttpServletResponse.getHeader(refreshHeader);
assertThat(headerRefreshToken).isEqualTo(refreshToken);
}
@Test
public void sendAccessAndRefreshToken_ํ ํฐ_์ ์ก() throws Exception {
//given
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
//when
jwtService.sendAccessAndRefreshToken(mockHttpServletResponse,accessToken,refreshToken);
//then
String headerAccessToken = mockHttpServletResponse.getHeader(accessHeader);
String headerRefreshToken = mockHttpServletResponse.getHeader(refreshHeader);
assertThat(headerAccessToken).isEqualTo(accessToken);
assertThat(headerRefreshToken).isEqualTo(refreshToken);
}
@Test
public void extractAccessToken_AccessToken_์ถ์ถ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
HttpServletRequest httpServletRequest = setRequest(accessToken, refreshToken);
//when
String extractAccessToken = jwtService.extractAccessToken(httpServletRequest).orElseThrow(()-> new Exception("ํ ํฐ์ด ์์ต๋๋ค"));
//then
assertThat(extractAccessToken).isEqualTo(accessToken);
assertThat(JWT.require(Algorithm.HMAC512(secret)).build().verify(extractAccessToken).getClaim(USERNAME_CLAIM).asString()).isEqualTo(username);
}
@Test
public void extractRefreshToken_RefreshToken_์ถ์ถ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
HttpServletRequest httpServletRequest = setRequest(accessToken, refreshToken);
//when
String extractRefreshToken = jwtService.extractRefreshToken(httpServletRequest).orElseThrow(()-> new Exception("ํ ํฐ์ด ์์ต๋๋ค"));
//then
assertThat(extractRefreshToken).isEqualTo(refreshToken);
assertThat(JWT.require(Algorithm.HMAC512(secret)).build().verify(extractRefreshToken).getSubject()).isEqualTo(REFRESH_TOKEN_SUBJECT);
}
private HttpServletRequest setRequest(String accessToken, String refreshToken) throws IOException {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
jwtService.sendAccessAndRefreshToken(mockHttpServletResponse,accessToken,refreshToken);
String headerAccessToken = mockHttpServletResponse.getHeader(accessHeader);
String headerRefreshToken = mockHttpServletResponse.getHeader(refreshHeader);
MockHttpServletRequest httpServletRequest = new MockHttpServletRequest();
httpServletRequest.addHeader(accessHeader, BEARER+headerAccessToken);
httpServletRequest.addHeader(refreshHeader, BEARER+headerRefreshToken);
return httpServletRequest;
}
@Test
public void extractUsername_Username_์ถ์ถ() throws Exception {
//given
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
HttpServletRequest httpServletRequest = setRequest(accessToken, refreshToken);
String requestAccessToken = jwtService.extractAccessToken(httpServletRequest).orElseThrow(()->new Exception("ํ ํฐ์ด ์์ต๋๋ค"));
//when
String extractUsername = jwtService.extractUsername(requestAccessToken).orElseThrow(()->new Exception("ํ ํฐ์ด ์์ต๋๋ค"));
//then
assertThat(extractUsername).isEqualTo(username);
}
}
๋ชจ๋ ํ ์คํธ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ํต๊ณผ๋ฉ๋๋ค.
LoginSuccessJWTProvideHandler ์์ - JWT ๋ฐ๊ธ
์ด์ ๋ก๊ทธ์ธ ์ฑ๊ณต ์ JWT๋ฅผ ์ ๊ณตํ๋ ์ฝ๋๋ฅผ ์์ฑํ๊ฒ ์ต๋๋ค.
์ ํฌ๋ LoginSuccessJWTProvideHandler๋ฅผ ํตํด ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ํ์ฒ๋ฆฌ๋ฅผ ํ ์ ์๋๋ก ํด๋์์ต๋๋ค.
์ด์ ์ด๊ณณ์ JWT๋ฅผ ๋ฐ๊ธํ๋ ์ฝ๋๋ฅผ ์ถ๊ฐํ๊ฒ ์ต๋๋ค.
๋ฐ๊ธํ ์ดํ ํ์์๊ฒ RefreshToken์ ์ ์ฅํด์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค
@Slf4j
@RequiredArgsConstructor
public class LoginSuccessJWTProvideHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtService jwtService;
private final MemberRepository memberRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String username = extractUsername(authentication);
String accessToken = jwtService.createAccessToken(username);
String refreshToken = jwtService.createRefreshToken();
jwtService.sendAccessAndRefreshToken(response, accessToken, refreshToken);
memberRepository.findByUsername(username).ifPresent(
member -> member.updateRefreshToken(refreshToken)
);
log.info( "๋ก๊ทธ์ธ์ ์ฑ๊ณตํฉ๋๋ค. username: {}" ,username);
log.info( "AccessToken ์ ๋ฐ๊ธํฉ๋๋ค. AccessToken: {}" ,accessToken);
log.info( "RefreshToken ์ ๋ฐ๊ธํฉ๋๋ค. RefreshToken: {}" ,refreshToken);
}
private String extractUsername(Authentication authentication){
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
}
SecurityConfig ์์
SecurityConfig๋ ์์ ํด์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
(์ถ๊ฐ - WebSecurityConfigurerAdapter๊ฐ Deprecate ๋์์ต๋๋ค. ์๋ ์ฌ์ดํธ์ ์ด๋ป๊ฒ ์์ ํด์ผ ํ๋์ง ๋์์์ต๋๋ค.
https://www.codejava.net/frameworks/spring-boot/fix-websecurityconfigureradapter-deprecated )
memberRepository์ ์ข ์์ฑ์ ์ถ๊ฐํด์ค๋๋ค.
์ถ๊ฐ๋ก MemberRepository์ JwtService๋ฅผ ํ๋์์ ๋ฐ์์ค๋๋ก ์์ ํ๊ฒ ์ต๋๋ค. ์๋๋ ์ ์ฒด ์ฝ๋์ ๋๋ค.
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final LoginService loginService;
private final ObjectMapper objectMapper;
private final MemberRepository memberRepository;
private final JwtService jwtService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().disable()//formLogin ์ธ์ฆ๋ฐฉ๋ฒ ๋นํ์ฑํ
.httpBasic().disable()//httpBasic ์ธ์ฆ๋ฐฉ๋ฒ ๋นํ์ฑํ(ํน์ ๋ฆฌ์์ค์ ์ ๊ทผํ ๋ username๊ณผ password ๋ฌผ์ด๋ด)
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login", "/signUp","/").permitAll()
.anyRequest().authenticated();
http.addFilterAfter(jsonUsernamePasswordLoginFilter(), LogoutFilter.class);
http.addFilterBefore(jwtAuthenticationProcessingFilter(), JsonUsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(loginService);
return new ProviderManager(provider);
}
@Bean
public LoginSuccessJWTProvideHandler loginSuccessJWTProvideHandler(){
return new LoginSuccessJWTProvideHandler(jwtService, memberRepository);//๋ณ๊ฒฝ
}
@Bean
public LoginFailureHandler loginFailureHandler(){
return new LoginFailureHandler();
}
@Bean
public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordLoginFilter(){
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordLoginFilter = new JsonUsernamePasswordAuthenticationFilter(objectMapper);
jsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
jsonUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessJWTProvideHandler());
jsonUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());//๋ณ๊ฒฝ
return jsonUsernamePasswordLoginFilter;
}
@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter(){
JwtAuthenticationProcessingFilter jsonUsernamePasswordLoginFilter = new JwtAuthenticationProcessingFilter(jwtService, memberRepository);
return jsonUsernamePasswordLoginFilter;
}
}
AccessToken์ ํตํ ์ธ์ฆ Filter ์ถ๊ฐ
์ด์ ์ธ์ฆ์ด ํ์ํ ๋ชจ๋ ์์ฒญ์์ AccessToken์ ํตํด ์ธ์ฆ์ ์งํํ๋ ์ฝ๋๋ฅผ ์์ฑํ๊ฒ ์ต๋๋ค.
์ฐ์ ํ์ฌ ์ํฉ์์ ์์ฒญ์ ๋ณด๋ด๋ฉด ์๋ํ๋ ํํฐ๋ค์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์์งํ ๋งํด์ ์ ๋ชจ๋ฅด๊ฒ ์ต๋๋ค. ๋ง์๊ฐ์์๋ ๊ทธ๋ฅ ์ ์ผ ๋จผ์ ์ฝ๋ ๋ฃ๊ณ ์ถ์ด์ ๋งจ ์ฒซ ํํฐ์ธ WebAsyncManagerIntegrationFilter ์ด์ ์ ๋ฃ์ด๋ฒ๋ฆฌ๊ณ ์ถ์ง๋ง.. ๊ทธ๋์ ์ฝ๋๋ฅผ ํ๋ฒ ๋ด๋ณด์์ต๋๋ค.
๋ค ๋ญ์๋ฆฌ์ธ์ง ๋ชจ๋ฅด๊ฒ ์ด์ ํฌ๊ธฐํ์ต๋๋ค.
์ดํ ์ํ๋ฆฌํฐ์ ์ฌ๋ฌ ํํฐ๋ค์ ๊ณต๋ถํ๋ฉฐ, ์ ์ ํ ํํฐ์ ์์น๋ก ์ฎ๊ธฐ๋๋ก ํ๊ณ , ์ฐ์ ์ ์ ํฌ๊ฐ ๋ฑ๋กํ๋ JsonUsernamePasswordAuthenticationFilter ์ด์ ์ ๋ฑ๋กํ๋๋ก ํ๊ฒ ์ต๋๋ค.
์๊ฐํด๋ ์๊ตฌ์ฌํญ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค
์๊ตฌ์ฌํญ
- "/login "์ผ๋ก ์ค๋ ์์ฒญ์ ๋ํด์๋ ํํฐ๋ฅผ ์๋์ํค์ง ์๊ณ , JsonUsernamePasswordAuthenticationFilter์๊ฒ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ๋ฅผ ์์ํฉ๋๋ค.
- ๊ทธ ์ธ์ ๋ค์ด์ค๋ ๋ชจ๋ ์์ฒญ์ ๋ํด์ ์๋ํฉ๋๋ค.
- RefreshToken์ ํฌํจํ์ฌ ์์ฒญ์ด ์ ์ก๋๋ ๊ฒฝ์ฐ๋ ๋ค์ ๋ค ๊ฐ์ง ์ํฉ์ด ์์ ์ ์์ต๋๋ค.
- ๋ ๋ค ์ ํจํ ๊ฒฝ์ฐ -> AccessToken์ฌ๋ฐ๊ธ, ์ธ์ฆ์ ์งํํ์ง ์์.
- RefreshToken์ ์ ํจํ๊ณ , AccessToken์ ์๊ฑฐ๋ ์ ํจํ์ง ์์ ๊ฒฝ์ฐ -> AccessToken ์ฌ๋ฐ๊ธ
- RefreshToken์ ์๊ฑฐ๋ ์ ํจํ์ง ์๊ณ , AccessToken์ ์ ํจํ ๊ฒฝ์ฐ -> ์ธ์ฆ์ ์ฑ๊ณต๋๋, RefreshToken์ ์ฌ๋ฐ๊ธํ์ง๋ ์์
- RefreshToken๊ณผ AccessToken ๋ชจ๋ ์๊ฑฐ๋ ์ ํจํ์ง ์์ ๊ฒฝ์ฐ -> ์ธ์ฆ์ ์คํจํฉ๋๋ค. 403์ ์ ๊ณตํฉ๋๋ค. (์ดํ 200์ ์ ๊ณตํ๋, ์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ์ ์ฝ์ํ ์ฝ๋๋ฅผ ์ถ๊ฐํ์ฌ ์ด๋ค ์๋ฌ์ธ์ง ๋ณดํธํ์ฌ ์ ๋ฌํ๊ฒ ์ต๋๋ค.)
์ฐธ๊ณ
์ ์ 3๋ฒ์ ๋ํด์ ๋งํ๊ณ ๋์ด๊ฐ๊ฒ ์ต๋๋ค.
์ RefreshToken์ ์ฌ๋ฐ๊ธํ์ง ์๋๋๊ณ ๋ฌผ์ด๋ณธ๋ค๋ฉด ์ ์ ์๊ฐ์ ์ด๋ฌํฉ๋๋ค.
RefreshToken์ ๋งค์ฐ ์ค์ํฉ๋๋ค.
RefreshToken์ ํตํด AccessToken์ ์ฌ๋ฐ๊ธ๋ฐ์ ์ ์๊ธฐ์, ์ด๋ฅผ ํ์ทจ๋นํ๋ฉด ๋ณด์์ ํฐ ์ํ์ด ์์ ์ ์์ต๋๋ค.
AccessToken์ ํ์ทจ๋นํ๋๋ผ๋ ์ ํจ๊ธฐ๊ฐ์ด ์งง๊ธฐ๋๋ฌธ์, ํฐ ํผํด๋ ๋ง์ ์ ์๋ค๊ณ ์๊ฐํฉ๋๋ค.
๊ทธ๋ฐ๋ฐ AccessToken์ ํตํด RefreshToken์ ์ฌ๋ฐ๊ธํ๋ค???
์ด๊ฑด ์ข ์๋ ๊ฑฐ ๊ฐ์ต๋๋ค.
AccessToken์ ํ์ทจ๋นํ๋ ์๊ฐ, ๊ณ์ํด์ RefreshToken์ ๋ฐ๊ธ๋ฐ์ ์ ์๊ธฐ์ ๋ณด์์ ํฐ ์ํ์ด ์์๊ฑฐ๋ผ ์๊ฐํ์ฌ ์ ๋ ์ด๋ ๊ฒ ์ค์ ํ์ต๋๋ค.
์ถ๊ฐ๋ก RefreshToken์ ๋ง๋ฃ๋์์ผ๋ AccessToken์ ์์ง ๋ง๋ฃ๋์ง ์์ ๊ฒฝ์ฐ๋ ์๋ค๊ณ ๋ด ๋๋ค.
RefreshToken์ ๋ง๋ฃ์๊ฐ 10๋ถ ์ ์, ์ ํจ์๊ฐ์ด 30๋ถ์ธ AccessToken์ ๋ฐ๊ธ๋ฐ์ ๊ฒฝ์ฐ๋ฅผ ๊ฐ์ ํด๋ณธ๋ค๋ฉด
10๋ถ ํ์๋ RefreshToken์ ๋ง๋ฃ๋๋ AccessToken์ ์ ํจํฉ๋๋ค.
์ด๋ฐ ์ํฉ์์๋ ์ ๋ ์ธ์ฆ์ฒ๋ฆฌ๋ฅผ ์งํํด๋ ๊ด์ฐฎ๋ค๋ ์๊ฐ ํ์ ์ด๋ ๊ฒ ์์๊ฐ์ด ์ค์ ํ์ต๋๋ค.
(์ฐพ์๋ณด๋ ์ค ์ด๋ค ๊ธ์์ AccessToken์ ํตํด RefreshToken์ ์ฌ๋ฐ๊ธํ๋ค๋ ๊ธ์ ๋ณด๊ณ , ์ด๊ฑด ์๋์ง ์๋ ์ถ์ด์ ์จ๋ด๋๋ค.)
์ด์ ๊ตฌํ์ ์์ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
JwtAuthenticationProcessingFilter ์์ฑ
OncePerRequestFilter๋ฅผ ์์๋ฐ์ ์ฌ์ฉํ๊ฒ ์ต๋๋ค.
OncePerRequestFilter์ ์ค๋ช ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๋ชจ๋ ์๋ธ๋ฆฟ ์ปจํ ์ด๋์์ ์์ฒญ ๋์คํจ์น๋น ๋จ์ผ ์คํ์ ๋ณด์ฅํ๋ ๊ฒ์ ๋ชฉํ๋ก ํ๋ ํํฐ ๊ธฐ๋ณธ ํด๋์ค์ ๋๋ค.
+ ์ ๊ฐ ์์ง ํํฐ๋ค์ ์ ๋ชจ๋ฅด๋๊ฒ๋ ์๊ณ , ๊ฐ์๋ฅผ ๋ณด์๋๋ฐ ํด๋น ํํฐ๋ฅผ ์์๋ฐ์ ๊ตฌํํ ๊ฐ์๋ ์์์ต๋๋ค.
import boardexample.myboard.domain.member.Member;
import boardexample.myboard.domain.member.repository.MemberRepository;
import boardexample.myboard.global.jwt.service.JwtService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final MemberRepository memberRepository;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();//5
private final String NO_CHECK_URL = "/login";//1
/**
* 1. ๋ฆฌํ๋ ์ ํ ํฐ์ด ์ค๋ ๊ฒฝ์ฐ -> ์ ํจํ๋ฉด AccessToken ์ฌ๋ฐ๊ธํ, ํํฐ ์งํ X, ๋ฐ๋ก ํ๊ธฐ๊ธฐ
*
* 2. ๋ฆฌํ๋ ์ ํ ํฐ์ ์๊ณ AccessToken๋ง ์๋ ๊ฒฝ์ฐ -> ์ ์ ์ ๋ณด ์ ์ฅํ ํํฐ ๊ณ์ ์งํ
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response);
return;//์ํด์ฃผ๋ฉด ์๋๋ก ๋ด๋ ค๊ฐ์ ๊ณ์ ํํฐ๋ฅผ ์งํํด๋ฒ๋ฆผ
}
String refreshToken = jwtService
.extractRefreshToken(request)
.filter(jwtService::isTokenValid)
.orElse(null); //2
if(refreshToken != null){
checkRefreshTokenAndReIssueAccessToken(response, refreshToken);//3
return;
}
checkAccessTokenAndAuthentication(request, response, filterChain);//4
}
private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
jwtService.extractAccessToken(request).filter(jwtService::isTokenValid).ifPresent(
accessToken -> jwtService.extractUsername(accessToken).ifPresent(
username -> memberRepository.findByUsername(username).ifPresent(
member -> saveAuthentication(member)
)
)
);
filterChain.doFilter(request,response);
}
private void saveAuthentication(Member member) {
UserDetails user = User.builder()
.username(member.getUsername())
.password(member.getPassword())
.roles(member.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) {
memberRepository.findByRefreshToken(refreshToken).ifPresent(
member -> jwtService.sendAccessToken(response, jwtService.createAccessToken(member.getUsername()))
);
}
}
1. "/login" ์ผ๋ก ๋ค์ด์ค๋ ์์ฒญ์ ๋ํด์๋ ์๋ํ์ง ์์ต๋๋ค.
2. RefreshToken์ด ์๊ฑฐ๋ ์ ํจํ์ง ์๋ค๋ฉด null์ ๋ฐํํฉ๋๋ค.
3. refreshToken์ด ์ ํจํ๋ค๋ฉด ํด๋น refreshToken์ ๊ฐ์ง ์ ์ ์ ๋ณด๋ฅผ ์ฐพ์์ค๊ณ , ์กด์ฌํ๋ค๋ฉด AccessToken์ ์ฌ๋ฐ๊ธํฉ๋๋ค.
์ด๋ ๋ฐ๋ก return์ํค๋๋ฐ, ๊ทธ ์ด์ ๋ refreshToken๋ง ๋ณด๋ธ ๊ฒฝ์ฐ์๋ ์ธ์ฆ์ ์ฒ๋ฆฌํ์ง ์๊ฒ ํ๊ธฐ ์ํด์์ ๋๋ค.
4. refreshToken์ด ์๋ค๋ฉด AccessToken์ ๊ฒ์ฌํ๋ ๋ก์ง์ ์ํํฉ๋๋ค.
request์์ AccessToken์ ์ถ์ถํ ํ, ์๋ค๋ฉด ํด๋น AccessToken์์ username์ ์ถ์ถํฉ๋๋ค.
username์ด ์ถ์ถ๋์๋ค๋ฉด ํด๋น ํ์์ ์ฐพ์์์ ๊ทธ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์ธ์ฆ์ฒ๋ฆฌ๋ฅผ ํฉ๋๋ค.
์ด๋ SecurityContextHolder์ Authentication ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ๋ฐํํ๋๋ฐ, NullAuthoritiesMapper๊ฐ ์ฐ์ ๋๋ค.
์ด๋ ์ ๊ฐ ๋ง๋ค์ด ์ค ๊ฒ์ด ์๋ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ ๊ณตํด์ฃผ๋ ๊ฒ์ ๋๋ค.
AbstractUserDetailsAuthenticationProvider์ ๋ฐฉ์์ ๋ณด๊ณ ๋ฐ๋ผํด ๋ณด์์ต๋๋ค.
ํ ์คํธ์ฝ๋ ์์ฑ
๊ธฐ๋ณธ ์ธํ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class JwtFilterAuthenticationTest {
@Autowired
MockMvc mockMvc;
@Autowired
MemberRepository memberRepository;
@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 = "username";
private static String KEY_PASSWORD = "password";
private static String USERNAME = "username";
private static String PASSWORD = "123456789";
private static String LOGIN_RUL = "/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(){
memberRepository.save(Member.builder().username(USERNAME).password(delegatingPasswordEncoder.encode(PASSWORD)).name("Member1").nickName("NickName1").role(Role.USER).age(22).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_RUL)
.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 ๋ชจ๋ ์กด์ฌํ์ง ์์ ๊ฒฝ์ฐ
/**
* AccessToken : ์กด์ฌํ์ง ์์,
* RefreshToken : ์กด์ฌํ์ง ์์
*/
@Test
public void Access_Refresh_๋ชจ๋_์กด์ฌ_X() throws Exception {
//when, then
mockMvc.perform(get(LOGIN_RUL+"123"))//login์ด ์๋ ๋ค๋ฅธ ์์์ ์ฃผ์
.andExpect(status().isForbidden());
}
AccessToken๋ง ์กด์ฌํ๋ ๊ฒฝ์ฐ -> ์ ํจํ ํ ํฐ์ผ ๋
/**
* AccessToken : ์ ํจ,
* RefreshToken : ์กด์ฌํ์ง ์์
*/
@Test
public void AccessToken๋ง_๋ณด๋ด์_์ธ์ฆ() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String accessToken= (String) accessAndRefreshToken.get(accessHeader);
//when, then
mockMvc.perform(get(LOGIN_RUL+"123").header(accessHeader,BEARER+ accessToken))//login์ด ์๋ ๋ค๋ฅธ ์์์ ์ฃผ์
.andExpectAll(status().isNotFound());//์๋ ์ฃผ์๋ก ๋ณด๋์ผ๋ฏ๋ก NotFound
}
AccessToken๋ง ์กด์ฌํ๋ ๊ฒฝ์ฐ -> ์ ํจํ์ง ์์ ํ ํฐ์ผ ๋
/**
* 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_RUL+"123").header(accessHeader,accessToken+"1"))//login์ด ์๋ ๋ค๋ฅธ ์์์ ์ฃผ์
.andExpectAll(status().isForbidden());//์๋ ์ฃผ์๋ก ๋ณด๋์ผ๋ฏ๋ก NotFound
}
RefreshToken๋ง ์กด์ฌํ๋ ๊ฒฝ์ฐ -> ์ ํจํ ํ ํฐ์ผ ๋
/**
* AccessToken : ์กด์ฌํ์ง ์์
* RefreshToken : ์ ํจ
*/
@Test
public void ์ ํจํRefreshToken๋ง_๋ณด๋ด์_AccessToken_์ฌ๋ฐ๊ธ_200() throws Exception {
//given
Map accessAndRefreshToken = getAccessAndRefreshToken();
String refreshToken= (String) accessAndRefreshToken.get(refreshHeader);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_RUL + "123").header(refreshHeader, BEARER+refreshToken))//login์ด ์๋ ๋ค๋ฅธ ์์์ ์ฃผ์
.andExpect(status().isOk()).andReturn();
String accessToken = result.getResponse().getHeader(accessHeader);
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken).getSubject();
assertThat(subject).isEqualTo(ACCESS_TOKEN_SUBJECT);
}
RefreshToken๋ง ์กด์ฌํ๋ ๊ฒฝ์ฐ -> ์ ํจํ์ง ์์ ๊ฒฝ์ฐ
/**
* 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_RUL + "123").header(refreshHeader, refreshToken))//Bearer์ ๋ถ์ด์ง ์์
.andExpect(status().isForbidden());
mockMvc.perform(get(LOGIN_RUL + "123").header(refreshHeader, BEARER+refreshToken+"1"))//์ ํจํ์ง ์์ ํ ํฐ
.andExpect(status().isForbidden());
}
AccessToken, RefreshToken ๋ชจ๋ ์ ํจํ ๊ฒฝ์ฐ - AccessToken ์ฌ๋ฐ๊ธ
/**
* 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_RUL + "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์ ์ ํจํ์ง ์์ ๊ฒฝ์ฐ - AccessToken ์ฌ๋ฐ๊ธ
/**
* 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_RUL + "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์ ์ ํจํ์ง ์์ ๊ฒฝ์ฐ - ์ธ์ฆ์ ๋๋ ์๋ฌด๊ฒ๋ ์ฌ๋ฐ๊ธ๋์ง ์์
/**
* 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_RUL + "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 ๋ชจ๋ ์ ํจํ์ง ์์ ๊ฒฝ์ฐ
/**
* 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_RUL + "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์ ์ฌ๋ฐ๊ธ๋์ง ์์
}
๋ก๊ทธ์ธ ์ฃผ์๋ก ๋ณด๋ด๋ฉด ํํฐ ์๋ X
@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_RUL) //get์ธ ๊ฒฝ์ฐ config์์ permitAll์ ํ๊ธฐ์ notFound
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken))
.andExpect(status().isOk())
.andReturn();
}
์ ์ฒด ํ ์คํธ์ฝ๋
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
MemberRepository memberRepository;
@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 = "username";
private static String KEY_PASSWORD = "password";
private static String USERNAME = "username";
private static String PASSWORD = "123456789";
private static String LOGIN_RUL = "/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(){
memberRepository.save(Member.builder().username(USERNAME).password(delegatingPasswordEncoder.encode(PASSWORD)).name("Member1").nickName("NickName1").role(Role.USER).age(22).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_RUL)
.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_RUL+"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_RUL+"123").header(accessHeader,BEARER+ accessToken))//login์ด ์๋ ๋ค๋ฅธ ์์์ ์ฃผ์
.andExpectAll(status().isNotFound());//์๋ ์ฃผ์๋ก ๋ณด๋์ผ๋ฏ๋ก NotFound
}
/**
* 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_RUL+"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);
//when, then
MvcResult result = mockMvc.perform(get(LOGIN_RUL + "123").header(refreshHeader, BEARER+refreshToken))//login์ด ์๋ ๋ค๋ฅธ ์์์ ์ฃผ์
.andExpect(status().isOk()).andReturn();
String accessToken = result.getResponse().getHeader(accessHeader);
String subject = JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken).getSubject();
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_RUL + "123").header(refreshHeader, refreshToken))//Bearer์ ๋ถ์ด์ง ์์
.andExpect(status().isForbidden());
mockMvc.perform(get(LOGIN_RUL + "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_RUL + "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_RUL + "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_RUL + "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_RUL + "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_RUL) //get์ธ ๊ฒฝ์ฐ config์์ permitAll์ ํ๊ธฐ์ notFound
.header(refreshHeader, BEARER + refreshToken)
.header(accessHeader, BEARER + accessToken))
.andExpect(status().isBadRequest())
.andReturn();
}
}
์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
https://github.com/ShinDongHun1/SpringBoot-Board-API