Skip to content

Commit

Permalink
feat: 세션 동기화 구현 (#442)
Browse files Browse the repository at this point in the history
* build: actuator 추가

* feat: 세션 그룹을 저장하고 조회하는 기능 추가

* build: 세션 그룹 스키마 작성

* feat: 세션 그룹 조회, 생성, 삭제 기능 구현

* feat: 세션 그룹 객체 및 업데이트 메서드 추가

* refactor: 세션 생성 시 만료일 파라미터 삭제

* feat: HttpSession -> SessionGroup으로 변경

* build: flyway V7 작성

* fix: 세션 연장을 위한 `@Transactional` 추가

* refactor: 세션 커스텀 예외 생성

* chore: 테스트 컨벤션 수정

* refactor: 세션 만료 설정 추가

* chore: 메서드 명 및 컨벤션 수정
  • Loading branch information
Kim0914 authored and hozzijeong committed Oct 20, 2023
1 parent 52cd1ed commit 5abeafc
Show file tree
Hide file tree
Showing 20 changed files with 663 additions and 22 deletions.
1 change: 1 addition & 0 deletions backend/pium/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
implementation 'com.google.firebase:firebase-admin:9.2.0'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.official.pium.config;

import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TimeConfig {

@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.official.pium.admin.controller.AdminArgumentResolver;
import com.official.pium.controller.MemberArgumentResolver;
import com.official.pium.repository.MemberRepository;
import com.official.pium.service.SessionGroupService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
Expand All @@ -14,10 +15,11 @@
public class WebMvcConfigure implements WebMvcConfigurer {

private final MemberRepository memberRepository;
private final SessionGroupService sessionGroupService;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new MemberArgumentResolver(memberRepository));
resolvers.add(new MemberArgumentResolver(memberRepository, sessionGroupService));
resolvers.add(new AdminArgumentResolver());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.official.pium.domain.Auth;
import com.official.pium.domain.Member;
import com.official.pium.service.AuthService;
import com.official.pium.service.SessionGroupService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.constraints.NotBlank;
Expand All @@ -19,9 +20,10 @@
public class AuthController {

private static final String SESSION_KEY = "KAKAO_ID";
private static final int EXPIRED_TIME_SIX_HOUR = 21600;
private static final int SESSION_EXPIRE_SECOND = 86400 * 30;

private final AuthService authService;
private final SessionGroupService sessionGroupService;

@PostMapping("/login")
public ResponseEntity<Void> login(
Expand All @@ -30,9 +32,8 @@ public ResponseEntity<Void> login(
Member loginMember = authService.login(code);

HttpSession session = request.getSession();
session.setAttribute(SESSION_KEY, loginMember.getKakaoId());
session.setMaxInactiveInterval(EXPIRED_TIME_SIX_HOUR);

session.setMaxInactiveInterval(SESSION_EXPIRE_SECOND);
sessionGroupService.add(session.getId(), SESSION_KEY, loginMember.getKakaoId().toString());
return ResponseEntity.ok().build();
}

Expand All @@ -41,6 +42,7 @@ public ResponseEntity<Void> logout(HttpServletRequest request, @Auth Member memb
HttpSession session = request.getSession(false);

if (session != null) {
sessionGroupService.delete(session.getId(), SESSION_KEY);
session.invalidate();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package com.official.pium.controller;

import com.official.pium.exception.AuthenticationException;
import com.official.pium.exception.AuthorizationException;
import com.official.pium.fcm.exception.FcmException;
import com.official.pium.exception.OAuthException;
import com.official.pium.exception.OAuthException.KaKaoMemberInfoRequestException;
import com.official.pium.exception.OAuthException.KakaoServerException;
import com.official.pium.exception.OAuthException.KakaoTokenRequestException;
import com.official.pium.exception.dto.GlobalExceptionResponse;
import com.official.pium.fcm.exception.FcmException;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.StringJoiner;
import javax.naming.AuthenticationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import com.official.pium.domain.Auth;
import com.official.pium.domain.Member;
import com.official.pium.exception.AuthenticationException;
import com.official.pium.repository.MemberRepository;
import com.official.pium.service.SessionGroupService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import javax.naming.AuthenticationException;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
Expand All @@ -19,6 +20,7 @@ public class MemberArgumentResolver implements HandlerMethodArgumentResolver {
private static final String SESSION_KEY = "KAKAO_ID";

private final MemberRepository memberRepository;
private final SessionGroupService sessionGroupService;

@Override
public boolean supportsParameter(MethodParameter parameter) {
Expand All @@ -36,11 +38,12 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m
throw new AuthenticationException("로그인이 필요합니다");
}

String sessionValue = sessionGroupService.findOrExtendsBySessionIdAndKey(session.getId(), SESSION_KEY);
try {
Long kakaoId = (Long) session.getAttribute(SESSION_KEY);
Long kakaoId = Long.valueOf(sessionValue);
return memberRepository.findByKakaoId(kakaoId)
.orElseThrow(() -> new AuthenticationException("회원을 찾을 수 없습니다."));
} catch (ClassCastException e) {
} catch (NumberFormatException e) {
throw new AuthenticationException("잘못된 세션 정보로 인해 사용자 인증에 실패하였습니다.");
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.official.pium.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.proxy.HibernateProxy;

@Entity
@Getter
@Table(name = "session_group")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SessionGroup extends BaseEntity {

private static final int SESSION_EXTEND_DAYS = 7;
private static final int MIN_EXPIRE_DAY = 1;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
@Column(name = "session_id", nullable = false)
private String sessionId;

@NotBlank
@Column(name = "session_key", nullable = false)
private String sessionKey;

@NotBlank
@Column(name = "session_value", nullable = false)
private String sessionValue;

@NotNull
@Column(name = "expire_time", nullable = false)
private LocalDateTime expireTime;

@Builder
private SessionGroup(String sessionId, String sessionKey, String sessionValue, LocalDateTime expireTime) {
this.sessionId = sessionId;
this.sessionKey = sessionKey;
this.sessionValue = sessionValue;
this.expireTime = expireTime;
}

public boolean isExpired(LocalDateTime currentTime) {
return expireTime.isBefore(currentTime);
}

public boolean canExtends(LocalDateTime currentTime) {
return ChronoUnit.DAYS.between(currentTime, expireTime) < SESSION_EXTEND_DAYS;
}

public void extendExpireTime(long extendDay) {
if (extendDay < MIN_EXPIRE_DAY) {
throw new IllegalArgumentException("세션 연장 가능 일수는 음수가 될 수 없습니다.");
}
this.expireTime = this.expireTime.plusDays(extendDay);
}

@Override
public final boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null) {
return false;
}
Class<?> oEffectiveClass =
o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass()
: o.getClass();
Class<?> thisEffectiveClass =
this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
.getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) {
return false;
}
SessionGroup sessionGroup = (SessionGroup) o;
return getId() != null && Objects.equals(getId(), sessionGroup.getId());
}

@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
.getPersistentClass().hashCode() : getClass().hashCode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.official.pium.exception;

public class AuthenticationException extends RuntimeException {

public AuthenticationException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.official.pium.repository;

import com.official.pium.domain.SessionGroup;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface SessionGroupRepository extends JpaRepository<SessionGroup, Long> {

Optional<SessionGroup> findBySessionIdAndSessionKey(String sessionId, String sessionKey);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.official.pium.service;

import com.official.pium.domain.SessionGroup;
import com.official.pium.exception.AuthenticationException;
import com.official.pium.repository.SessionGroupRepository;
import java.time.Clock;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class SessionGroupService {

private static final int EXTEND_EXPIRED_DAY = 30;

private final SessionGroupRepository sessionGroupRepository;
private final Clock clock;

@Transactional
public String findOrExtendsBySessionIdAndKey(String sessionId, String key) {
SessionGroup sessionGroup = sessionGroupRepository.findBySessionIdAndSessionKey(sessionId, key)
.orElseThrow(() -> new AuthenticationException("일치하는 세션을 찾을 수 없습니다."));

LocalDateTime currentTime = LocalDateTime.now(clock);
validateSession(sessionGroup, currentTime);
extendsSession(sessionGroup, currentTime);
return sessionGroup.getSessionValue();
}

private void validateSession(SessionGroup sessionGroup, LocalDateTime currentTime) throws AuthenticationException {
if (sessionGroup.isExpired(currentTime)) {
sessionGroupRepository.delete(sessionGroup);
throw new AuthenticationException("세션이 만료 되었습니다.");
}
}

private void extendsSession(SessionGroup sessionGroup, LocalDateTime currentTime) {
if (sessionGroup.canExtends(currentTime)) {
sessionGroup.extendExpireTime(EXTEND_EXPIRED_DAY);
}
}

@Transactional
public void add(String sessionId, String key, String value) {
sessionGroupRepository.findBySessionIdAndSessionKey(sessionId, key)
.ifPresent(existsSessionGroup -> {
throw new AuthenticationException("이미 존재하는 세션입니다. sessionId: " + sessionId);
});

SessionGroup sessionGroup = SessionGroup.builder()
.sessionId(sessionId)
.sessionKey(key)
.sessionValue(value)
.expireTime(LocalDateTime.now().plusMinutes(EXTEND_EXPIRED_DAY))
.build();
sessionGroupRepository.save(sessionGroup);
}

@Transactional
public void delete(String sessionId, String key) {
SessionGroup sessionGroup = sessionGroupRepository.findBySessionIdAndSessionKey(sessionId, key)
.orElseThrow(() -> new AuthenticationException("일치하는 세션을 찾을 수 없습니다."));

sessionGroupRepository.delete(sessionGroup);
}
}
11 changes: 2 additions & 9 deletions backend/pium/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=validate

spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:sql/schema.sql
spring.sql.init.data-locations=classpath:sql/data.sql
Expand All @@ -13,31 +12,25 @@ auth.kakao.redirect-uri=http://localhost:8282/authorization
auth.kakao.unlink-uri=https://kapi.kakao.com/v1/user/unlink
auth.kakao.client-id=REST_API_KEY
auth.kakao.admin-id=ADMIN_KEY

server.servlet.session.cookie.same-site=none
server.servlet.session.cookie.secure=true

logging.level.org.hibernate.orm.jdbc.bind=trace
logging.slack.webhook-url=https://WEB_HOOK_URL.com

spring.flyway.enabled=false
admin.account=asdf
admin.password=123
admin.secondPassword=123

view.image.favicion=https://static.pium.life/prod/favicon.ico
view.image.home=https://static.pium.life/prod/home.png

spring.mvc.hiddenmethod.filter.enabled=true

registration.image.directory=registration

petPlant.image.directory=test

aws.s3.root=https://test.image.storage
aws.s3.bucket=test/
aws.s3.folder=test/
aws.s3.directory=test
management.endpoints.enabled-by-default=false
management.endpoint.health.enabled=true

fcm.key.path=test/
fcm.key.scope=https://www.googleapis.com/auth/firebase.messaging
Expand Down
14 changes: 14 additions & 0 deletions backend/pium/src/main/resources/db/migration/V7__session_group.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS session_group
(
id BIGINT AUTO_INCREMENT NOT NULL,
session_id VARCHAR(255) NOT NULL,
session_key VARCHAR(255) NOT NULL,
session_value VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
expire_time DATETIME NOT NULL,
PRIMARY KEY (id)
);

ALTER TABLE session_group
ADD CONSTRAINT uniq_sessions UNIQUE(session_id, session_key);
Loading

0 comments on commit 5abeafc

Please sign in to comment.