Skip to content

Commit

Permalink
[Feat] 토큰 재발급 api 구현(#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
sjk4618 committed May 30, 2024
1 parent 58c6864 commit 712ec85
Show file tree
Hide file tree
Showing 12 changed files with 107 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum ErrorMessage {
INVALID_REFRESH_TOKEN_VALUE(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰의 값이 올바르지 않습니다."),
EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED.value(),"리프레시 토큰이 만료되었습니다. 다시 로그인해 주세요."),
MISMATCH_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 일치하지 않습니다."),
REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰을 찾을 수 없습니다."),


;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public enum SuccessMessage {
BLOG_CREATE_SUCCESS(HttpStatus.CREATED.value(), "블로그 생성이 완료되었습니다."),
BLOG_CONTENT_CREATE_SUCCESS(HttpStatus.CREATED.value(), "블로그에 글 작성이 완료되었습니다."),
GET_BLOG_CONTENT_SUCCESS(HttpStatus.OK.value(), "블로그 글 가져오기가 완료되었습니다."),

TOKEN_REISSUE_SUCCESS(HttpStatus.OK.value(), "토큰 재발급에 성공했습니다"),
;
private final int status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.sopt.springFirstSeminar.common.jwt.dto.TokenResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
Expand All @@ -20,8 +18,8 @@ public class JwtTokenGenerator {
// @Value("${jwt.refresh-token-expire-time}")
// private long REFRESH_TOKEN_EXPIRE_TIME;

private final long accessExpiration = 24 * 60 * 60 * 100L * 14;
private final long refreshExpiration = 24 * 60 * 60 * 1000L * 14;
private final long accessExpiration = 1 * 60 * 1000L; //1분으로 테스트
private final long refreshExpiration = 60 * 60 * 1000L; //60분으로 테스트

public String generateToken(final Long userId, boolean isAccessToken) {
final Date presentDate = new Date();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package org.sopt.springFirstSeminar.common.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import lombok.RequiredArgsConstructor;
import org.sopt.springFirstSeminar.common.jwt.dto.Token;
import org.sopt.springFirstSeminar.common.jwt.dto.TokenResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
setResponse(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class SecurityConfig {
private final JwtTokenValidator jwtTokenValidator;


private static final String[] AUTH_WHITE_LIST = {"/api/v1/member", "/test"};
private static final String[] AUTH_WHITE_LIST = {"/api/v1/member/signup", "/test", "/api/v1/member/reissue"};

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
Expand All @@ -46,7 +46,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
auth.requestMatchers(AUTH_WHITE_LIST).permitAll();
auth.anyRequest().authenticated();
})
// .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, jwtTokenValidator), UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, jwtTokenValidator), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.sopt.springFirstSeminar.common.jwt.dto;

public record TokenAndUserIdResponse(
String accessToken,
String refreshToken,
Long userId
) {

public static TokenAndUserIdResponse of(Token token, Long memberId) {
return new TokenAndUserIdResponse(token.accessToken(), token.refreshToken(), memberId);
}
}


This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,34 @@


import lombok.RequiredArgsConstructor;
import org.apache.tomcat.util.http.parser.Authorization;
import org.sopt.springFirstSeminar.common.ApiResponseUtil;
import org.sopt.springFirstSeminar.common.BaseResponse;
import org.sopt.springFirstSeminar.common.Constant;
import org.sopt.springFirstSeminar.common.dto.SuccessMessage;
import org.sopt.springFirstSeminar.common.jwt.auth.MemberId;
import org.sopt.springFirstSeminar.common.jwt.dto.TokenResponse;
import org.sopt.springFirstSeminar.common.jwt.dto.TokenAndUserIdResponse;
import org.sopt.springFirstSeminar.service.MemberService;
import org.sopt.springFirstSeminar.service.dto.MemberCreateDTO;
import org.sopt.springFirstSeminar.service.dto.MemberFindDTO;
import org.sopt.springFirstSeminar.service.dto.MemberDataDTO;
import org.springframework.http.HttpStatus;
import org.sopt.springFirstSeminar.service.dto.ReissueRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

import static org.sopt.springFirstSeminar.common.Constant.AUTHORIZATION;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/member")
public class MemberController {

private final MemberService memberService;

@PostMapping
@PostMapping("signup")
public ResponseEntity<BaseResponse<?>> postMember(@RequestBody MemberCreateDTO memberCreate)
{
final TokenResponse memberJoinResponse = memberService.createMember(memberCreate);
final TokenAndUserIdResponse memberJoinResponse = memberService.createMember(memberCreate);

return ApiResponseUtil.success(SuccessMessage.MEMBER_CREATE_SUCCESS, memberJoinResponse);
}
Expand All @@ -42,6 +42,15 @@ public ResponseEntity<BaseResponse<?>> findMemberById(@MemberId final Long membe
return ApiResponseUtil.success(SuccessMessage.MEMBER_FIND_SUCCESS, memberFindDTO);
}

@PostMapping("reissue")
public ResponseEntity<BaseResponse<?>> reissue(@RequestHeader(AUTHORIZATION) final String refreshToken,
@RequestBody final ReissueRequest reissueRequest) {

final TokenAndUserIdResponse reissueTokenResponse = memberService.reissue(refreshToken, reissueRequest);

return ApiResponseUtil.success(SuccessMessage.TOKEN_REISSUE_SUCCESS, reissueTokenResponse);
}

@DeleteMapping("/{memberId}")
public ResponseEntity deleteMemberById(@PathVariable final Long memberId) {
memberService.deleteMemberById(memberId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sopt.springFirstSeminar.service.dto.MemberFindDTO;

@Entity
@Getter
Expand Down Expand Up @@ -36,4 +37,8 @@ public static Member create(String name, Part part, int age) {
.part(part)
.build();
}

public static Member of(Member member) {
return new Member(member.getName(), member.getPart(), member.getAge());
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package org.sopt.springFirstSeminar.service;

import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.sopt.springFirstSeminar.common.dto.ErrorMessage;
import org.sopt.springFirstSeminar.common.jwt.JwtTokenGenerator;
import org.sopt.springFirstSeminar.common.jwt.JwtTokenProvider;
import org.sopt.springFirstSeminar.common.jwt.UserAuthentication;
import org.sopt.springFirstSeminar.common.jwt.JwtTokenValidator;
import org.sopt.springFirstSeminar.common.jwt.auth.RefreshToken;
import org.sopt.springFirstSeminar.common.jwt.auth.redis.repository.RefreshTokenRepository;
import org.sopt.springFirstSeminar.common.jwt.dto.Token;
import org.sopt.springFirstSeminar.common.jwt.dto.TokenResponse;
import org.sopt.springFirstSeminar.common.jwt.dto.TokenAndUserIdResponse;
import org.sopt.springFirstSeminar.domain.Member;
import org.sopt.springFirstSeminar.exception.NotFoundException;
import org.sopt.springFirstSeminar.exception.UnauthorizedException;
import org.sopt.springFirstSeminar.repository.MemberRepository;
import org.sopt.springFirstSeminar.service.dto.MemberCreateDTO;
import org.sopt.springFirstSeminar.service.dto.MemberFindDTO;
import org.sopt.springFirstSeminar.service.dto.MemberDataDTO;
import org.sopt.springFirstSeminar.service.dto.ReissueRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -30,10 +32,11 @@ public class MemberService {

private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenRepository refreshTokenRepository;
private final JwtTokenValidator jwtTokenValidator;

//멤버가입
@Transactional
public TokenResponse createMember(MemberCreateDTO memberCreate) {
public TokenAndUserIdResponse createMember(MemberCreateDTO memberCreate) {

Member createdMember = memberRepository.save(
Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age())
Expand All @@ -42,27 +45,77 @@ public TokenResponse createMember(MemberCreateDTO memberCreate) {
Token issuedToken = jwtTokenProvider.issueTokens(createdMemberId);
updateRefreshToken(issuedToken.refreshToken(), createdMemberId);

return TokenResponse.of(issuedToken.accessToken(), issuedToken.refreshToken(), createdMemberId);
return TokenAndUserIdResponse.of(issuedToken, createdMemberId);
}

@Transactional
public TokenAndUserIdResponse reissue(final String refreshToken, final ReissueRequest reissueRequest) {

Long memberId = reissueRequest.memberId();
validateRefreshToken(refreshToken,memberId);
Member member = findMemberBy(memberId);
Token issueedToken = jwtTokenProvider.issueTokens(memberId);
updateRefreshToken(issueedToken.refreshToken(), memberId);
return TokenAndUserIdResponse.of(issueedToken, memberId);


}


private void validateRefreshToken(final String refreshToken, final Long userId) {
try {
jwtTokenValidator.validateRefreshToken(refreshToken);
String storedRefreshToken = getRefreshToken(userId);
jwtTokenValidator.equalsRefreshToken(refreshToken, storedRefreshToken);
} catch (UnauthorizedException e) {
signOut(userId);
throw e;
}
}

private String getRefreshToken(final Long memberId) {
try {
return getRefreshTokenFromRedis(memberId);
} catch (EntityNotFoundException e) {
throw new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND);
}
}

private String getRefreshTokenFromRedis(Long userId) {
RefreshToken storedRefreshToken = refreshTokenRepository.findById(userId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.REFRESH_TOKEN_NOT_FOUND));
return storedRefreshToken.getRefreshToken();
}

private void updateRefreshToken(String refreshToken, Long memberId) {
refreshTokenRepository.save(RefreshToken.of(memberId, refreshToken));
}

public void signOut(final Long memberId) {
Member findMember = findMemberBy(memberId);
deleteRefreshToken(findMember);
}

public void findById(final Long memberId) {
findMember(memberId).orElseThrow(
() -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND));
}

private void deleteRefreshToken(final Member member) {
refreshTokenRepository.deleteById(member.getId());
}

public MemberFindDTO findMemberById(final Long memberId) {
return MemberFindDTO.of(findMember(memberId).orElseThrow(
() -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND)));
}

public Member findMemberBy(final Long memberId) {
return memberRepository.findById(memberId).orElseThrow(
() -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND));

}

@Transactional
public void deleteMemberById(final Long memberId) {
Member member = memberRepository.findById(memberId).orElseThrow(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.sopt.springFirstSeminar.service.dto;

public record ReissueRequest(
Long memberId
) {

}

0 comments on commit 712ec85

Please sign in to comment.