Skip to content

Commit

Permalink
[feat] 리프레시 토큰 구현 (#76)
Browse files Browse the repository at this point in the history
* [feat] 토큰 재발급 기능 #66

* [feat] 레디스 설정 #66

* [feat] DTO 추가 및 수정 #66

* [feat] EXCEPTION 추가 #66

* [feat] 레디스 추가 및 적용 #66

* [refact] 메소드 수정 #66
  • Loading branch information
suhhyun524 authored Jul 7, 2023
1 parent 5783dbe commit 6fb2eea
Show file tree
Hide file tree
Showing 16 changed files with 185 additions and 80 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
/dump.rdb

### STS ###
.apt_generated
Expand Down Expand Up @@ -39,4 +40,4 @@ out/
.DS_Store

### application ###
application-secret.yml
application-secret.yml
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
Expand Down
9 changes: 8 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ services:
dockerfile: Dockerfile
context: ./config/nginx
ports:
- "80:80"
- "80:80"

redis:
image: redis:latest
container_name: redis
hostname: redis
ports:
- "6379:6379"
25 changes: 11 additions & 14 deletions src/main/java/ceos/backend/domain/admin/AdminController.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package ceos.backend.domain.admin;

import ceos.backend.domain.admin.dto.request.*;
import ceos.backend.domain.admin.dto.response.CheckUsernameResponse;
import ceos.backend.domain.admin.dto.response.FindIdResponse;
import ceos.backend.domain.admin.dto.response.GetAdminsResponse;
import ceos.backend.domain.admin.dto.response.SignInResponse;
import ceos.backend.domain.admin.dto.response.*;
import ceos.backend.domain.admin.service.AdminService;
import ceos.backend.global.config.user.AdminDetails;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -39,7 +36,7 @@ public void signUp(@RequestBody @Valid SignUpRequest signUpRequest) {

@Operation(summary = "로그인")
@PostMapping("/signin")
public SignInResponse signIn(@RequestBody @Valid SignInRequest signInRequest) {
public TokenResponse signIn(@RequestBody @Valid SignInRequest signInRequest) {
log.info("로그인");
return adminService.signIn(signInRequest);
}
Expand Down Expand Up @@ -69,20 +66,20 @@ public void resetPwd(
}

@Operation(summary = "로그아웃")
@PostMapping("/logout")
@GetMapping("/logout")
public void logout(@AuthenticationPrincipal AdminDetails adminUser) {
log.info("로그아웃");
adminService.logout(adminUser);
}

// @Operation(summary = "토큰 재발급")
// @PostMapping("/refresh")
// public RefreshTokenResponse refreshToken(
// @RequestBody @Valid String refreshToken,
// @AuthenticationPrincipal AdminDetails adminUser) {
// log.info("토큰 재발급");
// return adminService.refreshToken(refreshToken, adminUser);
// }
@Operation(summary = "토큰 재발급")
@PostMapping("/reissue")
public TokenResponse refreshToken(
@RequestBody @Valid RefreshTokenRequest refreshTokenRequest,
@AuthenticationPrincipal AdminDetails adminUser) {
log.info("토큰 재발급");
return adminService.reissueToken(refreshTokenRequest, adminUser);
}

@Operation(summary = "슈퍼유저 - 유저 목록 보기")
@GetMapping("/super")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ceos.backend.domain.admin.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;

@Getter
public class RefreshTokenRequest {

@Schema(defaultValue = "string")
@NotNull(message = "리프레시 토큰을 입력해주세요.")
private String refreshToken;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@
import lombok.Getter;

@Getter
public class SignInResponse {
public class TokenResponse {

private String accessToken;
private String refreshToken;

@Builder
private SignInResponse(String accessToken, String refreshToken){
private TokenResponse(String accessToken, String refreshToken){
this.accessToken = accessToken;
this.refreshToken =refreshToken;
}

public static SignInResponse of(String accessToken, String refreshToken){
return SignInResponse.builder()
public static TokenResponse of(String accessToken, String refreshToken){
return TokenResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ public enum AdminErrorCode implements BaseErrorCode {
/* Data */
MISMATCH_NEW_PASSWORD(BAD_REQUEST, "ADMIN_400_3", "새비밀번호가 일치하지 않습니다"),
MISMATCH_PASSWORD(BAD_REQUEST, "ADMIN_400_4", "비밀번호가 일치하지 않습니다"),
DUPLICATE_DATA(CONFLICT, "ADMIN_409_1", "이미 존재하는 데이터입니다");
DUPLICATE_DATA(CONFLICT, "ADMIN_409_1", "이미 존재하는 데이터입니다"),

/* REFRESH TOKEN */
NOT_REFRESH_TOKEN(BAD_REQUEST, "ADMIN_400_5", "리프레시 토큰이 아닙니다"),
REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "ADMIN_404_2", "존재하지 않거나 만료된 리프레시 토큰입니다.");
private HttpStatus status;
private String code;
private String reason;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ceos.backend.domain.admin.exception;

import ceos.backend.global.error.BaseErrorException;

public class NotRefreshToken extends BaseErrorException {

public static final NotRefreshToken EXCEPTION = new NotRefreshToken();

public NotRefreshToken() {
super(AdminErrorCode.NOT_REFRESH_TOKEN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ceos.backend.domain.admin.exception;

import ceos.backend.global.error.BaseErrorException;

public class RefreshTokenNotFound extends BaseErrorException {

public static final RefreshTokenNotFound EXCEPTION = new RefreshTokenNotFound();

private RefreshTokenNotFound() {
super(AdminErrorCode.REFRESH_TOKEN_NOT_FOUND);
}
}
31 changes: 7 additions & 24 deletions src/main/java/ceos/backend/domain/admin/helper/AdminHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
import ceos.backend.domain.recruitment.helper.RecruitmentHelper;
import ceos.backend.global.common.dto.AwsSESPasswordMail;
import ceos.backend.global.common.event.Event;
import ceos.backend.global.config.jwt.TokenProvider;
import ceos.backend.global.config.user.AdminDetailsService;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
Expand All @@ -29,11 +29,11 @@
@Component
@RequiredArgsConstructor
public class AdminHelper {
private final RedisTemplate<String, String> redisTemplate;
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
private final RecruitmentHelper recruitmentHelper;
private final AdminRepository adminRepository;
private final AdminDetailsService adminDetailsService;
private final TokenProvider tokenProvider;

public String encodePassword(String password) {
return passwordEncoder.encode(password);
Expand Down Expand Up @@ -61,19 +61,6 @@ public Authentication adminAuthorizationInput(Admin admin) {
return authentication;
}

public String getAccessToken(Admin admin, Authentication authentication) {
return tokenProvider.createAccessToken(admin.getId(), authentication);
}

public String getRefreshToken(Admin admin, Authentication authentication) {
final String refreshToken = tokenProvider.createRefreshToken(authentication);

admin.setRefreshToken(refreshToken);
adminRepository.save(admin);

return refreshToken;
}

public void findDuplicateUsername(String username) {
if (adminRepository
.findByUsername(username)
Expand Down Expand Up @@ -161,17 +148,13 @@ public void resetPwd(ResetPwdRequest resetPwdRequest, Admin admin) {
adminRepository.save(admin);
}

public void deleteRefreshToken(Admin admin) {
admin.deleteRefreshToken();
adminRepository.save(admin);
public void matchesRefreshToken(String refreshToken, Admin admin) {
String savedToken = redisTemplate.opsForValue().get(admin.getId().toString());
if (savedToken == null || !savedToken.equals(refreshToken)) {
throw RefreshTokenNotFound.EXCEPTION;
}
}

// public void matchesRefreshToken(String refreshToken, Admin admin) {
// if(!admin.getRefreshToken().equals(refreshToken)) {
// throw .Exception;
// }
// }


public Admin findAdmin(Long adminId) {
return adminRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,14 @@ public CheckUsernameResponse toCheckUsernameResponse(boolean isAvailable) {
return CheckUsernameResponse.from(isAvailable);
}

public SignInResponse toSignInResponse(String accessToken, String refreshToken) {
return SignInResponse.of(accessToken, refreshToken);
public TokenResponse toTokenResponse(String accessToken, String refreshToken) {
return TokenResponse.of(accessToken, refreshToken);
}

public FindIdResponse toFindIdResponse(String username) {
return FindIdResponse.from(username);
}

public RefreshTokenResponse toRefreshTokenResponse(String accessToken) {
return RefreshTokenResponse.from(accessToken);
}

public GetAdminsResponse toGetAdmins(List<Admin> adminList) {

List<AdminBriefInfoVo> adminBriefInfoVos = adminList.stream()
Expand Down
46 changes: 24 additions & 22 deletions src/main/java/ceos/backend/domain/admin/service/AdminService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
import ceos.backend.domain.admin.domain.Admin;
import ceos.backend.domain.admin.domain.AdminRole;
import ceos.backend.domain.admin.dto.request.*;
import ceos.backend.domain.admin.dto.response.CheckUsernameResponse;
import ceos.backend.domain.admin.dto.response.FindIdResponse;
import ceos.backend.domain.admin.dto.response.GetAdminsResponse;
import ceos.backend.domain.admin.dto.response.SignInResponse;
import ceos.backend.domain.admin.dto.response.*;
import ceos.backend.domain.admin.helper.AdminHelper;
import ceos.backend.domain.admin.repository.AdminMapper;
import ceos.backend.domain.admin.repository.AdminRepository;
import ceos.backend.global.config.jwt.TokenProvider;
import ceos.backend.global.config.user.AdminDetails;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -22,6 +20,7 @@
@RequiredArgsConstructor
public class AdminService {

private final TokenProvider tokenProvider;
private final AdminHelper adminHelper;
private final AdminMapper adminMapper;
private final AdminRepository adminRepository;
Expand All @@ -47,16 +46,16 @@ public void signUp(SignUpRequest signUpRequest) {
}

@Transactional
public SignInResponse signIn(SignInRequest signInRequest) {
public TokenResponse signIn(SignInRequest signInRequest) {

final Admin admin = adminHelper.findForSignIn(signInRequest);
final Authentication authentication = adminHelper.adminAuthorizationInput(admin);

//토큰 발급
final String accessToken = adminHelper.getAccessToken(admin, authentication);
final String refreshToken = adminHelper.getRefreshToken(admin, authentication);
final String accessToken = tokenProvider.createAccessToken(admin.getId(), authentication);
final String refreshToken = tokenProvider.createRefreshToken(admin.getId(), authentication);

return adminMapper.toSignInResponse(accessToken, refreshToken);
return adminMapper.toTokenResponse(accessToken, refreshToken);
}

@Transactional
Expand Down Expand Up @@ -94,22 +93,25 @@ public void resetPwd(ResetPwdRequest resetPwdRequest, AdminDetails adminUser) {
public void logout(AdminDetails adminUser) {
final Admin admin = adminUser.getAdmin();

adminHelper.deleteRefreshToken(admin);
//레디스 삭제
tokenProvider.deleteRefreshToken(admin.getId());
}

// @Transactional
// public RefreshTokenResponse refreshToken(String refreshToken, AdminDetails adminUser) {
// final Admin admin = adminUser.getAdmin();
// final Authentication authentication = adminHelper.adminAuthorizationInput(admin);
// //리프레시 토큰 검증
// tokenProvider.validateToken(refreshToken);
// adminHelper.matchesRefreshToken()
//
// //토큰 재발급
// final String accessToken = adminHelper.getAccessToken(admin, authentication);
//
// return adminMapper.toRefreshTokenResponse(accessToken);
// }
@Transactional
public TokenResponse reissueToken(RefreshTokenRequest refreshTokenRequest, AdminDetails adminUser) {
final Admin admin = adminUser.getAdmin();
final Authentication authentication = adminHelper.adminAuthorizationInput(admin);
final String refreshToken = refreshTokenRequest.getRefreshToken();
//리프레시 토큰 검증
tokenProvider.validateRefreshToken(refreshToken);
adminHelper.matchesRefreshToken(refreshToken, admin);

//토큰 재발급
final String newAccessToken = tokenProvider.createAccessToken(admin.getId(), authentication);
final String newRefreshToken = tokenProvider.createRefreshToken(admin.getId(), authentication);

return adminMapper.toTokenResponse(newAccessToken, newRefreshToken);
}

@Transactional(readOnly = true)
public GetAdminsResponse getAdmins(AdminDetails adminUser) {
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/ceos/backend/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ceos.backend.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.port}")
private int port;

@Value("${spring.data.redis.host}")
private String host;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

@Bean
public RedisTemplate<String, String> redisTemplate() {
// redisTemplate를 받아와서 set, get, delete를 사용
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
// setKeySerializer, setValueSerializer 설정
// redis-cli을 통해 직접 데이터를 조회 시 알아볼 수 없는 형태로 출력되는 것을 방지
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());

return redisTemplate;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ protected void doFilterInternal(
filterChain.doFilter(request, response);
return;
}
if (StringUtils.isNotBlank(token) && tokenProvider.validateToken(token)) {
if (StringUtils.isNotBlank(token) && tokenProvider.validateAccessToken(token)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
Expand Down
Loading

0 comments on commit 6fb2eea

Please sign in to comment.