From 3d27da6d3fe4fb62d9cef1c8a8d19540a4fae343 Mon Sep 17 00:00:00 2001 From: Willy Date: Mon, 3 Jun 2024 15:20:16 +0900 Subject: [PATCH] =?UTF-8?q?[6=EF=B8=8F=E2=83=A3Assignment/#6]=206=EC=B0=A8?= =?UTF-8?q?=20=EC=84=B8=EB=AF=B8=EB=82=98=20=EC=8B=A4=EC=8A=B5=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- practice/build.gradle | 1 + .../sopt/practice/auth/AuthController.java | 38 ++++++++++++ .../org/sopt/practice/auth/AuthService.java | 62 +++++++++++++++++++ .../sopt/practice/auth/SecurityConfig.java | 10 ++- .../practice/common/jwt/JwtTokenProvider.java | 6 +- .../practice/controller/MemberController.java | 3 +- .../java/org/sopt/practice/domain/Member.java | 14 +++-- .../practice/dto/request/LoginRequest.java | 7 +++ .../dto/request/MemberCreateRequest.java | 3 +- .../dto/request/RefreshTokenRequest.java | 6 ++ .../practice/dto/response/LoginResponse.java | 13 ++++ .../dto/response/MemberFindAllDto.java | 2 +- .../practice/dto/response/MemberFindDto.java | 2 +- .../dto/response/UserJoinResponse.java | 4 +- .../exception/enums/ErrorMessage.java | 2 + .../sopt/practice/external/SwaggerConfig.java | 21 +++++++ .../practice/repository/MemberRepository.java | 2 +- .../sopt/practice/service/MemberService.java | 17 ++++- 18 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 practice/src/main/java/org/sopt/practice/auth/AuthController.java create mode 100644 practice/src/main/java/org/sopt/practice/auth/AuthService.java create mode 100644 practice/src/main/java/org/sopt/practice/dto/request/LoginRequest.java create mode 100644 practice/src/main/java/org/sopt/practice/dto/request/RefreshTokenRequest.java create mode 100644 practice/src/main/java/org/sopt/practice/dto/response/LoginResponse.java create mode 100644 practice/src/main/java/org/sopt/practice/external/SwaggerConfig.java diff --git a/practice/build.gradle b/practice/build.gradle index 2f61bb8..50379c9 100644 --- a/practice/build.gradle +++ b/practice/build.gradle @@ -23,6 +23,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/practice/src/main/java/org/sopt/practice/auth/AuthController.java b/practice/src/main/java/org/sopt/practice/auth/AuthController.java new file mode 100644 index 0000000..ef56f45 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/AuthController.java @@ -0,0 +1,38 @@ +package org.sopt.practice.auth; + +import lombok.RequiredArgsConstructor; +import org.sopt.practice.dto.request.LoginRequest; +import org.sopt.practice.dto.request.RefreshTokenRequest; +import org.sopt.practice.dto.response.LoginResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth") +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public ResponseEntity login( + @RequestBody LoginRequest loginRequest + ){ + LoginResponse loginResponse = authService.login(loginRequest); + return ResponseEntity.status(HttpStatus.OK) + .body(loginResponse); + } + + @PostMapping("/refresh") + public ResponseEntity refresh( + @RequestBody RefreshTokenRequest refreshTokenRequest + ){ + LoginResponse loginResponse = authService.refreshAccessToken(refreshTokenRequest); + return ResponseEntity.status(HttpStatus.OK) + .body(loginResponse); + } +} diff --git a/practice/src/main/java/org/sopt/practice/auth/AuthService.java b/practice/src/main/java/org/sopt/practice/auth/AuthService.java new file mode 100644 index 0000000..2232153 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/auth/AuthService.java @@ -0,0 +1,62 @@ +package org.sopt.practice.auth; + +import lombok.RequiredArgsConstructor; +import org.sopt.practice.auth.redis.domain.Token; +import org.sopt.practice.auth.redis.repository.RedisTokenRepository; +import org.sopt.practice.common.jwt.JwtTokenProvider; +import org.sopt.practice.domain.Member; +import org.sopt.practice.dto.request.LoginRequest; +import org.sopt.practice.dto.request.RefreshTokenRequest; +import org.sopt.practice.dto.response.LoginResponse; +import org.sopt.practice.exception.NotFoundException; +import org.sopt.practice.exception.enums.ErrorMessage; +import org.sopt.practice.repository.MemberRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final JwtTokenProvider jwtTokenProvider; + private final RedisTokenRepository redisTokenRepository; + private final MemberRepository memberRepository; + + public LoginResponse login(LoginRequest loginRequest){ + // 사용자의 자격 증명을 확인하는 로직 + Member member = memberRepository.findByUsername(loginRequest.username()) + .orElseThrow(() -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND_BY_ID_EXCEPTION)); + + // 아이디와 비밀번호가 일치하는지 확인하는 로직 + if(!member.getPassword().equals(loginRequest.password())){ + throw new NotFoundException(ErrorMessage.INVALID_MEMBER_PASSWORD_EXCEPTION); + } + + Long memberId = member.getId(); + + String accessToken = jwtTokenProvider.issueAccessToken( + UserAuthentication.createUserAuthentication(memberId) + ); + + String refreshToken = jwtTokenProvider.issueRefreshToken( + UserAuthentication.createUserAuthentication(memberId) + ); + + //리프레시 토큰을 Redis에 저장 + redisTokenRepository.save(Token.of(memberId, refreshToken)); + + return LoginResponse.of(accessToken, refreshToken); + } + + public LoginResponse refreshAccessToken(RefreshTokenRequest refreshTokenRequest){ + String refreshToken = refreshTokenRequest.refreshToken(); + Token token = redisTokenRepository.findByRefreshToken(refreshToken) + .orElseThrow(() -> new NotFoundException(ErrorMessage.INVALID_REFRESH_TOKEN_EXCEPTION)); + + Long userId = token.getId(); + String newAccessToken = jwtTokenProvider.issueRefreshToken( + UserAuthentication.createUserAuthentication(userId) + ); + + return LoginResponse.of(newAccessToken, refreshToken); + } +} diff --git a/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java b/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java index 15fbd7b..34d0f94 100644 --- a/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java +++ b/practice/src/main/java/org/sopt/practice/auth/SecurityConfig.java @@ -22,7 +22,15 @@ public class SecurityConfig { private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; - private static final String[] AUTH_WHITE_LIST = {"/api/v1/member"}; + private static final String[] AUTH_WHITE_LIST = { + "/swagger-ui/**", + "/swagger-ui.html", + "/v3/api-docs/**", + "/api/v1/member", + "/api/v1/auth/login", + "/api/v1/auth/refresh", + "/api/v1/member/find-all", + }; @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { diff --git a/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java b/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java index 44f27e3..718e05a 100644 --- a/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java +++ b/practice/src/main/java/org/sopt/practice/common/jwt/JwtTokenProvider.java @@ -19,6 +19,7 @@ public class JwtTokenProvider { private static final String USER_ID = "userId"; private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; + private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; @Value("${jwt.secret}") private String JWT_SECRET; @@ -27,6 +28,10 @@ public String issueAccessToken(final Authentication authentication){ return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); } + public String issueRefreshToken(final Authentication authentication){ + return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME); + } + public String generateToken(Authentication authentication, Long tokenExpirationTime){ final Date now = new Date(); final Claims claims = Jwts.claims() @@ -74,5 +79,4 @@ public Long getUserFromJwt(String token){ Claims claims = getBody(token); return Long.valueOf(claims.get(USER_ID).toString()); } - } diff --git a/practice/src/main/java/org/sopt/practice/controller/MemberController.java b/practice/src/main/java/org/sopt/practice/controller/MemberController.java index dc9e160..82b8d66 100644 --- a/practice/src/main/java/org/sopt/practice/controller/MemberController.java +++ b/practice/src/main/java/org/sopt/practice/controller/MemberController.java @@ -11,7 +11,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.net.URI; import java.util.List; import java.util.stream.Collectors; @@ -47,7 +46,7 @@ public ResponseEntity deleteMemberById( return ResponseEntity.noContent().build(); } - @GetMapping("find-all") + @GetMapping("/find-all") public ResponseEntity> findAllMembers(){ List members = memberService.findAllMembers(); List memberFindAllDtoList = members.stream() diff --git a/practice/src/main/java/org/sopt/practice/domain/Member.java b/practice/src/main/java/org/sopt/practice/domain/Member.java index ea3ebf7..598ae21 100644 --- a/practice/src/main/java/org/sopt/practice/domain/Member.java +++ b/practice/src/main/java/org/sopt/practice/domain/Member.java @@ -15,7 +15,9 @@ public class Member { @GeneratedValue(strategy = GenerationType.IDENTITY) //기본키 생성을 데이터베이스에 위임 private Long id; - private String name; + private String username; // 유저 이름 + + private String password; // 유저 비밀번호 @Enumerated(EnumType.STRING) //Enum의 이름을 데이터베이스에 저장 private Part part; @@ -23,9 +25,10 @@ public class Member { private int age; // 정적 팩토리 메서드로 객체 생성 - public static Member create(String name, Part part, int age){ + public static Member create(String name, String password, Part part, int age){ return Member.builder() - .name(name) + .username(name) + .password(password) .part(part) .age(age) .build(); @@ -33,8 +36,9 @@ public static Member create(String name, Part part, int age){ // 빌더패턴으로 객체 생성 @Builder - private Member(final String name, final Part part, final int age){ - this.name = name; + private Member(final String username, String password,final Part part, final int age){ + this.username = username; + this.password = password; this.part = part; this.age = age; } diff --git a/practice/src/main/java/org/sopt/practice/dto/request/LoginRequest.java b/practice/src/main/java/org/sopt/practice/dto/request/LoginRequest.java new file mode 100644 index 0000000..ad37567 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/dto/request/LoginRequest.java @@ -0,0 +1,7 @@ +package org.sopt.practice.dto.request; + +public record LoginRequest( + String username, + String password +) { +} diff --git a/practice/src/main/java/org/sopt/practice/dto/request/MemberCreateRequest.java b/practice/src/main/java/org/sopt/practice/dto/request/MemberCreateRequest.java index 16859c8..0e0ca38 100644 --- a/practice/src/main/java/org/sopt/practice/dto/request/MemberCreateRequest.java +++ b/practice/src/main/java/org/sopt/practice/dto/request/MemberCreateRequest.java @@ -3,7 +3,8 @@ import org.sopt.practice.domain.enums.Part; public record MemberCreateRequest( - String name, + String username, + String password, Part part, int age ) { diff --git a/practice/src/main/java/org/sopt/practice/dto/request/RefreshTokenRequest.java b/practice/src/main/java/org/sopt/practice/dto/request/RefreshTokenRequest.java new file mode 100644 index 0000000..dfd3d60 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/dto/request/RefreshTokenRequest.java @@ -0,0 +1,6 @@ +package org.sopt.practice.dto.request; + +public record RefreshTokenRequest( + String refreshToken +) { +} diff --git a/practice/src/main/java/org/sopt/practice/dto/response/LoginResponse.java b/practice/src/main/java/org/sopt/practice/dto/response/LoginResponse.java new file mode 100644 index 0000000..93e460a --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/dto/response/LoginResponse.java @@ -0,0 +1,13 @@ +package org.sopt.practice.dto.response; + +public record LoginResponse( + String accessToken, + String refreshToken +) { + public static LoginResponse of( + String accessToken, + String refreshToken + ){ + return new LoginResponse(accessToken, refreshToken); + } +} diff --git a/practice/src/main/java/org/sopt/practice/dto/response/MemberFindAllDto.java b/practice/src/main/java/org/sopt/practice/dto/response/MemberFindAllDto.java index 3e1060d..32cfb19 100644 --- a/practice/src/main/java/org/sopt/practice/dto/response/MemberFindAllDto.java +++ b/practice/src/main/java/org/sopt/practice/dto/response/MemberFindAllDto.java @@ -12,6 +12,6 @@ public record MemberFindAllDto( public static MemberFindAllDto of( Member member ){ - return new MemberFindAllDto(member.getName(), member.getPart(), member.getAge()); + return new MemberFindAllDto(member.getUsername(), member.getPart(), member.getAge()); } } diff --git a/practice/src/main/java/org/sopt/practice/dto/response/MemberFindDto.java b/practice/src/main/java/org/sopt/practice/dto/response/MemberFindDto.java index 2cc0dba..8762a71 100644 --- a/practice/src/main/java/org/sopt/practice/dto/response/MemberFindDto.java +++ b/practice/src/main/java/org/sopt/practice/dto/response/MemberFindDto.java @@ -12,6 +12,6 @@ public record MemberFindDto( public static MemberFindDto of( Member member ){ - return new MemberFindDto(member.getName(), member.getPart(), member.getAge()); + return new MemberFindDto(member.getUsername(), member.getPart(), member.getAge()); } } diff --git a/practice/src/main/java/org/sopt/practice/dto/response/UserJoinResponse.java b/practice/src/main/java/org/sopt/practice/dto/response/UserJoinResponse.java index faf8a10..6f479c3 100644 --- a/practice/src/main/java/org/sopt/practice/dto/response/UserJoinResponse.java +++ b/practice/src/main/java/org/sopt/practice/dto/response/UserJoinResponse.java @@ -2,12 +2,14 @@ public record UserJoinResponse( String accessToken, + String refreshToken, String userId ) { public static UserJoinResponse of( String accessToken, + String refreshToken, String userId ){ - return new UserJoinResponse(accessToken, userId); + return new UserJoinResponse(accessToken, refreshToken, userId); } } diff --git a/practice/src/main/java/org/sopt/practice/exception/enums/ErrorMessage.java b/practice/src/main/java/org/sopt/practice/exception/enums/ErrorMessage.java index 3540203..7f36e8f 100644 --- a/practice/src/main/java/org/sopt/practice/exception/enums/ErrorMessage.java +++ b/practice/src/main/java/org/sopt/practice/exception/enums/ErrorMessage.java @@ -12,6 +12,8 @@ public enum ErrorMessage { MEMBER_NOT_OWNED_BLOG_EXCEPTION(HttpStatus.NOT_FOUND.value(), "블로그 소유자가 아닙니다."), POST_NOT_FOUND_BY_ID_EXCEPTION(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 게시글이 존재하지 않습니다."), JWT_UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패했습니다."), + INVALID_MEMBER_PASSWORD_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "비밀번호가 일치하지 않습니다."), + INVALID_REFRESH_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "Refresh 토큰이 존재하지 않습니다."), ; private final int status; diff --git a/practice/src/main/java/org/sopt/practice/external/SwaggerConfig.java b/practice/src/main/java/org/sopt/practice/external/SwaggerConfig.java new file mode 100644 index 0000000..e1a31c9 --- /dev/null +++ b/practice/src/main/java/org/sopt/practice/external/SwaggerConfig.java @@ -0,0 +1,21 @@ +package org.sopt.practice.external; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI(){ + Info info = new Info() + .title("NOW SOPT PRACTICE SWAGGER") + .description("NOW SOPT 세미나 & 실습 API 구현") + .version("v1"); + + return new OpenAPI() + .info(info); + } +} diff --git a/practice/src/main/java/org/sopt/practice/repository/MemberRepository.java b/practice/src/main/java/org/sopt/practice/repository/MemberRepository.java index ff6d71b..f6cc909 100644 --- a/practice/src/main/java/org/sopt/practice/repository/MemberRepository.java +++ b/practice/src/main/java/org/sopt/practice/repository/MemberRepository.java @@ -10,6 +10,6 @@ public interface MemberRepository extends Repository { Member save(Member member); Optional findById(Long memberId); void delete(Member member); - List findAll(); + Optional findByUsername(String username); } diff --git a/practice/src/main/java/org/sopt/practice/service/MemberService.java b/practice/src/main/java/org/sopt/practice/service/MemberService.java index 641d025..5527b75 100644 --- a/practice/src/main/java/org/sopt/practice/service/MemberService.java +++ b/practice/src/main/java/org/sopt/practice/service/MemberService.java @@ -3,6 +3,8 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.sopt.practice.auth.UserAuthentication; +import org.sopt.practice.auth.redis.domain.Token; +import org.sopt.practice.auth.redis.repository.RedisTokenRepository; import org.sopt.practice.common.jwt.JwtTokenProvider; import org.sopt.practice.domain.Member; import org.sopt.practice.dto.request.MemberCreateRequest; @@ -22,19 +24,30 @@ public class MemberService { private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; + private final RedisTokenRepository redisTokenRepository; @Transactional //변경사항을 DB에 반영 public UserJoinResponse createMember( MemberCreateRequest memberCreateRequest ){ Member member = memberRepository.save( - Member.create(memberCreateRequest.name(), memberCreateRequest.part(), memberCreateRequest.age()) + Member.create(memberCreateRequest.username(), memberCreateRequest.password() ,memberCreateRequest.part(), memberCreateRequest.age()) ); + Long memberId = member.getId(); + String accessToken = jwtTokenProvider.issueAccessToken( UserAuthentication.createUserAuthentication(memberId) ); - return UserJoinResponse.of(accessToken, memberId.toString()); + + String refreshToken = jwtTokenProvider.issueRefreshToken( + UserAuthentication.createUserAuthentication(memberId) + ); + + // RefreshToken을 Redis에 저장 + redisTokenRepository.save(Token.of(memberId, refreshToken)); + + return UserJoinResponse.of(accessToken, refreshToken, memberId.toString()); } public MemberFindDto findMemberById(