Skip to content

Commit

Permalink
Merge pull request #81 from duohui12/duohui12
Browse files Browse the repository at this point in the history
6주차 과제 - 로그인 만들기
  • Loading branch information
hannut91 authored Aug 8, 2023
2 parents 1e8cd46 + cdf2cc4 commit 83324d9
Show file tree
Hide file tree
Showing 19 changed files with 631 additions and 15,804 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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.InvalidAccessTokenException;
import com.codesoom.assignment.errors.LoginFailException;
import com.codesoom.assignment.errors.UserNotFoundException;
import com.codesoom.assignment.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureException;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationService {

private final JwtUtil jwtUtil;
private final UserRepository userRepository;

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

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

if(!user.getPassword().equals(userLoginData.getPassword())){
throw new LoginFailException();
}

return jwtUtil.encode(user.getId());
}

public Long parseToken(String accessToken) {
if(accessToken == null || accessToken.isBlank()){
throw new InvalidAccessTokenException();
}

try{
Claims claims = jwtUtil.decode(accessToken);
return claims.get("userId", Long.class);
}catch (SignatureException e){
throw new InvalidAccessTokenException();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ public User deleteUser(Long id) {

private User findUser(Long id) {
return userRepository.findByIdAndDeletedIsFalse(id)
.orElseThrow(() -> new UserNotFoundException(id));
.orElseThrow(() -> new UserNotFoundException());
}
}
21 changes: 21 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,21 @@
package com.codesoom.assignment.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

private HandlerInterceptor handlerInterceptor;

public WebConfig(HandlerInterceptor handlerInterceptor) {
this.handlerInterceptor = handlerInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(handlerInterceptor);
}
}
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 @@ -15,19 +13,31 @@
public class ControllerErrorAdvice {
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(ProductNotFoundException.class)
public ErrorResponse handleProductNotFound() {
return new ErrorResponse("Product not found");
public ErrorResponse handleProductNotFound(ProductNotFoundException exception) {
return new ErrorResponse(exception.getMessage());
}

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(UserNotFoundException.class)
public ErrorResponse handleUserNotFound() {
return new ErrorResponse("User not found");
public ErrorResponse handleUserNotFound(UserNotFoundException exception) {
return new ErrorResponse(exception.getMessage());
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(UserEmailDuplicationException.class)
public ErrorResponse handleUserEmailIsAlreadyExisted() {
return new ErrorResponse("User's email address is already existed");
public ErrorResponse handleUserEmailIsAlreadyExisted(UserEmailDuplicationException exception) {
return new ErrorResponse(exception.getMessage());
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(LoginFailException.class)
public ErrorResponse handleLoginFail(LoginFailException exception) {
return new ErrorResponse(exception.getMessage());
}

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

import com.codesoom.assignment.application.AuthenticationService;
import com.codesoom.assignment.dto.SessionResponseData;
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 SessionResponseData login(@RequestBody @Valid UserLoginData userLoginData){
String accessToken = authenticationService.login(userLoginData);

return SessionResponseData.builder()
.accessToken(accessToken)
.build();
}
}
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 SessionResponseData {

private String accessToken;
}
23 changes: 23 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,23 @@
package com.codesoom.assignment.dto;

import com.github.dozermapper.core.Mapping;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.checkerframework.checker.units.qual.A;

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

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginData {
@NotBlank
private String email;

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

public class InvalidAccessTokenException extends RuntimeException{
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 LoginFailException extends RuntimeException{
public LoginFailException() {
super("Login Fail");
}

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

public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found: " + id);
public UserNotFoundException() {
super("User not found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.codesoom.assignment.interceptors;

import com.codesoom.assignment.application.AuthenticationService;
import com.codesoom.assignment.dto.ErrorResponse;
import com.codesoom.assignment.errors.InvalidAccessTokenException;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

private final AuthenticationService authenticationService;

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

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String requestMethod = request.getMethod();

/*
e2e 테스트시
... blocked by CORS policy:
Response to preflight request doesn't pass access control check:
It does not have HTTP ok status. 오류 발생해서 추가
*/
if (requestMethod.equals(HttpMethod.OPTIONS.name())) {
return true;
}

if(!requestURI.startsWith("/products")){
return true;
}

if(requestMethod.equals(HttpMethod.GET.name())){
return true;
}

String authorization = request.getHeader("Authorization");
if(authorization==null) throw new InvalidAccessTokenException();
String accessToken = authorization.substring("Bearer ".length());

Long userId = authenticationService.parseToken(accessToken);
return true;
}
}
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){
key = Keys.hmacShaKeyFor(secret.getBytes());
}

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

public Claims decode(String accessToken){
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.getBody();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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.InvalidAccessTokenException;
import com.codesoom.assignment.errors.LoginFailException;
import com.codesoom.assignment.utils.JwtUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

class AuthenticationServiceTest {

private static final String SECRET = "12345678901234567890123456789010";
private AuthenticationService authenticationService;
private final UserRepository userRepository = mock(UserRepository.class);
private final JwtUtil jwtUtil = new JwtUtil(SECRET);

private static final String VALID_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.neCsyNLzy3lQ4o2yliotWT06FwSGZagaHpKdAkjnGGw";
private static final String INVALID_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.neCsyNLzy3lQ4o2yliotWT06FwSGZagaHpKdAkjnGKK";
public static final String VALID_EMAIL = "[email protected]";
private static final String VALID_PASSWORD = "1111";
public static final String INVALID_EMAIL = "dh";
private static final String INVALID_PASSWORD = "0";
private static final UserLoginData VALID_LOGIN_DATA = UserLoginData.builder()
.email(VALID_EMAIL)
.password(VALID_PASSWORD)
.build();
private static final UserLoginData INVALID_LOGIN_DATA = UserLoginData.builder()
.email(INVALID_EMAIL)
.password(INVALID_PASSWORD)
.build();

@BeforeEach
void setup(){
authenticationService = new AuthenticationService(jwtUtil,userRepository);
}

@Test
void loginWithValidLoginData(){
given(userRepository.findByEmail(VALID_EMAIL))
.willReturn(Optional.of( User.builder()
.id(1L)
.email(VALID_EMAIL)
.password(VALID_PASSWORD)
.build()));

String accessToken = authenticationService.login(VALID_LOGIN_DATA);

assertThat(accessToken).isEqualTo(VALID_TOKEN);
verify(userRepository).findByEmail(VALID_EMAIL);
}

@Test
void loginWithInvalidLoginData(){
given(userRepository.findByEmail(INVALID_EMAIL))
.willThrow(new LoginFailException());

assertThatThrownBy(() -> authenticationService.login(INVALID_LOGIN_DATA))
.isInstanceOf(LoginFailException.class);
}

@Test
void parseTokenWithValidToken() {
Long id = authenticationService.parseToken(VALID_TOKEN);

assertThat(id).isEqualTo(1L);
}

@Test
void parseTokenWithInvalidToken() {
assertThatThrownBy(() -> authenticationService.parseToken(""))
.isInstanceOf(InvalidAccessTokenException.class);

assertThatThrownBy(() -> authenticationService.parseToken(INVALID_TOKEN))
.isInstanceOf(InvalidAccessTokenException.class);
}
}
Loading

0 comments on commit 83324d9

Please sign in to comment.