본문 바로가기

Spring

일정 관리 서버 Develop : API 추가 설계 및 세션 로그인 구현

댓글 연관관계를 추가한 ERD

고민했던 점

  • 처음에 ERD를 설계할 때 댓글에 회원 FK가 들어가서 테이블이 사이클 형태가 만들어져서 잘 설계한 것인지 고민됨
  • 일정 테이블에도 FK로 회원ID를 가지고 있는데 굳이 댓글 테이블에도 회원 FK를 넣어야 하나 고민했음
  • 하지만 생각해보니 일정은 "일정 작성자 식별자"만 가지고 있고, 댓글은 "댓글 작성자의 식별자" 를 가지고 있는 것이기 때문에 추가하는 것이 맞았음 -> 짱구가 쓴 일정에 짱구아빠 흰둥이가 댓글을 달 수 있기 때문에 엄연히 다르다!  

 

SQL

CREATE TABLE member
(
    id         BIGINT AUTO_INCREMENT PRIMARY KEY NOT NULL COMMENT '회원 식별자',
    name       VARCHAR(200) NOT NULL COMMENT '이름',
    email      VARCHAR(50) NOT NULL COMMENT '이메일',
    password   VARCHAR(50) NOT NULL COMMENT '비밀번호',
    created_at TIMESTAMP NOT NULL COMMENT '작성일',
    updated_at TIMESTAMP NOT NULL COMMENT '수정일'
);

CREATE TABLE schedule
(
    id         BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '일정 식별자',
    member_id  BIGINT COMMENT '작성자 식별자',
    title      VARCHAR(200) NOT NULL COMMENT '제목',
    content    TEXT NOT NULL COMMENT '일정 내용',
    created_at TIMESTAMP NOT NULL COMMENT '작성일',
    updated_at TIMESTAMP NOT NULL COMMENT '수정일',
    FOREIGN KEY (member_id) REFERENCES member(id)
);

CREATE TABLE comment
(
    id         BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '댓글 식별자',
    member_id  BIGINT COMMENT '작성자 식별자',
    schedule_id  BIGINT COMMENT '일정 식별자',
    content    TEXT NOT NULL COMMENT '댓글 내용',
    created_at TIMESTAMP NOT NULL COMMENT '작성일',
    updated_at TIMESTAMP NOT NULL COMMENT '수정일',
    FOREIGN KEY (member_id) REFERENCES member(id),
    FOREIGN KEY (schedule_id) REFERENCES schedule(id)
);

 

API

 

DTO

  • 로그인, 일정, 회원, 댓글 부분으로 총 4개의 역할에 따라 분리

 

세션 로그인 구현

1. 로그인 로직 실행

  • LoginRequestDto : 사용자로부터 이메일과 비밀번호를 입력받음
  • 사용자로부터 입력받은 비밀번호가 데이터베이스에 저장된 암호화된 비밀번호랑 동일한 경우, LoginResponseDto에 사용자 id값을 넣어서 반환
public LoginResponseDto login(LoginRequestDto requestDto){
        String rawPassword = requestDto.getPassword();
        Member member = memberRepository.findByEmail(requestDto.getEmail()).orElseThrow(() -> new MemberNotFoundException());
        if (passwordEncoder.matches(rawPassword, member.getPassword())){
            return new LoginResponseDto(member.getId());
        } else {
            throw new InvalidPasswordException();
        }
    }

 

2. HttpServletRequest를 받아서 session에 sessionKey 입력해 주기

  • HttpServletRequest : 클라이언트가 보낸 HTTP 요청 정보(헤더, 파라미터, 세션 정보 등)를 담고 있는 객체
  • 로그인이 성공했다면 클라이언트의 세션에 ( "loginUser": "사용자pk값") 형식으로 sessionKey 추가해서 응답
  • 키 값은 상수로 관리 (반드시 인터페이스나 추상클래스로 관리)
  • 세션ID는 쿠키를 통해서만 전송되도록 application.properties에 아래와 같이 설정 (설정하지 않는다면 최초 로그인시에 세션 ID값이 URL로 나옴)
server.servlet.session.tracking-modes=cookie
@PostMapping("/login")
    public ResponseEntity<LoginResponseDto> login(@Valid @RequestBody LoginRequestDto loginRequestDto, HttpServletRequest servletRequest){
        LoginResponseDto responseDto = authService.login(loginRequestDto);
        HttpSession session = servletRequest.getSession();
        session.setAttribute(Const.LOGIN_USER, responseDto);
        return new ResponseEntity<>(responseDto, HttpStatus.OK);
    }

 

  • 로그인이 성공한 경우에 세션ID값이 쿠키를 통해서 전송됨

 

3. Filter 생성하기

고민했던 점

  • 세션 활성화 여부를 확인할 때 조건에서 session.getId()를 사용했더니 로그인 검증이 제대로 되지 않았음
    • 세션 ID는 세션이 활성화 됐을 때만 존재하는 아이디인줄 알았음
    • 알고보니 HttpSession 객체가 생성되면 서버에서 세션 ID를 부여하고 이 값은 세션이 활성화되지 않거나 필요한 속성이 저장되지 않아도 반환되는 값이라고 함
    • 세션 ID가 존재한다고 해서 세션에 유효한 데이터가 있다는 보장 x
  • session.getAttribute("sessionKey")의 존재여부로 검증해서 해결 
    • 사용자가 로그인이 성공했다면 로그인시에 세션에 ( "loginUser": "사용자pk값") 형식으로 sessionKey 추가해서 응답했기 때문에 이 값이 존재하는지 확인하는 것으로 로그인을 했는지 확인 가능
    • 세션이 만료되거나 로그아웃 되면 해당 값은 존재하지 않음
    • sessionKey가 존재한다면 세션에 유효한 데이터가 있다는 보장 o
 if (!isWhiteList(requestURI)){
            // 세션이 존재하지 않는다면 생성하지 않음 (= 로그인을 하지 않았다는 의미)
            HttpSession session = httpServletRequest.getSession(false);
            if (session == null || session.getAttribute("loginUser") == null) {
                throw new UnauthorizedException();
            }
        }

 

코드 설명

  • WHITE_LIST : 필터를 거치지 않아도 되는 인가가 필요없는 경로 리스트
  • ServletRequest는 기능이 적기 때문에 HttpServletRequest로 다운캐스팅
  • chain.doFilter(request,response) 이후에 다음 필터가 있다면 그 필터로 가고, 없다면 바로 Servlet 호출
@Slf4j
public class LoginFilter implements Filter {
    public static final String[] WHITE_LIST = {"/auth/signup", "/auth/login", "/auth/session"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // ServletRequest는 기능이 적기 때문에 HttpServletRequest로 다운캐스팅
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String requestURI = httpServletRequest.getRequestURI();

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        log.info("로그인 필터 실행");

        // 검증을 제회하는 URI에 포함되지 않은 경우 검증 실행
        if (!isWhiteList(requestURI)){
            // 세션이 존재하지 않는다면 생성하지 않음 (= 로그인을 하지 않았다는 의미)
            HttpSession session = httpServletRequest.getSession(false);
            if (session == null || session.getAttribute("loginUser") == null) {
                throw new UnauthorizedException();
            }
        }

        // 더 이상 호출할 필터가 없으면 Servlet 바로 호출
        chain.doFilter(request,response);
    }

    private boolean isWhiteList(String requestURI) {
        return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
    }
}

 

4. Filter 적용하기

  • FilterRegistrationBean : 스프링 부트에서 필터를 등록하고 설정할 때 사용하는 클래스
  • 우선순위는 숫자가 적을수록 높음
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean loginFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LoginFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }
}

 


추가 +) 해설 세션을 듣고 추가한 내용 ^^;;

생각해보니 memberId값을 로그인을 수행한 후에 세션 속성에 넣었기 떄문에, 요청을 보낼 때 세션에 있는 해당 memberId 속성값을 사용해서 사용해야 하는데, 이걸 빼먹어 버렸다 🙀 (세션의 역할이 사용자 식별 이건데 ...ㅎㅎㅎㅎ)

 

세션 로그인을 통한 인가

  • 로그인 성공시에 세션에 지정해 줬던 값
session.setAttribute(Const.LOGIN_USER, responseDto.getId());
  • 세션에 저장된 loginUser값을 꺼내서 어떤 회원의 요청인지 확인할 때 사용!
  • @SessionAttribute : 세션에 저장된 데이터를 컨트롤러에서 사용할 수 있게 하는 역할
  • 이제 요청 Dto에는 userId가 없음!
@PatchMapping()
public ResponseEntity<ScheduleResponseDto> update(@RequestBody @Valid ScheduleUpdateRequestDto requestDto,
                                                  @SessionAttribute(name = Const.LOGIN_USER) Long userId){
    ScheduleResponseDto scheduleResponseDto = scheduleService.update(userId, requestDto);
    return new ResponseEntity<>(scheduleResponseDto, HttpStatus.OK);
}