댓글 연관관계를 추가한 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);
}
'Spring' 카테고리의 다른 글
일정 관리 서버 Develop : 댓글, 일정 페이징 조회 기능 추가 (0) | 2025.02.13 |
---|---|
일정 관리 서버 Develop : 예외처리 및 검증, 비밀번호 암호화 구현 (0) | 2025.02.13 |
일정 관리 서버 Develop 설계 (API 명세서, ERD 작성) (0) | 2025.02.11 |
Spring Validation (1) | 2025.02.06 |
Spring 의존관계 주입 방법 : Constructor, Setter, Field (0) | 2025.02.06 |