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

6주차 : 로그인기능 #82

Open
wants to merge 37 commits into
base: tmxhsk99
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9e8131c
test : JwtUtil.encode 테스트 작성
tmxhsk99 Aug 6, 2023
7668f36
feat : JwtUtil.encode 기능 작성
tmxhsk99 Aug 6, 2023
ed21b82
test : JwtUtil.encode 테스트 수정
tmxhsk99 Aug 6, 2023
bba4225
test : JwtUtil.decode 테스트 작성
tmxhsk99 Aug 6, 2023
fa88c9c
feat : JwtUtil.decode 기능 작성
tmxhsk99 Aug 6, 2023
8993888
test : JwtUtil.decode 테스트 수정
tmxhsk99 Aug 6, 2023
7792398
test : AuthenticationService login메서드 테스트 작성
tmxhsk99 Aug 7, 2023
6f34f14
chore : 누락된 테스트 어노테이션 추가
tmxhsk99 Aug 7, 2023
2e7d5e0
feat : 로그인 기능 작성
tmxhsk99 Aug 7, 2023
dd21842
chore : 가독성을 위한 라인정리
tmxhsk99 Aug 8, 2023
d94fdbf
test : null 체크 검증 제거
tmxhsk99 Aug 8, 2023
10a47bb
test : SessionController login 메서드 테스트 작성
tmxhsk99 Aug 8, 2023
267a067
feat : SessionController login 기능 작성
tmxhsk99 Aug 8, 2023
5be4f22
test : SessionController login 테스트 수정
tmxhsk99 Aug 8, 2023
9a97eef
test : AuthenticationService 성공케이스 테스트이외의 예외테스트 추가
tmxhsk99 Aug 9, 2023
b4b09c1
feat : AuthenticationService 존재하지않는 요청 시 처리 기능 추가
tmxhsk99 Aug 9, 2023
b569da8
chore : 테스트 시 authenticationService 생성이슈 수정
tmxhsk99 Aug 9, 2023
f5bb2b4
chore : authenticationService 픽스쳐 위치 각 해당 컨텍스트로 이동
tmxhsk99 Aug 10, 2023
b32fd9b
test : authenticationService 비밀번호 일치 에러 처리 테스트 추가
tmxhsk99 Aug 10, 2023
b515277
feat : authenticationService 비밀번호 일치 에러 처리 기능추가
tmxhsk99 Aug 10, 2023
d89b578
chore : 비밀번호 예외 테스트를 위한 전처리 추가
tmxhsk99 Aug 10, 2023
66aa167
refactor : TestHelper로 픽스처 리팩터링
tmxhsk99 Aug 11, 2023
ac22ea0
refactor : SessionControllerTest 픽스쳐 TestHelper 사용으로 변경
tmxhsk99 Aug 12, 2023
abefd61
test : AuthInterceptor 테스트 추가 및 TestHelper에 관련 픽스쳐 추가
tmxhsk99 Aug 12, 2023
019b774
feat : product 관련 인증 처리를 위한 AuthInterceptor 추가
tmxhsk99 Aug 12, 2023
1839623
feat : 인증 검증시 발생하는 커스텀 에러 추가
tmxhsk99 Aug 12, 2023
9fd2f24
feat : 커스텀 에러 추가에 따른 에러처리 추가
tmxhsk99 Aug 12, 2023
0e17692
feat : 인터셉터 적용 및 요청메서드 검증로직 추가
tmxhsk99 Aug 12, 2023
b65d560
chore : 명확하게 발생 Exception 타입으로 수정
tmxhsk99 Aug 12, 2023
a1cf15c
fix : interceptor Path 패턴 수정, 누락된 어노테이션 추가
tmxhsk99 Aug 12, 2023
fa862e8
test : ProductControllerTest 구조 수정 및 인증 테스트 추가
tmxhsk99 Aug 12, 2023
8f2991d
fix : 누락된 @CrossOrigin 어노테이션 추가
tmxhsk99 Aug 12, 2023
af8d6d6
fix : 누락된 ``@CrossOrigin` 어노테이션 추가
tmxhsk99 Aug 12, 2023
bf7c186
Merge remote-tracking branch 'origin/login-apply' into login-apply
tmxhsk99 Aug 12, 2023
d2350be
test : 유효하지 않은 유저 로그인 요청인 경우 테스트 추가
tmxhsk99 Aug 12, 2023
47aea1d
fix : 메서드 처리 분기문 수정
tmxhsk99 Aug 14, 2023
9f04b7a
refactor : 가독성을 위한 코드 리팩터링
tmxhsk99 Aug 14, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.codesoom.assignment.application;

import com.codesoom.assignment.domain.User;
import com.codesoom.assignment.domain.UserRepository;
import com.codesoom.assignment.dto.UserLoginData;
import com.codesoom.assignment.errors.InvalidLoginException;
import com.codesoom.assignment.errors.UserNotFoundException;
import com.codesoom.assignment.utils.JwtUtil;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationService {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;

public AuthenticationService(UserRepository userRepository, JwtUtil jwtUtil) {
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
}

public String login(UserLoginData loginData) {
User loginUser = userRepository.findByEmail(loginData.getEmail())
.orElseThrow(() -> new UserNotFoundException(loginData.getEmail()));

if (!loginUser.getPassword().equals(loginData.getPassword())) {
throw new InvalidLoginException("Check your password");
}
return jwtUtil.encode(loginUser.getId());
}
}
22 changes: 22 additions & 0 deletions app/src/main/java/com/codesoom/assignment/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.codesoom.assignment.config;

import com.codesoom.assignment.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;

public WebConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/products/**");
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.codesoom.assignment.controllers;

import com.codesoom.assignment.dto.ErrorResponse;
import com.codesoom.assignment.errors.ProductNotFoundException;
import com.codesoom.assignment.errors.UserEmailDuplicationException;
import com.codesoom.assignment.errors.UserNotFoundException;
import com.codesoom.assignment.errors.*;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand All @@ -30,4 +28,22 @@ public ErrorResponse handleUserNotFound() {
public ErrorResponse handleUserEmailIsAlreadyExisted() {
return new ErrorResponse("User's email address is already existed");
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(InvalidLoginException.class)
public ErrorResponse handleInvalidLogin() {
return new ErrorResponse("Invalid login request");
}

@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(AccessTokenNotFoundException.class)
public ErrorResponse handleAccessTokenNotFound() {
return new ErrorResponse("Access token not found");
}

@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(InvalidAccessTokenException.class)
public ErrorResponse handleInvalidAccessToken() {
return new ErrorResponse("Invalid access token");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.codesoom.assignment.controllers;

import com.codesoom.assignment.application.AuthenticationService;
import com.codesoom.assignment.dto.SessionResponse;
import com.codesoom.assignment.dto.UserLoginData;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/session")
@CrossOrigin
public class SessionController {

private final AuthenticationService authenticationService;

public SessionController(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public SessionResponse login(@RequestBody @Valid UserLoginData userLoginData) {
String accessToken = authenticationService.login(userLoginData);

return SessionResponse.builder()
.accessToken(accessToken)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface UserRepository {
Optional<User> findByIdAndDeletedIsFalse(Long id);

Optional<User> findByEmail(String email);

void deleteAll();
}
13 changes: 13 additions & 0 deletions app/src/main/java/com/codesoom/assignment/dto/SessionResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.codesoom.assignment.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@AllArgsConstructor
public class SessionResponse {

private String accessToken;
}
26 changes: 26 additions & 0 deletions app/src/main/java/com/codesoom/assignment/dto/UserLoginData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.codesoom.assignment.dto;

import com.github.dozermapper.core.Mapping;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginData {
@NotBlank
@Size(min = 3)
@Mapping("email")
private String email;

@NotBlank
@Size(min = 4, max = 1024)
@Mapping("password")
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.codesoom.assignment.errors;

public class AccessTokenNotFoundException extends RuntimeException {

public AccessTokenNotFoundException() {
super("Access token not found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.codesoom.assignment.errors;

public class InvalidAccessTokenException extends RuntimeException {

public InvalidAccessTokenException(String message) {
super("Invalid access token : " + message);
}

public InvalidAccessTokenException() {
super("Invalid access token");
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.codesoom.assignment.errors;

public class InvalidLoginException extends RuntimeException {
public InvalidLoginException(String message) {
super("Invalid login Exception: " + message);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found: " + id);
}

public UserNotFoundException(String email) {
super("User not found: " + email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public interface JpaUserRepository
Optional<User> findByIdAndDeletedIsFalse(Long id);

Optional<User> findByEmail(String email);

void deleteAll();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.codesoom.assignment.interceptor;

import com.codesoom.assignment.errors.AccessTokenNotFoundException;
import com.codesoom.assignment.errors.InvalidAccessTokenException;
import com.codesoom.assignment.utils.JwtUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class AuthInterceptor implements HandlerInterceptor {

private final JwtUtil jwtUtil;

public AuthInterceptor(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

if (isGetMethod(request)) {
return true;
}

if (isPostMethod(request) || isPatchMethod(request) || isDeleteMethod(request)) {
return checkAccessToken(request);
}

return true;
}

private boolean isGetMethod(HttpServletRequest request) {
return request.getMethod().equals("GET");
}

private boolean isPostMethod(HttpServletRequest request) {
return request.getMethod().equals("POST");
}

private boolean isPatchMethod(HttpServletRequest request) {
return request.getMethod().equals("PATCH");
}

private boolean isDeleteMethod(HttpServletRequest request) {
return request.getMethod().equals("DELETE");
}

private boolean checkAccessToken(HttpServletRequest request) throws InvalidAccessTokenException, AccessTokenNotFoundException {
String authorization = request.getHeader("Authorization");
if (authorization == null) {
throw new AccessTokenNotFoundException();
}
String accessToken = authorization.substring("Bearer ".length());
try {
jwtUtil.decode(accessToken);
return true;
} catch (Exception e) {
throw new InvalidAccessTokenException(e.getMessage());
}
}

}
33 changes: 33 additions & 0 deletions app/src/main/java/com/codesoom/assignment/utils/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.codesoom.assignment.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;

@Component
public class JwtUtil {
private final Key key;

public JwtUtil(@Value("${jwt.secret}") String secret) {
this.key = Keys.hmacShaKeyFor(secret.getBytes());
}

public String encode(Long userId) {
return Jwts.builder()
.claim("userId", userId)
.signWith(key)
.compact();
}

public Claims decode(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.codesoom.assignment.application;

import com.codesoom.assignment.domain.User;
import com.codesoom.assignment.dto.UserLoginData;
import com.codesoom.assignment.errors.InvalidLoginException;
import com.codesoom.assignment.errors.UserNotFoundException;
import com.codesoom.assignment.utils.TestHelper;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.*;

import static com.codesoom.assignment.utils.TestHelper.*;


@SuppressWarnings({"InnerClassMayBeStatic", "NonAsciiCharacters"})
@DisplayName("AuthenticationService 클래스")
class AuthenticationServiceTest extends JpaTest {



@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class login_메서드는 {
private AuthenticationService authenticationService = new AuthenticationService(getUserRepository(), getJwtUtil());


@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class 유효한_유저로그인정보_요청를_받으면 {
private UserLoginData AUTH_USER_DATA = UserLoginData.builder()
.email(AUTH_EMAIL)
.password(AUTH_PASSWORD)
.build();

@BeforeEach
void setUp() {
getUserRepository().deleteAll();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실제 db를 지우지 않도록 조심해야겠네요.

저는 테스트를 실행할 때는 Profile을 test로 하게 하고, 설정에서 테스트 DB를 사용하도록 했어요.

// build.gradle.kts
tasks.withType<Test> {
    useJUnitPlatform()
    environment(mapOf("SPRING_PROFILES_ACTIVE" to "test"))
}
spring:
  datasource: ...

---

spring:
  profiles: test
  datasource:
    url: jdbc:mariadb://localhost:3306/codesoomtest
    username: root
    password: root1234
    driver-class-name: org.mariadb.jdbc.Driver

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

H2를 보통 테스트 환경에서 쓰다 보니까
인지 못했네요 실제 회사에서는
신경써서 하겠습니다!

getUserRepository().save(AUTH_USER);
}

@DisplayName("인증토큰을 반환한다.")
@Test
void It_returns_token() {
String accessToken = authenticationService.login(AUTH_USER_DATA);
Assertions.assertThat(accessToken).isEqualTo(VALID_TOKEN);
}

}

@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class 유효하지_않은_로그인정보를_받으면 {

@BeforeEach
void setUp() {
getUserRepository().deleteAll();
getUserRepository().save(AUTH_USER);
}

@DisplayName("해당 정보의 회원이 존재하지 않으면 UserNotFoundException을 반환한다.")
@Test
void It_throws_UserNotFoundException() {
Assertions.assertThatThrownBy(() -> authenticationService.login(IS_NOT_EXISTS_USER_DATA)).isInstanceOf(UserNotFoundException.class);
}

@DisplayName("비밀번호가 일치하지 않으면 InvalidLoginException을 반환한다.")
@Test
void It_throws_InvalidLoginRequest() {
Assertions.assertThatThrownBy(() -> authenticationService.login(INVALID_PASSWORD_USER_DATA)).isInstanceOf(InvalidLoginException.class);
}
}

}
}
Loading