Kakao OAuth2 + JWT + Redis๋ฅผ ํตํ ์ธ์ฆ ๊ณผ์ ๊ตฌํ (2) - JWT ์ ์ฉ
by rlaehddnd0422์ง๋ ํฌ์คํ ์์ ์นด์นด์ค ๋ก๊ทธ์ธ๊ณผ ๊ทธ ํ์ฒ๋ฆฌ ๊ณผ์ ๋ค์ ๊ตฌํํ๋ฉด์ loadUser() ๋ฉ์๋์์
- ์ต์ด ๋ก๊ทธ์ธ์ ๊ฒฝ์ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ์์ ์ ์ฅํ๊ณ , Details ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด Authentication ๊ฐ์ฒด์ ๋ด๊ณ , SecurityContext์ Authentication ๊ฐ์ฒด๋ฅผ ๋ณด๊ดํ๋๋ก ์ค์ ํ์๊ณ
- ์ต์ด ๋ก๊ทธ์ธ์ด ์๋ ๊ฒฝ์ฐ์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํ์์ ๊ฐ์ ธ์ Details ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด Authentication ๊ฐ์ฒด์ ๋ด๊ณ , SecurityContext์ Authentication ๊ฐ์ฒด๋ฅผ ๋ณด๊ดํ๋๋ก ์ค์ ํ์์ต๋๋ค.
์ด๋ฒ ํฌ์คํ ์์๋ JWT๋ฅผ ํ๋ก์ ํธ์ ์ด๋ป๊ฒ ์ ์ฉํ๋์ง ์ฝ๋๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
* ์ด๋ฒ ํฌ์คํ ์์๋ ๋จ์ JWT ์ ์ฉ์ ๋ํด์ ์์๋ณด๊ณ , accessToken ๋ง๋ฃ์ ๋ํ ์ฒ๋ฆฌ ๊ณผ์ ์ ๋ค์ ํฌ์คํ ์ ๊ธฐ๋กํ๊ฒ ์ต๋๋ค!
JWT๊ฐ ์ด๋ค ์ธ์ฆ ๋ฐฉ์์ธ์ง๋ ์๋ ํฌ์คํ ์ ์ฐธ๊ณ ํด์ฃผ์ธ์.
0. ์ค์ ์ ๋ณด
application.yml
jwt:
secret_key: ${jwt.secret_key}
access-token-validity-in-seconds: 30000 # (๋ฐฐํฌ ์ 30๋ถ -> 1800 ์ค์ )
refresh-token-validity-in-seconds: 86400 # (๋ฐฐํฌ ์ 1์ผ -> 86400์ค์ )
์๊ทธ๋์ฒ์ ์ฌ์ฉํ ์ํฌ๋ฆฟ ํค์, ์ก์ธ์ค ํ ํฐ ๋ง๋ฃ์๊ฐ, ๋ฆฌํ๋ ์ ํ ํฐ ๋ง๋ฃ ์๊ฐ์ yml์ ์ง์ ํด์ฃผ์์ต๋๋ค. ์ํฌ๋ฆฟ ํค ๊ฐ์ ์ค์ํ ์ ๋ณด์ด๋๋งํผ github๋ก ํ์๊ด๋ฆฌํ์ง ์๊ณ , ๋ณ๋๋ก ๋ค๋ฅธ ํ์ผ์ ์ค์ ํด์ฃผ๊ณ ํด๋น ํ์ผ์ gitignore์ ๋ฑ๋กํด์ฃผ์์ต๋๋ค.
build.gradle์ ์์กด์ฑ ์ถ๊ฐ
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
1. TokenProvider - ์ฌ์ฉ์ ์ ๋ณด๋ก JWT ํ ํฐ ์์ฑ
@Component
public class TokenProvider {
private static final String AUTH_KEY = "AUTHORITY";
private static final String AUTH_EMAIL = "EMAIL";
private final String secretKey;
private final long accessTokenValidityMilliSeconds;
private final long refreshTokenValidityMilliSeconds;
private Key secretkey;
public TokenProvider(@Value("${jwt.secret_key}") String secretKey,
@Value("${jwt.access-token-validity-in-seconds}") long accessTokenValiditySeconds,
@Value("${jwt.refresh-token-validity-in-seconds}") long refreshTokenValiditySeconds) {
this.secretKey = secretKey;
this.accessTokenValidityMilliSeconds = accessTokenValiditySeconds * 1000;
this.refreshTokenValidityMilliSeconds = refreshTokenValiditySeconds * 1000;
}
@PostConstruct
public void initKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.secretkey = Keys.hmacShaKeyFor(keyBytes);
}
...
}
๊ฐ ํ ํฐ์ ๋ํ ๋ง๋ฃ์๊ฐ๊ณผ secreyKey ๊ฐ์ yml ์ค์ ์ ๋ณด๋ฅผ ํ ๋๋ก ์์ฑ์ ์ฃผ์ ํ๊ณ , ์์ฑ์ ์ฃผ์ ํ JWT ์์ฑ์ ์ฌ์ฉ๋ Key๋ initKey() ๋ฉ์๋๋ก secretKey๋ฅผ decodeํ์ฌ Key์ ์ฃผ์ ํด์ฃผ์์ต๋๋ค.
// access, refresh Token ์์ฑ
public TokenDto createToken(String email, String role) {
long now = (new Date()).getTime();
Date accessValidity = new Date(now + this.accessTokenValidityMilliSeconds);
Date refreshValidity = new Date(now + this.refreshTokenValidityMilliSeconds);
String accessToken = Jwts.builder()
.addClaims(Map.of(AUTH_EMAIL, email))
.addClaims(Map.of(AUTH_KEY, role))
.signWith(secretkey, SignatureAlgorithm.HS256)
.setExpiration(accessValidity)
.compact();
String refreshToken = Jwts.builder()
.addClaims(Map.of(AUTH_EMAIL, email))
.addClaims(Map.of(AUTH_KEY, role))
.signWith(secretkey, SignatureAlgorithm.HS256)
.setExpiration(refreshValidity)
.compact();
return TokenDto.of(accessToken, refreshToken);
}
// token์ด ์ ํจํ ์ง ๊ฒ์ฌ
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
return false;
} catch (UnsupportedJwtException e) {
return false;
} catch (IllegalArgumentException e) {
return false;
}
}
// token์ด ๋ง๋ฃ๋์๋์ง ๊ฒ์ฌ
public boolean validateExpire(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
return false;
}
}
// token์ผ๋ก๋ถํฐ Authentication ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ๋ฆฌํดํ๋ ๋ฉ์๋
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
List<String> authorities = Arrays.asList(claims.get(AUTH_KEY)
.toString()
.split(","));
List<? extends GrantedAuthority> simpleGrantedAuthorities = authorities.stream()
.map(auth -> new SimpleGrantedAuthority(auth))
.collect(Collectors.toList());
KakaoMemberDetails principal = new KakaoMemberDetails(
(String) claims.get(AUTH_EMAIL),
simpleGrantedAuthorities, Map.of());
return new UsernamePasswordAuthenticationToken(principal, token, simpleGrantedAuthorities);
}
}
- TokenProvider์์ ํ ํฐ ์์ฑ ๋ฟ๋ง ์๋๋ผ ํ ํฐ ๊ฒ์ฆ ๊ด๋ จ ๋ฉ์๋์, ํ ํฐ์ผ๋ก๋ถํฐ Authentication ๊ฐ์ฒด๋ฅผ ๋ฆฌํดํ๋ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐ ๊ตฌํํ์ต๋๋ค.
TokenProvider๋ผ๊ธฐ์๋ ํ ํฐ ์์ฑ ์ธ์ ๊ธฐ๋ฅ๋ค๋ ์์ด ์ถํ ํด๋์ค ๋ถ๋ฆฌ๋ฅผ ํ๋์ง, ๋ฆฌ๋ค์ด๋ฐ ํ๋์ง ์์ ํ๋๋ก ํ๊ฒ ์ต๋๋ค !
createToken()
public TokenDto createToken(String email, String role) {
long now = (new Date()).getTime();
Date accessValidity = new Date(now + this.accessTokenValidityMilliSeconds);
Date refreshValidity = new Date(now + this.refreshTokenValidityMilliSeconds);
String accessToken = Jwts.builder()
.addClaims(Map.of(AUTH_EMAIL, email))
.addClaims(Map.of(AUTH_KEY, role))
.signWith(secretkey, SignatureAlgorithm.HS256)
.setExpiration(accessValidity)
.compact();
String refreshToken = Jwts.builder()
.addClaims(Map.of(AUTH_EMAIL, email))
.addClaims(Map.of(AUTH_KEY, role))
.signWith(secretkey, SignatureAlgorithm.HS256)
.setExpiration(refreshValidity)
.compact();
return TokenDto.of(accessToken, refreshToken);
}
- createToekn ๋ฉ์๋์์ email๊ณผ role๊ณผ ์์์ ์ฃผ์ ํ ์ํฌ๋ฆฟ ํค์ ํจ๊ป accessToken๊ณผ refreshToken์ ๋ง๋ค ์ ์์ต๋๋ค.
- ์ด ์ธ์ ๋ฉ์๋๋ ์ด๋ ต์ง ์์ผ๋ฏ๋ก ๋ฐ๋ก ์ค๋ช ํ์ง๋ ์๊ฒ ์ต๋๋ค !
์ด์ ํ์ฒ๋ฆฌ ์๋น์ค๋ก ๋ก๊ทธ์ธ ์ฒ๋ฆฌ๊ฐ ๋๋ ํ ์ด ํด๋์ค๋ฅผ ํตํด Token์ ์ฌ์ฉ์์๊ฒ ๋ฐ๊ธํ๋๋ก ๊ตฌํํด๋ณด๊ฒ ์ต๋๋ค.
2. OAuth2SuccessHandler
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final String REDIRECT_URI = "http://localhost:8080/api/sign/login/kakao?accessToken=%s&refreshToken=%s";
private final TokenProvider tokenProvider;
private final MemberRepository memberRepository;
@Transactional
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
KakaoUserInfo kakaoUserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
Member member = memberRepository.findByEmail(kakaoUserInfo.getEmail())
.orElseThrow(MemberNotFoundException::new);
TokenDto tokenDto = tokenProvider.createToken(member.getEmail(), member.getRole().name());
String redirectURI = String.format(REDIRECT_URI, tokenDto.getAccessToken(), tokenDto.getRefreshToken());
getRedirectStrategy().sendRedirect(request, response, redirectURI);
}
- SimpleUrlAuthenticationSuccessHandler๋ฅผ ์์ํ์ฌ onAuthenticaitonSuccess() ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํ๋ฉด Authentication ๊ฐ์ฒด๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋๋ฐ์.
- ํ์ฒ๋ฆฌ ์๋น์ค์ธ KakaoMemberDetailsService์์ ์ํ๋ฆฌํฐ ์ปจํ ์คํธ์ Authentication ๊ฐ์ฒด๋ฅผ ์ ์ฅํ ๋๋ถ์ ์ด๋ก๋ถํฐ ์ ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊บผ๋ด์ token ์ ๋ณด๋ฅผ ์์ฑํ๊ณ ์ด ํ ํฐ ์ ๋ณด๋ฅผ ํ๋ผ๋ฏธํฐ์ ๋ด์ redirect URI๋ก ๋ณด๋ด๋๋ก ์ฒ๋ฆฌํ์ต๋๋ค. ์ด๋ ๊ฒ ๋ฑ๋กํ ํธ๋ค๋ฌ๋ SecurityConfig์ ์ถ๊ฐ์ ์ผ๋ก ๋ฑ๋กํด์ฃผ๋ฉด ๋๊ฒ ์ต๋๋ค !
SecurityConfig์ ๋ฑ๋ก
.oauth2Login(oAuth2Login -> {
oAuth2Login.userInfoEndpoint(userInfoEndpointConfig ->
userInfoEndpointConfig.userService(kakaoMemberDetailsService)); // 1
oAuth2Login.successHandler(oAuth2SuccessHandler); // 2
});
** ์นด์นด์ค ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ ๊ฒฝ์ฐ 1๋ฒ์ด ๋จผ์ ์คํ๋๊ณ , ๊ทธ ํ 2๋ฒ์ด ์คํ๋ฉ๋๋ค.
์ onAuthenticationSuccess() ๋ฉ์๋์์ ๋ฆฌ๋ค์ด๋ ํธ๋ URI๋ ์๋ ์ปจํธ๋กค๋ฌ์ ๋งคํ๋์ด Json์ผ๋ก ํด๋ผ์ด์ธํธ์๊ฒ ๋๊ฒจ์ค๋๋ค.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/sign")
public class SignController {
@GetMapping("/login/kakao")
public ResponseEntity loginKakao(@RequestParam(name = "accessToken") String accessToken,
@RequestParam(name = "refreshToken") String refreshToken) {
return new ResponseEntity(TokenDto.of(accessToken, refreshToken), HttpStatus.OK);
}
...
ํ ํฐ ๋ฐ๊ธ์ ๋ชจ๋ ๋๋ฌ์ต๋๋ค. ๋ง์ง๋ง์ผ๋ก ์ฌ์ฉ์๋ก๋ถํฐ ์์ฒญ์ด ์์ ๋, ์ฌ์ฉ์๊ฐ ํค๋์ ์ฒจ๋ถํ ํ ํฐ๋ค์ด ์ ํจํ์ง ๊ฒ์ฆํ๋ ํํฐ์ ํํฐ์ ํต๊ณผํ์ง ๋ชปํ์ ๋ ์ฒ๋ฆฌํ handler๋ฅผ ๋ฑ๋กํด์ฃผ๋ฉด ๋๊ฒ ์ต๋๋ค.
3. JWT Filter
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private static final String ACCESS_HEADER = "AccessToken";
private static final String REFRESH_HEADER = "RefreshToken";
private final TokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// #1
if (isRequestPassURI(request, response, filterChain)) {
return;
}
String accessToken = getTokenFromHeader(request, ACCESS_HEADER);
if (tokenProvider.validate(accessToken) && tokenProvider.validateExpire(accessToken)) {
SecurityContextHolder.getContext().setAuthentication(tokenProvider.getAuthentication(accessToken));
}
filterChain.doFilter(request, response);
}
...
}
- ๋ง์ฝ ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ์ด ํ์ํ์ง ์์ ์์ฒญ URI๋ผ๋ฉด 1๋ฒ ๋ก์ง์ด ์คํ๋์ด ํํฐ๋ฅผ ํต๊ณผํฉ๋๋ค.
- ์ ํจ์ฑ ๊ฒ์ฆ์ด ํ์ํ์ง ์์ ์์ฒญ์ด๋ผ ํจ์ ๋ก๊ทธ์ธ api, ์์ธ์ฒ๋ฆฌ end point api ๋ฑ๋ฑ
- ๋ง์ฝ ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ์ด ํ์ํ ์์ฒญ์ด๋ผ๋ฉด getTokenFromHeader๋ฅผ ํตํด ํค๋์์ accessToken์ ๊ฐ์ ธ์ค๊ณ tokenProvider์์ ๊ฒ์ฆ์ ์์ํ์ฌ ํต๊ณผํ๋ฉด, ํ ํฐ์ผ๋ก๋ถํฐ authentication ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ์ํ๋ฆฌํฐ ์ปจํ ์คํธ์ ๋ณด๊ดํ๊ณ ํํฐ์ ํต๊ณผํฉ๋๋ค.
- ๊ฒ์ฆ์ ์คํจํ ๊ฒฝ์ฐ์ ๋ํ ์ฒ๋ฆฌ๋ ์๋์์ ๋ณ๋๋ก ์ฒ๋ฆฌ
- ์ด ํํฐ ๋ํ SecurityConfig์ ๋ฑ๋กํด์ฃผ๋ฉด ๋๊ฒ ์ต๋๋ค.
SecurityConfig์ ๋ฑ๋ก
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
- ๋ณ๋์ ์์ฒด ์ธ์ฆ ๋ก์ง์ ๊ตฌํํ JwtFilter๋ฅผ ์์ ๊ฐ์ด ๋ฑ๋กํ์ฌ ๊ธฐ์กด ์ํ๋ฆฌํฐ ๋ก์ง์ UsernamePasswordAuthenticationFilter๋ฅผ ๋์ฒดํด์ค์๋ค.
4. JWT Handler
๋ง์ง๋ง์ผ๋ก JWT Filter๋ฅผ ํต๊ณผํ์ง ๋ชปํ ๊ฒฝ์ฐ์ ๋ํ์ฌ ์ฒ๋ฆฌํด์ฃผ์ด์ผ ํฉ๋๋ค. ํต๊ณผ ํ์ง ๋ชปํ ๊ฒฝ์ฐ๋ผ ํจ์ ์ธ์ฆ์ ํต๊ณผ๋์ง ๋ชปํ ๊ฒฝ์ฐ, ๋๋ ์ธ๊ฐ(๊ถํ)์ ํต๊ณผํ์ง ๋ชปํ ๊ฒฝ์ฐ ๋ ๊ฐ์ง๊ฐ ์๊ฒ ์ต๋๋ค.
1) ์ธ์ฆ ์คํจ
@Component
public class JwtAuthenticationFailEntryPoint implements AuthenticationEntryPoint {
private static final String EXCEPTION_ENTRY_POINT = "/api/exception/entry-point";
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendRedirect(EXCEPTION_ENTRY_POINT);
}
}
- ์ธ์ฆ์ ๋ํ ์คํจ ์ฒ๋ฆฌ ํธ๋ค๋ฌ๋ AuthenticationEntryPoint ์ธํฐํ์ด์ค์ commence ๋ฉ์๋๋ฅผ ๊ตฌํํ๋ฉด ๋๊ฒ ์ต๋๋ค.
2) ์ธ๊ฐ ์คํจ (๊ถํ ๋ฏธ๋ฌ)
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private static final String EXCEPTION_ACCESS_HANDLER = "/api/exception/access-denied";
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.sendRedirect(EXCEPTION_ACCESS_HANDLER);
}
}
- ์ธ๊ฐ์ ๋ํ ์คํจ ์ฒ๋ฆฌ ํธ๋ค๋ฌ๋ AccessDeniedHandler ์ธํฐํ์ด์ค์ handler ๋ฉ์๋๋ฅผ ๊ตฌํํ๋ฉด ๋๊ฒ ์ต๋๋ค.
์ ๋ ์ธ์ฆ/์ธ๊ฐ์ ์คํจํ ๊ฒฝ์ฐ, ์๋ ์ปจํธ๋กค๋ฌ์์ ํด๋น URI์ ๋ฐ์ ์์ธ๋ฅผ ๋์ง๋๋ก ์ฒ๋ฆฌํ์๊ณ , ์์ธ๋ @ExceptionAdvisor์์ ๋ฐ์์ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ตํ๋๋ก ์ค์ ํด์ฃผ์์ต๋๋ค.
@RestController
@RequestMapping("/api/exception")
public class ExceptionController {
@GetMapping("/access-denied")
public void accessDeniedException() {
throw new AccessDeniedException();
}
@GetMapping("/entry-point")
public void authenticateException() {
throw new AuthenticationEntryPointException();
}
}
** ์ด API์ ๋ํ ์ ๊ทผ ๋ํ Jwtํํฐ์์ ์ถ๊ฐ์ ์ผ๋ก isRequestPassURI์ ๋ฑ๋กํด์ค์๋ค !
@RestControllerAdvice
public class ExceptionAdvisor {
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseEntity accessDeniedException(AccessDeniedException e) {
return new ResponseEntity("์ ๊ทผ ๋ถ๊ฐ๋ฅํ ๊ถํ์
๋๋ค.", HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(AuthenticationEntryPointException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseEntity authenticationEntryPointException(AuthenticationEntryPointException e) {
return new ResponseEntity("๋ก๊ทธ์ธ์ด ํ์ํ ์์ฒญ์
๋๋ค.", HttpStatus.UNAUTHORIZED);
}
SecurityConfig์ ํด๋น ํธ๋ค๋ฌ ๋ฑ๋ก
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exceptionHandling -> {
exceptionHandling.authenticationEntryPoint(jwtAuthenticationFailEntryPoint);
exceptionHandling.accessDeniedHandler(jwtAccessDeniedHandler);
});
- ๋ง์ง๋ง์ผ๋ก ํธ๋ค๋ฌ๊น์ง ์ํ๋ฆฌํฐ ์ค์ ์ ๋ณด์ ์์ ๊ฐ์ด ๋ฑ๋กํด์ฃผ์์ต๋๋ค.
์ด๋ ๊ฒ JWT๋ฅผ ๋์ ํด๋ณด์๋๋ฐ, ๋ค์ ํฌ์คํ ์์๋ ๋ง์ง๋ง์ผ๋ก redis๋ฅผ ๋์ ํ์ฌ accessToken๊ฐ ๋ง๋ฃ๋ ๊ฒฝ์ฐ์ ๋ํ ์ฒ๋ฆฌ๋ฅผ ํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค !
'๐ Backend > Spring Security' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋ธ๋ก๊ทธ์ ์ ๋ณด
Study Repository
rlaehddnd0422