Skip to content

Commit

Permalink
[BE] 로그아웃 기능 추가 (#151)
Browse files Browse the repository at this point in the history
* refactor(jwtManager): jwt 토큰에 만료 시간을 추가

* refactor: 로그인 시 참가자의 이름을 같이 반환하도록 변경

* feat: path 경로 수정 및 로그아웃 api 추가

* chore: 토큰 만료시간 추가

* chore: 서브모듈 업데이트 반영

- JWT 토큰 만료 시간 추가

* refactor(JwtCookieManager): 쿠키 생성의 책임을 컨트롤러에서 분리

* refactor(MeetingController): 약속 최초 생성시 주최자에 대한 JWT 토큰을 쿠키로 전송하도록 변경

* style(JwtManager): 생성자 매개변수 팀 컨벤션 개행 적용

* refactor: 쿠키 관련 상수 명 설정

* refactor(JwtCookieManager): 클래스 이름을 CookieManager 로 변경

* refactor(CookieManager): SAME SITE 상수 명을 변경하여 의미를 개선

* refactor(JwtProperties): JWT 관련 환경 변수들을 POJO로 관리하도록 개선

* chore: 서브모듈 업데이트 반영

* refactor(JwtProperties): expirationPeriod 필드 NotNull 검증 추가

* refactor: ConfigurationPropertiesScan을 사용하여 전역으로 관리하도록 변경

* refactor(CookieManager): 클래스의 책임에 맞도록 상수 및 변수 이동
  • Loading branch information
seokmyungham authored Aug 4, 2024
1 parent 8ac3259 commit f970575
Show file tree
Hide file tree
Showing 16 changed files with 183 additions and 66 deletions.
2 changes: 2 additions & 0 deletions backend/src/main/java/kr/momo/MomoApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@ConfigurationPropertiesScan
@SpringBootApplication
public class MomoApplication {

Expand Down
32 changes: 32 additions & 0 deletions backend/src/main/java/kr/momo/controller/CookieManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package kr.momo.controller;

import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

@Component
public class CookieManager {

private static final String ACCESS_TOKEN = "ACCESS_TOKEN";
private static final String SAME_SITE_OPTION = "None";
private static final long SESSION_COOKIE_AGE = -1;
private static final long EXPIRED_COOKIE_AGE = 0;

public String createNewCookie(String value, String path) {
return createCookie(value, path, SESSION_COOKIE_AGE);
}

public String createExpiredCookie(String path) {
return createCookie("", path, EXPIRED_COOKIE_AGE);
}

private String createCookie(String value, String path, long maxAge) {
return ResponseCookie.from(ACCESS_TOKEN, value)
.httpOnly(true)
.secure(true)
.path(path)
.sameSite(SAME_SITE_OPTION)
.maxAge(maxAge)
.build()
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package kr.momo.controller.attendee;

import jakarta.validation.Valid;
import kr.momo.controller.CookieManager;
import kr.momo.controller.MomoApiResponse;
import kr.momo.service.attendee.AttendeeService;
import kr.momo.service.attendee.dto.AttendeeLoginRequest;
import kr.momo.service.attendee.dto.AttendeeLoginResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -16,28 +18,28 @@
@RequiredArgsConstructor
public class AttendeeController {

private static final String ACCESS_TOKEN = "ACCESS_TOKEN";
private static final String SAME_SITE_SETTING = "None";

private final AttendeeService attendeeService;
private final CookieManager cookieManager;

@PostMapping("/api/v1/meetings/{uuid}/login")
public ResponseEntity<Void> login(@PathVariable String uuid, @RequestBody @Valid AttendeeLoginRequest request) {
String token = attendeeService.login(uuid, request);
String path = String.format("/api/v1/meetings/%s/", uuid);
public ResponseEntity<MomoApiResponse<String>> login(
@PathVariable String uuid, @RequestBody @Valid AttendeeLoginRequest request
) {
AttendeeLoginResponse response = attendeeService.login(uuid, request);
String path = String.format("/meeting/%s", uuid);
String cookie = cookieManager.createNewCookie(response.token(), path);

return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, createCookie(token, path))
.build();
.header(HttpHeaders.SET_COOKIE, cookie)
.body(new MomoApiResponse<>(response.name()));
}

private String createCookie(String value, String path) {
return ResponseCookie.from(ACCESS_TOKEN, value)
.httpOnly(true)
.secure(true)
.path(path)
.sameSite(SAME_SITE_SETTING)
.build()
.toString();
@PostMapping("/api/v1/meetings/{uuid}/logout")
public ResponseEntity<Void> logout(@PathVariable String uuid) {
String cookie = cookieManager.createExpiredCookie(uuid);

return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

import jakarta.validation.Valid;
import java.net.URI;
import kr.momo.controller.CookieManager;
import kr.momo.controller.MomoApiResponse;
import kr.momo.controller.auth.AuthAttendee;
import kr.momo.service.meeting.MeetingService;
import kr.momo.service.meeting.dto.MeetingCreateRequest;
import kr.momo.service.meeting.dto.MeetingCreateResponse;
import kr.momo.service.meeting.dto.MeetingResponse;
import kr.momo.service.meeting.dto.MeetingSharingResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
Expand All @@ -22,13 +25,18 @@
public class MeetingController {

private final MeetingService meetingService;
private final CookieManager cookieManager;

@PostMapping("/api/v1/meetings")
public ResponseEntity<MomoApiResponse<MeetingSharingResponse>> create(
public ResponseEntity<MomoApiResponse<MeetingCreateResponse>> create(
@RequestBody @Valid MeetingCreateRequest request
) {
MeetingSharingResponse response = meetingService.create(request);
MeetingCreateResponse response = meetingService.create(request);
String path = String.format("/meeting/%s", response.uuid());
String cookie = cookieManager.createNewCookie(response.token(), path);

return ResponseEntity.created(URI.create("/meeting/" + response.uuid()))
.header(HttpHeaders.SET_COOKIE, cookie)
.body(new MomoApiResponse<>(response));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

public enum AuthErrorCode implements ErrorCodeType {

INVALID_TOKEN(HttpStatus.BAD_REQUEST, "유효하지 않은 토큰입니다."),
NOT_FOUND_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다.");
UNAUTHORIZED_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import kr.momo.exception.MomoException;
import kr.momo.exception.code.MeetingErrorCode;
import kr.momo.service.attendee.dto.AttendeeLoginRequest;
import kr.momo.service.attendee.dto.AttendeeLoginResponse;
import kr.momo.service.auth.JwtManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -24,7 +25,7 @@ public class AttendeeService {
private final JwtManager jwtManager;

@Transactional
public String login(String uuid, AttendeeLoginRequest request) {
public AttendeeLoginResponse login(String uuid, AttendeeLoginRequest request) {
Meeting meeting = meetingRepository.findByUuid(uuid)
.orElseThrow(() -> new MomoException(MeetingErrorCode.INVALID_UUID));

Expand All @@ -36,14 +37,14 @@ public String login(String uuid, AttendeeLoginRequest request) {
.orElseGet(() -> signup(meeting, name, password));
}

private String verifyPassword(Attendee attendee, AttendeePassword password) {
private AttendeeLoginResponse verifyPassword(Attendee attendee, AttendeePassword password) {
attendee.verifyPassword(password);
return jwtManager.generate(attendee.getId());
return AttendeeLoginResponse.from(jwtManager.generate(attendee.getId()), attendee);
}

private String signup(Meeting meeting, AttendeeName name, AttendeePassword password) {
private AttendeeLoginResponse signup(Meeting meeting, AttendeeName name, AttendeePassword password) {
Attendee attendee = new Attendee(meeting, name, password, Role.GUEST);
attendeeRepository.save(attendee);
return jwtManager.generate(attendee.getId());
return AttendeeLoginResponse.from(jwtManager.generate(attendee.getId()), attendee);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.momo.service.attendee.dto;

import kr.momo.domain.attendee.Attendee;

public record AttendeeLoginResponse(String token, String name) {

public static AttendeeLoginResponse from(String token, Attendee attendee) {
return new AttendeeLoginResponse(token, attendee.name());
}
}
24 changes: 12 additions & 12 deletions backend/src/main/java/kr/momo/service/auth/JwtManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,28 @@
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import kr.momo.exception.MomoException;
import kr.momo.exception.code.AuthErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtManager {

private static final String CLAIM_ID = "id";

private final String secretKey;

public JwtManager(@Value("${security.jwt.secret_key}") String secretKey) {
this.secretKey = secretKey;
}
private final JwtProperties jwtProperties;

public String generate(long id) {
Date expirationDate = new Date(System.currentTimeMillis() + jwtProperties.getExpirationPeriodInMillis());
return JWT.create()
.withClaim(CLAIM_ID, id)
.sign(Algorithm.HMAC256(secretKey));
.withExpiresAt(expirationDate)
.sign(Algorithm.HMAC256(jwtProperties.getSecretKey()));
}

public long extract(String token) {
Expand All @@ -39,21 +39,21 @@ public long extract(String token) {

private DecodedJWT verifyToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getSecretKey());
JWTVerifier verifier = JWT.require(algorithm).build();
return verifier.verify(token);
} catch (InvalidClaimException e) {
log.warn("토큰 내 클레임이 유효하지 않습니다. {} ", e.getMessage());
throw new MomoException(AuthErrorCode.INVALID_TOKEN);
throw new MomoException(AuthErrorCode.UNAUTHORIZED_TOKEN);
} catch (JWTDecodeException e) {
log.warn("JWT 토큰 구성이 올바르지 않습니다. {} ", e.getMessage());
throw new MomoException(AuthErrorCode.INVALID_TOKEN);
throw new MomoException(AuthErrorCode.UNAUTHORIZED_TOKEN);
} catch (SignatureVerificationException e) {
log.warn("토큰의 서명이 알고리즘을 사용하여 검증했을 때 유효하지 않습니다. {} ", e.getMessage());
throw new MomoException(AuthErrorCode.INVALID_TOKEN);
throw new MomoException(AuthErrorCode.UNAUTHORIZED_TOKEN);
} catch (JWTVerificationException e) {
log.warn("토큰 검증 과정에서 예기치 못한 에러가 발생하였습니다. {} ", e.getMessage());
throw new MomoException(AuthErrorCode.INVALID_TOKEN);
throw new MomoException(AuthErrorCode.UNAUTHORIZED_TOKEN);
}
}
}
28 changes: 28 additions & 0 deletions backend/src/main/java/kr/momo/service/auth/JwtProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kr.momo.service.auth;

import jakarta.validation.constraints.NotNull;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.validation.annotation.Validated;

@Getter
@Validated
@RequiredArgsConstructor
@ConfigurationProperties("security.jwt")
public class JwtProperties {

@NotNull
private final String secretKey;

@NotNull
@DurationUnit(ChronoUnit.HOURS)
private final Duration expirationPeriod;

public long getExpirationPeriodInMillis() {
return getExpirationPeriod().toMillis();
}
}
16 changes: 11 additions & 5 deletions backend/src/main/java/kr/momo/service/meeting/MeetingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import kr.momo.exception.MomoException;
import kr.momo.exception.code.AttendeeErrorCode;
import kr.momo.exception.code.MeetingErrorCode;
import kr.momo.service.auth.JwtManager;
import kr.momo.service.meeting.dto.MeetingCreateRequest;
import kr.momo.service.meeting.dto.MeetingCreateResponse;
import kr.momo.service.meeting.dto.MeetingResponse;
import kr.momo.service.meeting.dto.MeetingSharingResponse;
import lombok.RequiredArgsConstructor;
Expand All @@ -28,13 +30,17 @@ public class MeetingService {
private final MeetingRepository meetingRepository;
private final AvailableDateRepository availableDateRepository;
private final AttendeeRepository attendeeRepository;
private final JwtManager jwtManager;

@Transactional
public MeetingSharingResponse create(MeetingCreateRequest request) {
public MeetingCreateResponse create(MeetingCreateRequest request) {
Meeting meeting = saveMeeting(request.meetingName(), request.meetingStartTime(), request.meetingEndTime());
saveAvailableDates(request.meetingAvailableDates(), meeting);
saveHostAttendee(meeting, request.hostName(), request.hostPassword());
return MeetingSharingResponse.from(meeting);

Attendee attendee = saveHostAttendee(meeting, request.hostName(), request.hostPassword());
String token = jwtManager.generate(attendee.getId());

return MeetingCreateResponse.from(meeting, attendee, token);
}

private Meeting saveMeeting(String meetingName, LocalTime startTime, LocalTime endTime) {
Expand All @@ -47,9 +53,9 @@ private void saveAvailableDates(List<LocalDate> dates, Meeting meeting) {
availableDateRepository.saveAll(availableDates.getAvailableDates());
}

private void saveHostAttendee(Meeting meeting, String hostName, String hostPassword) {
private Attendee saveHostAttendee(Meeting meeting, String hostName, String hostPassword) {
Attendee attendee = new Attendee(meeting, hostName, hostPassword, Role.HOST);
attendeeRepository.save(attendee);
return attendeeRepository.save(attendee);
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.momo.service.meeting.dto;

import kr.momo.domain.attendee.Attendee;
import kr.momo.domain.meeting.Meeting;

public record MeetingCreateResponse(String uuid, String name, String token) {

public static MeetingCreateResponse from(Meeting meeting, Attendee attendee, String token) {
return new MeetingCreateResponse(meeting.getUuid(), attendee.name(), token);
}
}
3 changes: 2 additions & 1 deletion backend/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ spring:

security:
jwt:
secret_key: ${random.value}"
secret-key: ${random.value}"
expiration-period: 1h
2 changes: 1 addition & 1 deletion backend/src/main/resources/security
Loading

0 comments on commit f970575

Please sign in to comment.