-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 레디스 적용 및 로그아웃 api 추가 #40
Changes from 21 commits
d9647af
bd442bf
756096a
522fecd
252bc20
6595bc6
7d608b1
2c829ff
d364000
ed7f2f3
05423dc
f4389a2
fe68ceb
b0e0d5f
2ebf1ee
95cf36a
a4df6e8
fbeb794
3d29b7c
6ec0f14
407ea48
f463404
953e5a4
ce8714e
508194d
a3fb9f4
b80f3f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.atwoz.admin.application.auth; | ||
|
||
public interface AdminAccessTokenProvider { | ||
|
||
String createAccessToken(Long id); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,9 +6,12 @@ | |
import com.atwoz.admin.application.auth.dto.AdminSignUpRequest; | ||
import com.atwoz.admin.application.auth.dto.AdminTokenResponse; | ||
import com.atwoz.admin.domain.admin.Admin; | ||
import com.atwoz.admin.domain.admin.AdminRefreshToken; | ||
import com.atwoz.admin.domain.admin.AdminRefreshTokenRepository; | ||
import com.atwoz.admin.domain.admin.AdminRepository; | ||
import com.atwoz.admin.domain.admin.AdminTokenProvider; | ||
import com.atwoz.admin.domain.admin.service.AdminRefreshTokenProvider; | ||
import com.atwoz.admin.exception.exceptions.AdminNotFoundException; | ||
import com.atwoz.admin.exception.exceptions.InvalidRefreshTokenException; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
@@ -18,10 +21,10 @@ | |
@Transactional | ||
public class AdminAuthService { | ||
|
||
private static final String ID = "id"; | ||
|
||
private final AdminRepository adminRepository; | ||
private final AdminTokenProvider adminTokenProvider; | ||
private final AdminAccessTokenProvider adminAccessTokenProvider; | ||
private final AdminRefreshTokenProvider adminRefreshTokenProvider; | ||
private final AdminRefreshTokenRepository adminRefreshTokenRepository; | ||
|
||
public AdminTokenResponse signUp(final AdminSignUpRequest adminSignUpRequest) { | ||
AdminProfileSignUpRequest adminProfileSignUpRequest = adminSignUpRequest.adminProfileSignUpRequest(); | ||
|
@@ -33,37 +36,46 @@ public AdminTokenResponse signUp(final AdminSignUpRequest adminSignUpRequest) { | |
adminProfileSignUpRequest.phoneNumber() | ||
); | ||
Admin savedAdmin = adminRepository.save(admin); | ||
AdminRefreshToken adminRefreshToken = createAdminRefreshToken(savedAdmin); | ||
adminRefreshTokenRepository.save(adminRefreshToken); | ||
String accessToken = adminAccessTokenProvider.createAccessToken(savedAdmin.getId()); | ||
|
||
return createAdminTokenResponse(savedAdmin.getId()); | ||
} | ||
|
||
private AdminTokenResponse createAdminTokenResponse(final Long id) { | ||
return new AdminTokenResponse( | ||
adminTokenProvider.createAccessToken(id), | ||
adminTokenProvider.createRefreshToken(id) | ||
); | ||
return new AdminTokenResponse(accessToken, adminRefreshToken.refreshToken()); | ||
} | ||
|
||
public AdminTokenResponse login(final AdminLoginRequest adminLoginRequest) { | ||
Admin foundAdmin = findAdminByEmail(adminLoginRequest.email()); | ||
foundAdmin.validatePassword(adminLoginRequest.password()); | ||
AdminRefreshToken adminRefreshToken = createAdminRefreshToken(foundAdmin); | ||
adminRefreshTokenRepository.save(adminRefreshToken); | ||
String accessToken = adminAccessTokenProvider.createAccessToken(foundAdmin.getId()); | ||
|
||
return createAdminTokenResponse(foundAdmin.getId()); | ||
} | ||
|
||
public AdminAccessTokenResponse reGenerateAccessToken(final String refreshToken) { | ||
Admin foundAdmin = findAdminById(adminTokenProvider.extract(refreshToken, ID, Long.class)); | ||
|
||
return new AdminAccessTokenResponse(adminTokenProvider.createAccessToken(foundAdmin.getId())); | ||
return new AdminTokenResponse(accessToken, adminRefreshToken.refreshToken()); | ||
} | ||
|
||
private Admin findAdminByEmail(final String email) { | ||
return adminRepository.findAdminByEmail(email) | ||
.orElseThrow(AdminNotFoundException::new); | ||
} | ||
|
||
private Admin findAdminById(final Long id) { | ||
return adminRepository.findAdminById(id) | ||
.orElseThrow(AdminNotFoundException::new); | ||
private AdminRefreshToken createAdminRefreshToken(final Admin savedAdmin) { | ||
return AdminRefreshToken.createWith( | ||
adminRefreshTokenProvider, | ||
savedAdmin.getEmail(), | ||
savedAdmin.getId() | ||
); | ||
} | ||
|
||
public AdminAccessTokenResponse reGenerateAccessToken(final String refreshToken) { | ||
AdminRefreshToken foundAdminRefreshToken = adminRefreshTokenRepository.findById(refreshToken) | ||
.orElseThrow(InvalidRefreshTokenException::new); | ||
Long memberId = foundAdminRefreshToken.memberId(); | ||
String createdAccessToken = adminAccessTokenProvider.createAccessToken(memberId); | ||
|
||
return new AdminAccessTokenResponse(createdAccessToken); | ||
} | ||
|
||
public void logout(final String refreshToken) { | ||
adminRefreshTokenRepository.delete(refreshToken); | ||
} | ||
Comment on lines
+82
to
84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프론트엔드에서 헤더에서 토큰을 제거해주고 동시에 로그아웃 api를 요청해서 리프레쉬 토큰까지 제거해서 로그아웃을 시켜준다라는 의미로 작성하신 코드가 맞을까요?! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네 맞습니다! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.atwoz.admin.domain.admin; | ||
|
||
import com.atwoz.admin.domain.admin.service.AdminRefreshTokenProvider; | ||
import lombok.Builder; | ||
import org.springframework.data.annotation.Id; | ||
|
||
@Builder | ||
public record AdminRefreshToken( | ||
@Id String refreshToken, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확인해보니 레디스로 관리할 경우에는 말씀하신 것 처럼 |
||
Long memberId | ||
) { | ||
|
||
public static AdminRefreshToken createWith(final AdminRefreshTokenProvider adminRefreshTokenProvider, | ||
final String email, | ||
final Long memberId) { | ||
return AdminRefreshToken.builder() | ||
.refreshToken(adminRefreshTokenProvider.createRefreshToken(email)) | ||
.memberId(memberId) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.atwoz.admin.domain.admin; | ||
|
||
import java.util.Optional; | ||
|
||
public interface AdminRefreshTokenRepository { | ||
|
||
void save(AdminRefreshToken adminRefreshToken); | ||
|
||
Optional<AdminRefreshToken> findById(String refreshToken); | ||
|
||
void delete(String refreshToken); | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.atwoz.admin.domain.admin.service; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AT 발급 인터페이스는 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희가 RT만 레디스에 저장하기 때문에 RT 도메인 객체는 도메인에 위치해야합니다. 하지만 RT의 암호화된 토큰은 JwtTokenProvider에서 만들어집니다. 만약 JwtTokenProvider 통해 서비스 레이어에서 생성해준다면 RT 도메인 객체는 응용 계층에서 만든 암호화된 토큰을 받게 됩니다. 이로 인한 응용 계층의 의존이 생기기 때문에 RTProvider를 팩토리 메서드 인자로 넘겨줘서 내부에서 암호화된 토큰이 만들어지도록 구현해야한다고 생각했습니다. RTProvider는 도메인 서비스이기에 도메인 레이어에 위치해야합니다. 하지만 ATProvider는 도메인에 어떠한 영향도 미치지 않기 때문에 응용 계층에서 생성해서 응답 바디에 넘겨주면 됩니다. 제가 AT, RT 생성 기능을 모두 추상화한 인터페이스로 둔 이유가 바로 이 이유 때문이었습니다. 추가적인 의견 및 제가 잘못 이해해서 설계한 부분이 있다면 댓글로 의견 남겨주시면 감사하겠습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 이해했습니다. AT 발급 (ATProvider)은 도메인에 영향을 미치지 않고 응용 계층에서만 이용되기 때문에 응용 계층에 인터페이스를 위치시키고, RT 발급 (RTPRovider)을 AT 발급처럼 응용 계층에 둔다면 도메인 계층에 있는 AdminRefreshToken에서 응용 계층에 대한 의존이 발생되기 때문에 도메인 패키지 하위에 서비스 패키지를 별도로 만들어주신거군요. |
||
|
||
public interface AdminRefreshTokenProvider { | ||
|
||
String createRefreshToken(String email); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.atwoz.admin.exception.exceptions; | ||
|
||
public class InvalidRefreshTokenException extends RuntimeException { | ||
|
||
public InvalidRefreshTokenException() { | ||
super("리프레쉬 토큰이 유효하지 않습니다"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
package com.atwoz.admin.infrastructure.auth; | ||
|
||
import com.atwoz.admin.domain.admin.AdminTokenProvider; | ||
import com.atwoz.admin.application.auth.AdminAccessTokenProvider; | ||
import com.atwoz.admin.domain.admin.service.AdminRefreshTokenProvider; | ||
import com.atwoz.admin.ui.auth.support.AdminTokenExtractor; | ||
import com.atwoz.member.exception.exceptions.auth.ExpiredTokenException; | ||
import com.atwoz.member.exception.exceptions.auth.SignatureInvalidException; | ||
import com.atwoz.member.exception.exceptions.auth.TokenFormInvalidException; | ||
|
@@ -24,9 +26,12 @@ | |
|
||
@NoArgsConstructor | ||
@Component | ||
public class AdminJwtTokenProvider implements AdminTokenProvider { | ||
public class AdminJwtAccessTokenProvider implements AdminAccessTokenProvider, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 개인적인 생각으로는 해당 클래스에서 AT와 RT를 모두 만들고 있기 때문에 기존 이름 (AdminJwtTokenProvider)이 더 적합할 것 같은데, 클래스 이름을 변경하신 이유가 있으실까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JwtTokenProvider 구조를 변경하면서 이름이 자동으로 변경되었던 것 같습니다 수정하도록 하겠습니다! |
||
AdminRefreshTokenProvider, | ||
AdminTokenExtractor { | ||
|
||
private static final String ID = "id"; | ||
private static final String EMAIL = "email"; | ||
private static final String TOKEN_TYPE = "token type"; | ||
private static final String REFRESH_TOKEN = "refresh token"; | ||
private static final String ACCESS_TOKEN = "access token"; | ||
|
@@ -60,16 +65,17 @@ public String createAccessToken(final Long id) { | |
} | ||
|
||
@Override | ||
public String createRefreshToken(final Long id) { | ||
public String createRefreshToken(final String email) { | ||
Claims claims = Jwts.claims(); | ||
claims.put(ID, id); | ||
claims.put(EMAIL, email); | ||
claims.put(TOKEN_TYPE, REFRESH_TOKEN); | ||
claims.put(ROLE, ADMIN); | ||
|
||
return createToken(claims, refreshTokenExpirationPeriod); | ||
} | ||
|
||
private String createToken(final Claims claims, final int expirationPeriod) { | ||
private String createToken(final Claims claims, | ||
final int expirationPeriod) { | ||
return Jwts.builder() | ||
.setClaims(claims) | ||
.setIssuedAt(issuedAt()) | ||
|
@@ -94,7 +100,9 @@ private Date expiredAt(final int expirationPeriod) { | |
} | ||
|
||
@Override | ||
public <T> T extract(final String token, final String claimName, final Class<T> classType) { | ||
public <T> T extract(final String token, | ||
final String claimName, | ||
final Class<T> classType) { | ||
try { | ||
return Jwts.parserBuilder() | ||
.setSigningKey(secret.getBytes()) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package com.atwoz.admin.infrastructure.auth; | ||
|
||
import com.atwoz.admin.domain.admin.AdminRefreshToken; | ||
import com.atwoz.admin.domain.admin.AdminRefreshTokenRepository; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.concurrent.TimeUnit; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.data.redis.core.ValueOperations; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@RequiredArgsConstructor | ||
@Repository | ||
public class AdminRedisRefreshTokenRepository implements AdminRefreshTokenRepository { | ||
|
||
@Value("${redis.expiration-period}") | ||
private int expirationPeriod; | ||
|
||
private final RedisTemplate<String, Long> redisTemplate; | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 공백 있습니다 :) |
||
@Override | ||
public void save(final AdminRefreshToken adminRefreshToken) { | ||
ValueOperations<String, Long> valueOperations = redisTemplate.opsForValue(); | ||
valueOperations.set(adminRefreshToken.refreshToken(), adminRefreshToken.memberId()); | ||
redisTemplate.expire(adminRefreshToken.refreshToken(), expirationPeriod, TimeUnit.DAYS); | ||
} | ||
|
||
@Override | ||
public Optional<AdminRefreshToken> findById(final String refreshToken) { | ||
ValueOperations<String, Long> valueOperations = redisTemplate.opsForValue(); | ||
Long memberId = valueOperations.get(refreshToken); | ||
if (Objects.isNull(memberId)) { | ||
return Optional.empty(); | ||
} | ||
|
||
return Optional.of(new AdminRefreshToken(refreshToken, memberId)); | ||
} | ||
|
||
@Override | ||
public void delete(final String refreshToken) { | ||
redisTemplate.delete(refreshToken); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.ResponseCookie; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.DeleteMapping; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
|
@@ -56,6 +57,14 @@ public ResponseEntity<AdminAccessTokenResponse> reGenerateAccessToken( | |
return ResponseEntity.ok(adminAuthService.reGenerateAccessToken(refreshToken)); | ||
} | ||
|
||
@DeleteMapping("/logout") | ||
public ResponseEntity<Void> login(@AdminRefreshToken final String refreshToken) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그아웃을 하는 메서드인만큼 메서드명을 logout으로 하는 게 더 좋을 것 같습니다! |
||
adminAuthService.logout(refreshToken); | ||
|
||
return ResponseEntity.ok() | ||
.build(); | ||
} | ||
|
||
|
||
private HttpHeaders createCookieHeaders(final String refreshToken) { | ||
ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, refreshToken) | ||
|
@@ -65,7 +74,7 @@ private HttpHeaders createCookieHeaders(final String refreshToken) { | |
.maxAge(maxAge) | ||
.build(); | ||
HttpHeaders httpHeaders = new HttpHeaders(); | ||
httpHeaders.add(HttpHeaders.COOKIE, cookie.toString()); | ||
httpHeaders.add(HttpHeaders.SET_COOKIE, cookie.toString()); | ||
|
||
return httpHeaders; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AT를 만들기까지의 과정과 RT를 만들기까지의 과정을 메서드 분리하는 것은 어떨지 여쭤보고 싶습니다..! (RT의 경우, 리포지터리에 save하는 것도 createAdminRefreshToken 메서드에 포함시키면 될 것 같습니다.)
또는, Admin을 save하는 것을 메서드 분리해도 좋을 듯 합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메서드 분리를 굳이 할 필요가 없어보여 하지 않았는데 다른 곳에서 재사용을 하지 않아도 작성하신 코드를 보니 분리하는게 나을 것 같다는 생각이 드네요. 메서드 분리하도록 하겠습니다!