[개발일지] 회원가입/로그인 구현

이건희·2023년 12월 26일
0

Eyeve Project

목록 보기
3/10

서론

사실 회원가입/로그인은 4~5개월 전에 완성했지만 개강하고 학업이 바쁘고 하다보니 정리할 시간이 없었다... 그래서 종강 한 지금이라도 작성해본다..

모든 코드는 직접 작성하려고 노력해서 깔끔한 코드가 아닐 수도 있다. 혹시 더 깔끔하고 효율적인 방식이 있다면 댓글로 작성해 주시길 바란다. 타당하면 적극 반영하겠다..


본론

전체 코드

우선 전체 코드를 보고 하나 씩 설명하는 방식으로 진행하겠다.

LoginController

@Controller
@Slf4j
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;

    /*
    회원 가입
    - 회원 가입 성공 시 : 유저 저장 후 200 OK 반환
    - 회원 가입 실패 시 : 400 Bad Request 반환
     */
    @PostMapping("/users")
    public ResponseEntity<Void> registerUser(@RequestBody User user) {
        boolean registerUserResult = loginService.registerUser(user);
        log.info("회원 가입 사용자 이름 : " + user.getUserId() + ", 비밀번호 : " + user.getUserPassword());
        log.info("회원 가입 성공 여부 : " + registerUserResult);
        return registerUserResult ?
                new ResponseEntity<>(HttpStatus.OK) : new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }

    /*
    로그인
    - 로그인 성공 시 : 세션 생성 후 200 OK와 로그인 유저 정보 반환
    - 로그인 실패 시 : 세션 생성 하지 않고 400 Bad Request 반환
     */
    @PostMapping("/users/login")
    @ResponseBody
    public ResponseEntity<UserDTO> loginUser(@RequestBody User user,
                                             HttpServletRequest request) {
        User loginCheckResult = loginService.loginCheck(user);

        log.info("로그인 사용자 이름 : " + user.getUserId() + ", 비밀번호 : " + user.getUserPassword());
        log.info("로그인 성공 여부 : " + loginCheckResult);

        if (loginCheckResult != null) {
            loginService.createSession(request,loginCheckResult);
            UserDTO userDTO = new UserDTO(loginCheckResult.getUserId(),
                    loginCheckResult.getUserName(),
                    loginCheckResult.getUserType(),
                    null);

            return new ResponseEntity<>(userDTO, HttpStatus.OK);
        }
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }

    /*
    자동 로그인 메서드
    - 만약 세션이 유효하지 않다면 Interceptor에서 먼저 처리 후 401 응답
    - 만약 세션이 유효하다면 해당 메서드로 넘어와서 200 반환
     */
    @GetMapping("/auto-login")
    @ResponseStatus(HttpStatus.OK)
    public void autoLogin() {

    }


    /*
    로그아웃 메서드
    - request에서 session 삭제
    - 이후 Cookie 생성 후, maxAge 0초로해서 response에 담아서 보냄
     */
    @PostMapping("/users/logout")
    public ResponseEntity logoutUser(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }

        return new ResponseEntity(HttpStatus.OK);
    }

LoginService

@Service
@RequiredArgsConstructor
@Slf4j
public class LoginService {

    private final UserService userService;
    private final UserRepository userRepository;
    /*
    회원 가입 메서드
    - validateUser(중복 유저 검사) 반환 값을 토대로 false 또는 저장 후 true 반환
     */
    @Transactional
    public boolean registerUser(User user) {
        if (validateUser(user)) {
            return false;
        }
        userRepository.save(user);
        return true;
    }

    /*
    중복 유저 검사 메서드
    - 동일 아이디 존재 검사 후 해당 아이디의 유저 존재 시 true, 없을 시 false 반환
     */
    public boolean validateUser(User user) {
        Optional<User> findUser = userRepository.findById(user.getUserId());
        return findUser.isPresent();
    }


    /*
    로그인 체크 메서드
    - 해당 userId의 유저가 존재하고, 입력한 비밀번호가 일치하면 true 반환
    - 해당 유저가 존재하지 않거나, 비밀번호가 틀릴 시 false 반환
     */
    public User loginCheck(User user) {
        try {
            User findUser = userService.findById(user.getUserId());
            if (findUser.getUserPassword().equals(user.getUserPassword())) {
                return findUser;
            }
        } catch(UserNotFoundException e) {
            log.info("로그인 실패 - userId : {} 존재하지 않음", user.getUserId());
        }
        return null;
    }

    /*
    세션 생성 메서드
    - 해당 유저 아이디로 세션을 생성하고 넣어줌
     */
    public void createSession(HttpServletRequest request, User user) {
        HttpSession session = request.getSession();
        session.setAttribute("user", user);
    }
}

회원가입

사실 회원 가입은 간단해서 별로 작성할 게 없다. 프론트엔드와의 API 설계가 확실하면 프론트엔드에서 데이터를 받아 DB에 저장하면 되기 때문이다. 물론 암호화 로직(비밀번호 해시 함수 적용) 등을 적용하면 조금 더 복잡해 질 수 있겠지만 그래도 왠만해선 간단하다.

현 프로젝트는 상용화가 목적이 아니기 때문에 따로 해시 함수 등은 적용하지 않았다.

로직 설명

  1. 프론트엔드에서 /users로 Json 형태의 API 요청을 함
  2. Controller의 registerUser에서 이를 받음
  3. Service의 registerUser 호출
  4. Service의 registerUser에서는 validateUser 호출
  5. validateUser는 해당 UserId가 존재하는지 검사(UserId는 Primary Key이다).
  6. 만약 해당 UserId가 존재하면 false, 존재하지 않으면 true 반환
  7. 이를 토대로 프론트엔드에게 200 OK 또는 400 Bad Request 반환

로그인

로그인 단계에서 우선적으로 검사해야 하는 것은 다음과 같다.

  1. 해당 유저가 존재하는지
  2. 입력한 비밀번호가 DB에 저장된 비밀번호와 일치하는지

만약 위의 조건들을 만족하면 서버 측에서 세션을 생성하고 세션ID를 응답에 포함시킨다. 그리고 클라이언트 측에서는 이후 모든 요청에 세션ID를 넣어서 보내고, 서버는 요청에서 해당 세션ID를 검사한 후 유효할 시 다른 URL에 대한 요청을 승인한다. 세션은 아래에서 따로 코드와 설명하겠다.

로직 설명

  1. 프론트엔드에서 /users/login으로 Json 형태의 API 요청을 함
  2. Controller의 loginUser에서 Service의 loginCheck 호출
  3. loginCheck에서는 요청한 UserId의 유저를 찾음
    • findById는 Optional로 해당 User가 없으면 UserNotFoundException을 발생하게 작성함
  4. 해당 유저가 존재하고 비밀번호가 일치하면 응답을 만들고 세션ID를 넣어 200OK와 함께 응답 전송
  5. 존재하지 않거나 비밀번호가 다를 시 400 Bad Request 전송

세션

세션도 생성은 로그인 시에 createSession 메서드에서 생성하고 Login 성공 시 이를 응답에 넣어준다.

세션을 생성하고 넣는 부분은 원래 다른 로직이었는데, 테스트 배포 중 오류가 발생해 부득이하게 현재의 방식으로 개발했다.

  • 오류를 해결했고, 코드도 정상 코드로 바꿨다. 어떤 오류 인지는 링크에 기술하였다.

추가적인 세션 검사 코드는 아래와 같다.

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {

    /*
    CORS 문제 해결 메서드
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                //.allowedOrigins("http://localhost:3000")
                .allowedMethods("OPTIONS", "GET", "POST", "PUT", "DELETE")
                .allowCredentials(true);
    }

    /*
    Interceptor 등록, 세션 처리 메서드
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/users", "/users/login");
    }

 }

추가적으로 미디어 서버 설정 등 코드가 있지만 보안 상 넣지 않았다. 또한 아직 개발이 완료된 상태가 아니기 때문에 CORS도 모든 URL 허용으로 되어 있다.

LoginInterceptor

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    /*
    세션 처리 메서드
    - 세션을 조회하여 유효하면 요청 진행
    - 세션이 유효하지 않을 시, 401 Unauthorized 응답
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if(request.getMethod().equals("OPTIONS")){
            return true;
        }
        if (session == null) {
            response.sendError(401);
            log.info("No Session : request URL : " +request.getRequestURL());
            return false;
        }
        User user = (User) session.getAttribute("user");
        if (user == null) {
            response.sendError(401);
            log.info("No User Session : request URL : " +request.getRequestURL());
            return false;
        }
        log.info("success URL : " + request.getRequestURL());
        return true; //현재 요청 계속 처리
    }

}

로직 설명

  1. WebConfig의 addInterceptor에서 LoginInterceptor를 등록한다.
    • /users, /users/login를 제외한 모든 URL에서 LoginInterceptor를 거친 후 동작을 수행한다.
  2. LoginInterceptor의 preHandle 메서드에서 해당 요청에 세션이 없거나 세션ID가 유효하지 않으면 상태코드 401을 보내고 해당 요청을 중단한다.
  3. 만약 세션이 유효할 시 요청을 진행한다.

위와 같이 그렇게 어려운 부분이 없다.

이외에도 로그아웃은 클라이언트가 가지고 있는 세션ID를 파기한 후, 만료 기간이 0(유효하지 않은 세션)을 발급한다.

또한 autoLogin 부분은 프론트엔드 측에서 세션 검사를 위해 만들었다. 세션이 없거나 유효하지 않다면 preHandle에서 401에러를 보내기 때문에 세션이 유효할 시에만 200을 보내게 된다.


이외에도 DB 설계, API 명세 등이 있지만 아직 시선 추적 부분의 설계가 완료되지 않아서 우선적으로 로그인/회원가입, WebRTC 시그널링(WebSocket), 방 생성/관리 로직 등을 포스팅 할 예정이다.

profile
백엔드 개발자가 되겠어요

0개의 댓글