Skip to content

Commit

Permalink
feat: 레디스 적용 및 로그아웃 api 추가 (#40)
Browse files Browse the repository at this point in the history
* refactor: 기존에 한 클래스에서 사용하던 기능 분리 작업 진행

* feat: 로그아웃 기능 추가

* refactor: 쿠키 헤더 설정 변경

* feat: AdminAuthService 로직 수정 및 로그아웃 기능 구현

* feat: 관리자 RT가 유효하지 않을 때 발생시킬 예외 클래스 추가

* feat: 관리자 리프레쉬 토큰의 정보를 저장하고 관리하는 도메인 클래스 생성

* feat: 관리자 리프레쉬 토큰을 저장하기 위한 Repository 구현

* refactor: TokenProvider의 기능 분리로 인한 코드 수정

* remove: 불필요한 클래스 제거

- AdminTokenProvider.

* feat: redis를 추가하기 위한 설정파일 추가

* test: AdminRedisRefreshTokenRepository를 테스트하기 위한 Fake Repository 구현

* test: AdminRedisRefreshTokenRepository 테스트 클래스 작성

* test: 관리자 RT를 테스트에서 쉽게 사용하기 위해 Fixture 작성

* test: 관리자 로그아웃 기능 테스트 추가

* test: 프로덕션 코드 수정으로 인한 테스트 수정

* docs: build.gradle에 redis 설정 추가

* docs: application.yml에 redis 설정 추가

* test: application.yml에 redis 설정 추가

* docs: 관리자 명세서 최신화

* docs: 토큰 저장시간 암호화

* docs: CI 구성에 레디스 추가

* refactor: 메서드명 변경
login -> logout

* refactor: admin 생성하는 로직 메서드 분리

* refactor: 도메인 객체에 불필요하게 존재했던 @id 어노테이션 제거

* refactor: 공백 제거

* refactor: 클래스명 변경

AdminJwtAccessTokenProvider -> AdminJwtTokenProvider

* refactor: AdminRefreshToken 키 값에 @id 추가
  • Loading branch information
eom-tae-in authored Jul 11, 2024
1 parent 6245b56 commit 8f51049
Show file tree
Hide file tree
Showing 27 changed files with 440 additions and 77 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/backend-test-when-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ jobs:
defaults:
run:
working-directory: ./

services:
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379

steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
// db
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// flyway
implementation 'org.flywaydb:flyway-core'
Expand Down
10 changes: 10 additions & 0 deletions src/docs/asciidoc/admin.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,13 @@ include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발
==== 응답
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/http-response.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/response-fields.adoc[]


=== 로그아웃 (DELETE api/admins/auth/logout)

==== 요청
include::{snippets}/admin-auth-controller-test/관리자_로그아웃/http-request.adoc[]
include::{snippets}/admin-auth-controller-test/관리자_로그아웃/request-headers.adoc[]

==== 응답
include::{snippets}/admin-auth-controller-test/관리자_로그아웃/http-response.adoc[]
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
Expand Up @@ -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;
Expand All @@ -18,52 +21,65 @@
@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) {
Admin admin = createAdmin(adminSignUpRequest);
Admin savedAdmin = adminRepository.save(admin);
AdminRefreshToken adminRefreshToken = createAdminRefreshToken(savedAdmin);
adminRefreshTokenRepository.save(adminRefreshToken);
String accessToken = adminAccessTokenProvider.createAccessToken(savedAdmin.getId());

return new AdminTokenResponse(accessToken, adminRefreshToken.refreshToken());
}

private Admin createAdmin(final AdminSignUpRequest adminSignUpRequest) {
AdminProfileSignUpRequest adminProfileSignUpRequest = adminSignUpRequest.adminProfileSignUpRequest();
Admin admin = Admin.createWith(
return Admin.createWith(
adminSignUpRequest.email(),
adminSignUpRequest.password(),
adminSignUpRequest.confirmPassword(),
adminProfileSignUpRequest.name(),
adminProfileSignUpRequest.phoneNumber()
);
Admin savedAdmin = adminRepository.save(admin);

return createAdminTokenResponse(savedAdmin.getId());
}

private AdminTokenResponse createAdminTokenResponse(final Long id) {
return new AdminTokenResponse(
adminTokenProvider.createAccessToken(id),
adminTokenProvider.createRefreshToken(id)
private AdminRefreshToken createAdminRefreshToken(final Admin savedAdmin) {
return AdminRefreshToken.createWith(
adminRefreshTokenProvider,
savedAdmin.getEmail(),
savedAdmin.getId()
);
}

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);
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);
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/atwoz/admin/domain/admin/AdminRefreshToken.java
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,
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);
}
10 changes: 0 additions & 10 deletions src/main/java/com/atwoz/admin/domain/admin/AdminTokenProvider.java

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;

public interface AdminRefreshTokenProvider {

String createRefreshToken(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.atwoz.admin.exception.exceptions.AdminLoginInvalidException;
import com.atwoz.admin.exception.exceptions.AdminNotFoundException;
import com.atwoz.admin.exception.exceptions.InvalidPasswordException;
import com.atwoz.admin.exception.exceptions.InvalidRefreshTokenException;
import com.atwoz.admin.exception.exceptions.PasswordMismatchException;
import com.atwoz.admin.exception.exceptions.UnauthorizedAccessToAdminException;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -38,6 +39,11 @@ public ResponseEntity<String> handleAdminLoginInvalidException(final AdminLoginI
return getUnauthorized(e);
}

@ExceptionHandler(InvalidRefreshTokenException.class)
public ResponseEntity<String> handleRefreshTokenInvalidException(final InvalidRefreshTokenException e) {
return getUnauthorized(e);
}

private ResponseEntity<String> getNotFoundResponse(final Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(e.getMessage());
Expand Down
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;
Expand All @@ -24,9 +26,12 @@

@NoArgsConstructor
@Component
public class AdminJwtTokenProvider implements AdminTokenProvider {
public class AdminJwtTokenProvider implements AdminAccessTokenProvider,
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";
Expand Down Expand Up @@ -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())
Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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;

@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);
}
}
11 changes: 10 additions & 1 deletion src/main/java/com/atwoz/admin/ui/auth/AdminAuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -56,6 +57,14 @@ public ResponseEntity<AdminAccessTokenResponse> reGenerateAccessToken(
return ResponseEntity.ok(adminAuthService.reGenerateAccessToken(refreshToken));
}

@DeleteMapping("/logout")
public ResponseEntity<Void> logout(@AdminRefreshToken final String refreshToken) {
adminAuthService.logout(refreshToken);

return ResponseEntity.ok()
.build();
}


private HttpHeaders createCookieHeaders(final String refreshToken) {
ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, refreshToken)
Expand All @@ -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;
}
Expand Down
Loading

0 comments on commit 8f51049

Please sign in to comment.