Skip to content
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 구현 #97

Merged
merged 1 commit into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// reids
implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE'

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/example/tree/TreeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;

@SpringBootApplication
@EnableJpaAuditing
@EnableRedisRepositories
public class TreeApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
@RequestMapping("/users")
public class MemberController {
private final MemberService memberService;


@PostMapping("/checkId")
@Operation(summary = "아이디 중복 체크", description = "서비스에서 사용할 고유 ID를 중복 체크합니다.")
public ApiResponse<MemberResponseDTO.checkName> checkName(
Expand All @@ -35,4 +37,11 @@ public ApiResponse<MemberResponseDTO.reissue> reissue(
) {
return ApiResponse.onSuccess(memberService.reissue(request));
}
@PostMapping("/login-tmp")
@Operation(summary = "로그인 임시", description = "로그인 임시.")
public ApiResponse<MemberResponseDTO.registerMember> loginTemp(
@RequestBody final MemberRequestDTO.loginMember request
){
return ApiResponse.onSuccess((memberService.login(request)));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.example.tree.domain.member.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -18,8 +20,16 @@ public static class registerMember {
private String userName;
}

@Getter
public static class loginMember {
private String phoneNumber;
}

@Getter
public static class reissue {

@NotNull
@NotBlank
private String refreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.example.tree.domain.member.entity.redis;

import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

import java.time.LocalDateTime;

// 테스트를 위해 짧게 하기도 해야함
// 60 * 60 * 24 * 14 <- 2주
@RedisHash(value = "refreshToken_Treehouse", timeToLive = 60 * 8)
@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshToken {

@Id
private Long memberId;

@Indexed
private String refreshToken;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, String> {
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByPhone(String phone);

Optional<Member> findByUserName(String userName);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
import lombok.extern.slf4j.Slf4j;
import org.example.tree.domain.member.entity.Member;
import org.example.tree.domain.member.entity.MemberRole;
import org.example.tree.domain.member.entity.redis.RefreshToken;
import org.example.tree.domain.member.repository.MemberRepository;
import org.example.tree.domain.member.repository.RefreshTokenRepository;
import org.example.tree.global.exception.GeneralException;
import org.example.tree.global.exception.GlobalErrorCode;
import org.example.tree.global.security.jwt.RefreshToken;
import org.example.tree.global.redis.service.RedisService;
import org.example.tree.global.security.provider.TokenProvider;
import org.example.tree.global.security.jwt.dto.TokenDTO;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
Expand All @@ -21,53 +19,32 @@
@RequiredArgsConstructor
public class MemberCommandService {
private final MemberRepository memberRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final RedisService redisService;
private final TokenProvider tokenProvider;




public Member register(Member member) {
try {
return memberRepository.save(member);
}
catch (Exception e){
log.error("eerror");
return null;
}
return memberRepository.save(member);
}
public TokenDTO login(Member member) {

String accessToken = tokenProvider.createAccessToken(String.valueOf(member.getId()), List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name())));
String rawToken = tokenProvider.createRefreshToken(String.valueOf(member.getId()));
RefreshToken refreshToken = RefreshToken.builder()
.memberId(member.getId())
.token(rawToken)
.build();
refreshTokenRepository.save(refreshToken);
String accessToken = tokenProvider.createAccessToken(member, List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name())));
String refreshToken = redisService.generateRefreshToken(member).getRefreshToken();
return TokenDTO.builder()
.accessToken(accessToken)
.refreshToken(refreshToken.getToken())
.refreshToken(refreshToken)
.build();
}

public TokenDTO reissue(Member member) {
RefreshToken invalidToken = refreshTokenRepository.findByMemberId(member.getId())
.orElseThrow(() -> new GeneralException(GlobalErrorCode.REFRESH_TOKEN_NOT_FOUND));
refreshTokenRepository.delete(invalidToken);
String accessToken = tokenProvider.createAccessToken(String.valueOf(member.getId()),List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name())));
String rawToken = tokenProvider.createRefreshToken(String.valueOf(member.getId()));
RefreshToken refreshToken = RefreshToken.builder()
.memberId(member.getId())
.token(rawToken)
.build();
refreshTokenRepository.save(refreshToken);
public TokenDTO reissueToken(Member member, RefreshToken refreshToken) {

String accessToken = tokenProvider.createAccessToken(member, List.of(new SimpleGrantedAuthority(MemberRole.ROLE_USER.name())));
String newRefreshToken = redisService.reGenerateRefreshToken(member, refreshToken).getRefreshToken();
return TokenDTO.builder()
.accessToken(accessToken)
.refreshToken(refreshToken.getToken())
.refreshToken(newRefreshToken)
.build();
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,29 @@
import org.example.tree.global.exception.GlobalErrorCode;
import org.example.tree.global.security.provider.TokenProvider;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
@Transactional
public class MemberQueryService {
private final MemberRepository memberRepository;
private final TokenProvider jwtTokenProvider;

public Optional<Member> checkName(String userName) {
return memberRepository.findByUserName(userName);
}
public Member findById(Long id) {
return memberRepository.findById(String.valueOf(id))
return memberRepository.findById(id)
.orElseThrow(()->new GeneralException(GlobalErrorCode.MEMBER_NOT_FOUND));
}

public Optional<Member> findByPhoneNumber(String phone) {
return memberRepository.findByPhone(phone);
public Boolean existById(Long id){
return memberRepository.existsById(id);
}

public Member findByToken(String token) {
// 토큰 검증
if (!jwtTokenProvider.validateToken(token)) {
throw new GeneralException(GlobalErrorCode.INVALID_TOKEN);
}

// 토큰을 사용하여 사용자 정보 가져오기
String memberId = jwtTokenProvider.getMemberIdFromToken(token);
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new GeneralException(GlobalErrorCode.MEMBER_NOT_FOUND));
return member;
public Optional<Member> findByPhoneNumber(String phone) {
return memberRepository.findByPhone(phone);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import org.example.tree.domain.member.dto.MemberRequestDTO;
import org.example.tree.domain.member.dto.MemberResponseDTO;
import org.example.tree.domain.member.entity.Member;
import org.example.tree.domain.member.entity.redis.RefreshToken;
import org.example.tree.global.exception.GeneralException;
import org.example.tree.global.exception.GlobalErrorCode;
import org.example.tree.global.redis.service.RedisService;
import org.example.tree.global.security.jwt.dto.TokenDTO;
import org.springframework.stereotype.Component;

Expand All @@ -18,6 +22,8 @@ public class MemberService {
private final MemberQueryService memberQueryService;
private final MemberConverter memberConverter;

private final RedisService redisService;

@Transactional
public MemberResponseDTO.checkName checkName(MemberRequestDTO.checkName request) {
Optional<Member> optionalMember = memberQueryService.checkName(request.getUserName());
Expand All @@ -32,10 +38,18 @@ public MemberResponseDTO.registerMember register(MemberRequestDTO.registerMember
return memberConverter.toRegister(savedMember, savedToken.getAccessToken(), savedToken.getRefreshToken());
}

@Transactional
public MemberResponseDTO.registerMember login(MemberRequestDTO.loginMember request) {
Member member = memberQueryService.findByPhoneNumber(request.getPhoneNumber()).orElseThrow(() -> new GeneralException(GlobalErrorCode.MEMBER_NOT_FOUND));
TokenDTO savedToken = memberCommandService.login(member);
return memberConverter.toRegister(member, savedToken.getAccessToken(), savedToken.getRefreshToken());
}

@Transactional
public MemberResponseDTO.reissue reissue(MemberRequestDTO.reissue request) {
Member member = memberQueryService.findByToken(request.getRefreshToken());
TokenDTO token = memberCommandService.reissue(member);
RefreshToken refreshToken = redisService.findRefreshToken(request.getRefreshToken()).orElseThrow(() -> new GeneralException(GlobalErrorCode.REFRESH_TOKEN_EXPIRED));
Member member = memberQueryService.findById(refreshToken.getMemberId());
TokenDTO token = memberCommandService.reissueToken(member,refreshToken);
return memberConverter.toReissue(token.getAccessToken(), token.getRefreshToken());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ public class RootController {
public String healthCheck(){
return "I'm healty!";
}

@GetMapping("/test")
@Operation(summary = "access Token 유효시간, 인증여부 테스트", description = "테스트용.")
public String testAccessToken(){return "test";}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ private ResponseEntity<Object> handleExceptionInternal(

ApiResponse<Object> body =
ApiResponse.onFailure(errorCode, null);
// e.printStackTrace();
e.printStackTrace();

WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public enum GlobalErrorCode {

// 401 Unauthorized - 권한 없음
INVALID_TOKEN(UNAUTHORIZED, "토큰이 유효하지 않습니다."),
INVALID_REFRESH_TOKEN(UNAUTHORIZED, "리프레시 토큰이 유효하지 않습니다."),
LOGIN_REQUIRED(UNAUTHORIZED, "로그인이 필요한 서비스입니다."),
// 403 Forbidden - 인증 거부
AUTHENTICATION_DENIED(FORBIDDEN, "인증이 거부 되었습니다."),
Expand All @@ -31,6 +32,8 @@ public enum GlobalErrorCode {
NEED_AGREE_REQUIRE_TERMS(NOT_FOUND, "필수 약관에 동의해 주세요."),
MEMBER_NOT_FOUND(NOT_FOUND, "등록된 사용자 정보가 없습니다."),
REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "리프레시 토큰이 존재하지 않습니다."),
REFRESH_TOKEN_EXPIRED(NOT_FOUND, "리프레시 토큰이 만료 되었습니다."),
TOKEN_EXPIRED(NOT_FOUND, "토큰이 만료 되었습니다."),
// 409 CONFLICT : Resource 를 찾을 수 없음
DUPLICATE_PHONE_NUMBER(CONFLICT, "중복된 전화번호가 존재합니다."),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.example.tree.global.exception;

import lombok.Getter;
import org.springframework.security.core.AuthenticationException;

@Getter
public class JwtReissueException extends AuthenticationException {

private String accessToken;

private String refreshToken;

public JwtReissueException(GlobalErrorCode code, String accessToken, String refreshToken) {
super(code.name());
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.example.tree.global.redis.repository;

import org.example.tree.domain.member.entity.redis.RefreshToken;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface RefreshTokenRepository extends CrudRepository<RefreshToken,Long> {

Optional<RefreshToken> findByRefreshToken(String refreshToken);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.example.tree.global.redis.service;

import org.example.tree.domain.member.dto.MemberRequestDTO;
import org.example.tree.domain.member.entity.Member;
import org.example.tree.domain.member.entity.redis.RefreshToken;

import java.util.Optional;

public interface RedisService {

RefreshToken generateRefreshToken(Member member);
RefreshToken reGenerateRefreshToken(Member member,RefreshToken refreshToken);

Optional<RefreshToken> findRefreshToken(String refreshToken);

}
Loading
Loading