본문 바로가기

프로젝트/멋사 개인 프로젝트 (mutsa-SNS)

[05] mutsa-SNS 2일차 - (2) 로그인 기능

로그인시 JWT token을 부여하는 기능을 구현하였다.

 

JWT 란?
JWT (JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 JSON형태의 토큰이다.

jwt token 구성 요소

위의 형태로 구성되어 있으며, header에는 JWT 에서 사용할 타입과 해시 알고리즘의 종류가 담겨 있으며, payload에는 사용자 권한과 정보, signature에는 header, payload 를 Base64 URL-safe Encode 를 한 이후 header 에 명시된 해시함수를 적용하고, 개인키(Private Key)로 서명한 전자서명이 담겨있다.

header와 payload는 단순히 인코딩된 값이기 때문에 제 3자가 복호화 및 조작할 수 있지만, signature는 서버 측에서 관리하는 비밀키가 유출되지 않는 이상 복호화할 수 없기 때문에, 토큰의 위변조 여부를 확인하는데 사용된다.

 

<reference>

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC#JWT_%EA%B5%AC%EC%A1%B0

 

[WEB] 📚 JWT 토큰 인증 이란? - 💯 이해하기 쉽게 정리

인증 방식 종류 (Cookie & Session & Token) 보통 서버가 클라이언트 인증을 확인하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다. JWT를 배우기 앞서 우선 쿠키와 세션의 통신 방식을 복습해

inpa.tistory.com

1. Service

UserService

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;

    @Value("${jwt.token.secret}") // 환경변수내 정보 이용
    private String secretKey;

    private long expireTimeMs = 1000 * 60 * 60; //만료시간 1h

    public UserDto join(UserJoinRequest req) {

        //중복 userName시 error
        userRepository.findByUserName(req.getUserName())
                .ifPresent(user -> {
                    //throw new AppException(ErrorCode.DUPLICATED_USER_NAME, String.format("userName : %s는 이미 있습니다.", req.getUserName()));
                    throw new AppException(ErrorCode.DUPLICATED_USER_NAME, "");
                });

        //UserRole index
        String role = "USER";

        //db에 유저가 아무도 없을 때, 새로 생성되는 계정을 admin으로
        if (userRepository.count() == 0) {
            role = "ADMIN";
        }

        Timestamp registeredAt = new Timestamp(System.currentTimeMillis());
        //한국시간 utc + 9h (timestamp + 32400000) 해줘야 하는가
        //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");

        User savedUser = userRepository.save(req.toEntity(encoder.encode(req.getPassword()), role, registeredAt));

        return UserDto.builder()
                .id(savedUser.getId())
                .userName(savedUser.getUserName())
                .role(savedUser.getRole())
                .build();
    }

    public String login(String userName, String password) {

        //userName이 존재하는가
        User user = userRepository.findByUserName(userName)
                .orElseThrow(()-> new AppException(ErrorCode.NOT_FOUNDED_USER_NAME, ""));

        //password 일치하는가
        if(!encoder.matches(password, user.getPassword())) {
            throw new AppException(ErrorCode.INVALID_PASSWORD ,"");
        }
        
        //위의 예외 발생하지 않으면 로그인 성공. token 발행

        return JwtTokenUtil.createToken(userName, secretKey, expireTimeMs);

    }

}

로그인시 controller에서 호출하는 business logic이다. 앞서 작성한 join기능에 이어 login기능을 추가하였다.

UserRepository에서 userName을 통해 해당되는 행의 정보를 찾는 method를 호출하여, 만약 DB에 이미 userName이 없다면, exception을 throw한다.

혹은 입력한 password와 UserRepository에서 userName을 통해 찾은 행의 password와 일치하지 않을 시, exception을 throw한다.

위의 exception이 나지 않았다면, JWT (JSON Web Token)형식의 token를 발행하여 controller에 return하여 넘겨준다. 실제 scretKey는 environment variable에 저장하였다.

 

2. Controller

UserController

@RestController
@RequestMapping("/api/v1/users/")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/join")
    public Response<UserJoinResponse> join(@RequestBody UserJoinRequest req) {
        UserDto userDto = userService.join(req);
        return Response.success(new UserJoinResponse(userDto.getId(), userDto.getUserName()));
    }

    @PostMapping("/login")
    public Response<UserLoginResponse> login(@RequestBody UserLoginRequest req) {
        String token = userService.login(req.getUserName(), req.getPassword());
        return Response.success(new UserLoginResponse(token));
    }

}

앞서 1일차에 작성한 controller에 login기능을 추가하였다.

service에서 business logic을 진행 할 때, exception이 발생하지 않았을 경우, Response.success를 통해 해당 Response를 view에 전송해준다.

 

3. TestCode

UserControllerTest

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    UserService userService;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    @DisplayName("회원가입 성공")
    @WithMockUser
    void join_success() throws Exception{
        UserJoinRequest userJoinRequest = UserJoinRequest.builder()
                .userName("haneul")
                .password("1234")
                .build();

        when(userService.join(any())).thenReturn(mock(UserDto.class));

        mockMvc.perform(post("/api/v1/users/join")
                .with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsBytes(userJoinRequest)))
                .andDo(print())
                .andExpect(status().isOk());

    }

    @Test
    @DisplayName("회원가입 실패 - userName 중복")
    @WithMockUser
    void join_fail() throws Exception{
        UserJoinRequest userJoinRequest = UserJoinRequest.builder()
                .userName("haneul")
                .password("1234")
                .build();

        when(userService.join(any()))
                .thenThrow(new AppException(ErrorCode.DUPLICATED_USER_NAME, ""));

        mockMvc.perform(post("/api/v1/users/join")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsBytes(userJoinRequest)))
                .andDo(print())
                .andExpect(status().isConflict());
    }

    @Test
    @DisplayName("로그인 성공")
    @WithMockUser

    void login_success() throws Exception{
        UserLoginRequest userLoginRequest = UserLoginRequest.builder()
                .userName("haneul")
                .password("1234")
                .build();
        when(userService.login(any(), any()))
                .thenReturn("token");

        mockMvc.perform(post("/api/v1/users/login")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsBytes(userLoginRequest)))
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    @DisplayName("로그인 실패 - userName 없음")
    @WithMockUser

    void login_fail1() throws Exception {

        UserLoginRequest userLoginRequest = UserLoginRequest.builder()
                .userName("haneul")
                .password("1234")
                .build();

        when(userService.login(any(), any()))
                .thenThrow(new AppException(ErrorCode.NOT_FOUNDED_USER_NAME, ""));

        mockMvc.perform(post("/api/v1/users/login")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsBytes(userLoginRequest)))
                .andDo(print())
                .andExpect(status().isNotFound());
    }

    @Test
    @DisplayName("로그인 실패 - password 틀림")
    @WithMockUser

    void login_fail2() throws Exception {

        UserLoginRequest userLoginRequest = UserLoginRequest.builder()
                .userName("haneul")
                .password("1234")
                .build();

        when(userService.login(any(), any()))
                .thenThrow(new AppException(ErrorCode.INVALID_PASSWORD, ""));

        mockMvc.perform(post("/api/v1/users/login")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsBytes(userLoginRequest)))
                .andDo(print())
                .andExpect(status().isUnauthorized());

    }





}

로그인 성공/ 실패의 경우의 테스트 코드를 추가하였다.

실패의 경우는 userName이 중복일 경우와, password가 틀린 경우를 구분하였다.

 

<AWS 배포 URL>

http://ec2-3-35-225-29.ap-northeast-2.compute.amazonaws.com:8080/swagger-ui/

 

<전체 Refernece>

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC#JWT_%EA%B5%AC%EC%A1%B0

 

[WEB] 📚 JWT 토큰 인증 이란? - 💯 이해하기 쉽게 정리

인증 방식 종류 (Cookie & Session & Token) 보통 서버가 클라이언트 인증을 확인하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다. JWT를 배우기 앞서 우선 쿠키와 세션의 통신 방식을 복습해

inpa.tistory.com