diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/dto/ErrorMessage.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/dto/ErrorMessage.java index 263f9d2..7d88d38 100644 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/dto/ErrorMessage.java +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/dto/ErrorMessage.java @@ -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(), "리프레시 토큰을 찾을 수 없습니다."), ; diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/dto/SuccessMessage.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/dto/SuccessMessage.java index b828272..b28f2f9 100644 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/dto/SuccessMessage.java +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/dto/SuccessMessage.java @@ -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; diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/JwtTokenGenerator.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/JwtTokenGenerator.java index 879ca44..dc7ab46 100644 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/JwtTokenGenerator.java +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/JwtTokenGenerator.java @@ -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; @@ -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(); diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/JwtTokenProvider.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/JwtTokenProvider.java index 90c69ac..cad7740 100644 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/JwtTokenProvider.java +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/JwtTokenProvider.java @@ -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 diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/auth/CustomAccessDeniedHandler.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/auth/CustomAccessDeniedHandler.java index 3ae38c9..b4fac77 100644 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/auth/CustomAccessDeniedHandler.java +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/auth/CustomAccessDeniedHandler.java @@ -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); diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/auth/SecurityConfig.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/auth/SecurityConfig.java index f35ed0b..27c66c0 100644 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/auth/SecurityConfig.java +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/auth/SecurityConfig.java @@ -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 { @@ -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(); diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/dto/TokenAndUserIdResponse.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/dto/TokenAndUserIdResponse.java new file mode 100644 index 0000000..dafbba9 --- /dev/null +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/dto/TokenAndUserIdResponse.java @@ -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); + } +} + + diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/dto/TokenResponse.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/dto/TokenResponse.java deleted file mode 100644 index 08ff274..0000000 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/common/jwt/dto/TokenResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.sopt.springFirstSeminar.common.jwt.dto; - -public record TokenResponse( - String accessToken, - String refreshToken, - Long userId -) { - - public static TokenResponse of( - String accessToken, - String refreshToken, - Long userId - ) { - return new TokenResponse(accessToken, refreshToken, userId); - } -} - - diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/controller/MemberController.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/controller/MemberController.java index 4395f0f..980b8d6 100644 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/controller/MemberController.java +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/controller/MemberController.java @@ -2,23 +2,23 @@ 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") @@ -26,10 +26,10 @@ public class MemberController { private final MemberService memberService; - @PostMapping + @PostMapping("signup") public ResponseEntity> postMember(@RequestBody MemberCreateDTO memberCreate) { - final TokenResponse memberJoinResponse = memberService.createMember(memberCreate); + final TokenAndUserIdResponse memberJoinResponse = memberService.createMember(memberCreate); return ApiResponseUtil.success(SuccessMessage.MEMBER_CREATE_SUCCESS, memberJoinResponse); } @@ -42,6 +42,15 @@ public ResponseEntity> findMemberById(@MemberId final Long membe return ApiResponseUtil.success(SuccessMessage.MEMBER_FIND_SUCCESS, memberFindDTO); } + @PostMapping("reissue") + public ResponseEntity> 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); diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/domain/Member.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/domain/Member.java index a4e3489..73fcc7c 100644 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/domain/Member.java +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/domain/Member.java @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.sopt.springFirstSeminar.service.dto.MemberFindDTO; @Entity @Getter @@ -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()); + } } diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/service/MemberService.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/service/MemberService.java index a96efb3..823f2d5 100644 --- a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/service/MemberService.java +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/service/MemberService.java @@ -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; @@ -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()) @@ -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( diff --git a/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/service/dto/ReissueRequest.java b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/service/dto/ReissueRequest.java new file mode 100644 index 0000000..f5e3e55 --- /dev/null +++ b/springFirstSeminar/src/main/java/org/sopt/springFirstSeminar/service/dto/ReissueRequest.java @@ -0,0 +1,7 @@ +package org.sopt.springFirstSeminar.service.dto; + +public record ReissueRequest( + Long memberId +) { + +}