일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 디프만16기
- Scaffold
- dip
- flutter
- exception
- java
- Oidc
- 코딩테스트
- 코드 트리
- Kafka
- nGrinder
- pub.dev
- Kotlin
- 자료구조
- c
- 코드트리
- kakao
- 디프만
- 운영체제
- Spring
- 연습문제
- 코딩 테스트
- Sharding
- AOP
- C언어
- depromeet
- 코딩
- OAuth
- 부하 테스트
- Redis
- Today
- Total
Nick Dev
[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 4편 본문
목차
- 카카오 OIDC 소셜 로그인 구현하기(이론편)
- 인증 서버로 요청 보내는 client 선택하기 (Feign VS RestTemplate VS RestClient VS WebClient)
- 카카오 OIDC 소셜 로그인 구현하기(구현편)
- JWT 기반 인증 구현하기
이제 우리 서비스용 JWT 토큰(access, refresh token)을 발행해보자
Jwt 토큰 생성 로직
별 거 없다
그냥 코드 내용 처럼 memberId와 유효기간으로 토큰 생성하면 된다.
만약 memberId 외에 name, email 등을 추가하고 싶으면 Jwts.Builder()에 .addClaims()로 추가하면 된다.
jwt:
header: Authorization
secret: weqrwfsdafewjkdshkjghdsajkhvdsjakvhjkladfshijoqwehiuofweqhguiorqehuifdhbvkjldfahvkjadfhfiuwo
access-token-validity-in-seconds: 3600
refresh-token-validity-in-seconds: 604800
@Component
class JwtTokenProvider(
val jwtProperties: JwtProperties,
) {
// accessToken, refreshToken 생성
fun generateTokens(memberId: Long): JwtTokenDto {
val now = LocalDateTime.now()
val accessToken = generateToken(memberId, now, jwtProperties.accessTokenValidityInSeconds)
val refreshToken = generateToken(memberId, now, jwtProperties.refreshTokenValidityInSeconds)
return JwtTokenDto(accessToken, refreshToken)
}
// 토큰 검증 (유효 기간 안지난 토큰인지)
fun validateToken(token: String): Boolean {
val claims = getClaims(token)
return !claims.body.expiration.before(Date())
}
// 토큰에서 memberId 가져오기
fun getMemberIdFromToken(token: String): Long {
val claims = getClaims(token)
return claims.body.subject.toLong()
}
// 토큰 생성
private fun generateToken(
memberId: Long,
now: LocalDateTime,
validityInSeconds: Long,
): String {
val expiration = now.plusSeconds(validityInSeconds)
return Jwts.builder()
.setSubject(memberId.toString())
.setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
.setExpiration(Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant()))
.signWith(jwtProperties.key)
.compact()
}
// token에서 claim 정보 추출하기
private fun getClaims(token: String): Jws<Claims> =
try {
Jwts.parserBuilder()
.setSigningKey(jwtProperties.key)
.build()
.parseClaimsJws(token)
} catch (e: SignatureException) {
throw ApplicationException(JWT_INVALID_SIGNATURE, e)
} catch (e: ExpiredJwtException) {
throw ApplicationException(JWT_EXPIRED, e)
} catch (e: MalformedJwtException) {
throw ApplicationException(JWT_MALFORMED, e)
} catch (e: UnsupportedJwtException) {
throw ApplicationException(JWT_UNSUPPORTED, e)
} catch (e: JwtException) {
throw ApplicationException(JWT_GENERAL_ERR, e)
} catch (e: Exception) {
throw ApplicationException(UNDEFINED_EXCEPTION, e)
}
}
@ConfigurationProperties(prefix = "jwt")
data class JwtProperties(
val secret: String,
val accessTokenValidityInSeconds: Long,
val refreshTokenValidityInSeconds: Long,
) {
val key: Key = Keys.hmacShaKeyFor(secret.toByteArray())
}
Access token 정상인 경우
정상인 경우에는 필터에서 access token 파싱해서 SecurityContextHolder에 Member 객체를 넣어뒀다.
Refresh Token 처리 flow
필터를 구현해서 header에서 access token 유무 검사하고, 있으면 통과되고 없으면 에러를 던진다.
만약 access token이 만료된 경우라면 refresh token을 가지고 다시 access token과 refresh token을 재발급 받으면 된다.
이제 그 flow를 사진으로 보자!
정리하면 갱신 요청을 refresh token과 함게 보내면
서버에서는 캐시에 저장된 refresh token과 해당 유저의 refresh token이 같은지 비교하고
같으면 새 토큰들 발행하고 refresh token 갱신, 다르면 에러를 던진다.
Refresh Token 개발
🖥️ RefreshTokenEntity
@RedisHash 설정으로 하면 따로 RedisConfig 없이 간단하게 Redis에 저장할 수 있다
@Id import문 제대로 확인하시길!!
import org.springframework.data.annotation.Id
import org.springframework.data.redis.core.RedisHash
@RedisHash(value = REFRESH_TOKEN, timeToLive = 60 * 60 * 24 * 7)
data class RefreshTokenEntity(
@Id
val id: String,
val refreshToken: String,
) {
fun validateRefreshToken(refreshToken: String): Boolean {
return this.refreshToken == refreshToken
}
}
🖥️ RefreshTokenRedisRepository
JpaRepository 가 아닌 CrudRepository 다
@Repository
interface RefreshTokenRedisRepository : CrudRepository<RefreshTokenEntity, String>
🖥️ RefreshTokenService
import org.springframework.stereotype.Service
@Service
class RefreshTokenService(
private val refreshTokenRedisRepository: RefreshTokenRedisRepository,
private val jwtTokenProvider: JwtTokenProvider,
) {
/**
* refresh token을 이용하여 access token과 refresh token을 재발급한다.
* 재발급한 refresh token은 Redis에 저장한다.
*/
fun reissueTokens(refreshToken: String): JwtTokenDto {
// refresh token 검증 & memberId 추출
val memberId = jwtTokenProvider.getMemberIdFromToken(refreshToken)
// Redis에서 refresh token 조회
val refreshTokenEntity =
refreshTokenRedisRepository.findById(memberId.toString())
.orElseThrow { ApplicationException(ApplicationExceptionType.JWT_REFRESH_NOT_FOUND_IN_REDIS) }
// refresh token 검증
if (!refreshTokenEntity.validateRefreshToken(refreshToken)) {
throw ApplicationException(ApplicationExceptionType.JWT_REFRESH_INVALID)
}
// 토큰 재발급
val tokens = jwtTokenProvider.generateTokens(memberId)
// refresh token 저장
saveRefreshToken(memberId, tokens.refreshToken)
return tokens
}
/**
* refresh token을 저장한다.
*/
fun saveRefreshToken(
memberId: Long,
refreshToken: String,
) {
refreshTokenRedisRepository.save(RefreshTokenEntity(memberId.toString(), refreshToken))
}
}
🖥️ AuthController
@RestController
@RequestMapping("/v1/auth")
class AuthController(
private val authService: AuthService,
private val refreshTokenService: RefreshTokenService,
) {
@Operation(summary = "소셜 로그인", description = "소셜 로그인 후 JWT access, refresh 토큰 반환")
@PostMapping("/login")
fun socialLogin(
@RequestBody loginRequest: LoginRequest,
response: HttpServletResponse,
): ResponseEntity<JwtTokenDto> {
// 소셜 로그인 후, JWT 토큰 반환
val tokens = authService.authenticateAndRegister(loginRequest)
return ResponseEntity.ok(tokens)
}
@Operation(summary = "JWT 토큰 재발급", description = "refresh 토큰을 이용하여 access, refresh 토큰 재발급")
@PostMapping("/token/refresh")
fun refreshAccessToken(
@RequestBody refreshRequest: RefreshRequest,
): JwtTokenDto {
return refreshTokenService.reissueTokens(refreshRequest.refreshToken)
}
}
끝!
처음 제대로 된 소셜 로그인 구현이라 시간이 오래 걸렸다.
직접 flow chart도 그려보고, 공식 문서도 읽어보고, 블로그 및 깃허브도 보면서 소셜 로그인 과정은 완벽히 이해한거 같다!
직접 flow chart 그려보는게 확실히 구조나 흐름을 이해하기에 굉장히 도움이 되는 거 같다.
'SPURT' 카테고리의 다른 글
[SPURT] SPURT를 개발하면서 최적화하려고 했던 부분들 (2) | 2025.04.18 |
---|---|
[SPURT] Docker와 GitHub Actions를 활용한 CI/CD 구축 가이드 (0) | 2025.03.07 |
[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 3편 (0) | 2025.02.25 |
[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 2편 (1) | 2025.02.24 |
[SPURT] Kakao OIDC로 소셜 로그인 구현하기 - 1편 (2) | 2025.02.24 |