Skip to content

Commit

Permalink
Merge pull request #120 from prgrms-be-devcourse/feature/#91
Browse files Browse the repository at this point in the history
[#91] 사용자 인증, 인가 기능 구현 - 3
  • Loading branch information
charlesuu committed Sep 25, 2023
2 parents e040fd0 + becc3ae commit 430fea5
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 48 deletions.
29 changes: 29 additions & 0 deletions src/main/java/com/programmers/ticketparis/auth/dto/Session.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.programmers.ticketparis.auth.dto;

import java.time.LocalDateTime;

import com.programmers.ticketparis.member.enums.MemberRole;

import lombok.Getter;

@Getter
public class Session {

private MemberRole memberRole;
private Long memberId;
private LocalDateTime lastAccessTime;

private Session(MemberRole memberRole, Long memberId) {
this.memberRole = memberRole;
this.memberId = memberId;
this.lastAccessTime = LocalDateTime.now();
}

public static Session of(MemberRole memberRole, Long memberId) {
return new Session(memberRole, memberId);
}

public void updateLastAccessTime() {
this.lastAccessTime = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.programmers.ticketparis.auth.dto;

import com.programmers.ticketparis.member.enums.MemberRule;
import com.programmers.ticketparis.member.enums.MemberRole;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;

Expand All @@ -9,17 +10,17 @@
public class SessionValueDto {

@Schema(description = "고객, 판매자 판단")
private MemberRule memberRule;
private MemberRole memberRule;

@Schema(description = "고객, 판매자 ID")
private Long memberId;

private SessionValueDto(MemberRule memberRule, Long memberId) {
private SessionValueDto(MemberRole memberRule, Long memberId) {
this.memberRule = memberRule;
this.memberId = memberId;
}

public static SessionValueDto of(MemberRule memberRule, Long memberId) {
public static SessionValueDto of(MemberRole memberRule, Long memberId) {
return new SessionValueDto(memberRule, memberId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

import org.springframework.util.PatternMatchUtils;

import com.programmers.ticketparis.auth.dto.SessionValueDto;
import com.programmers.ticketparis.auth.exception.AuthException;
import com.programmers.ticketparis.auth.dto.Session;
import com.programmers.ticketparis.auth.service.AuthService;
import com.programmers.ticketparis.common.exception.ExceptionRule;
import com.programmers.ticketparis.auth.util.SessionThreadLocal;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
Expand All @@ -20,7 +19,7 @@
@RequiredArgsConstructor
public class AuthenticationFilter implements Filter {

private static final String[] whitelist = {"/api/customers", "/api/customers/login", "/api/sellers",
private static final String[] excludePaths = {"/api/customers", "/api/customers/login", "/api/sellers",
"/api/sellers/login", "/api/logout"};

private final AuthService authService;
Expand All @@ -34,17 +33,18 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
String requestURI = httpServletRequest.getRequestURI();

if (isLoginCheckPath(requestURI)) {
SessionValueDto sessionValueDto = authService.getSessionOrNull(httpServletRequest);
if (sessionValueDto == null) {
throw new AuthException(ExceptionRule.AUTHENTICATION_FAILED);
}
//인증 : (세션이 존재하는지 확인하는 Boolean 반환 메서드를 만들까했지만, 매 요청마다 select 두 번 나가는건 아닌 것 같아서 한 번의 조회로 해결했음)
Session loggedInMemberInfo = authService.getAuthenticatedSession(httpServletRequest);

//ThreadLocal에 로그인 회원 정보 배치
SessionThreadLocal.setSessionValueDto(loggedInMemberInfo);
}

chain.doFilter(request, response);
}

//화이트 리스트의 경우 인증 체크X
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
return !PatternMatchUtils.simpleMatch(excludePaths, requestURI);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.programmers.ticketparis.auth.mvc.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;

import com.programmers.ticketparis.auth.exception.AuthException;
import com.programmers.ticketparis.auth.util.SessionThreadLocal;
import com.programmers.ticketparis.auth.util.UrlToMemberRuleMatcher;
import com.programmers.ticketparis.common.exception.ExceptionRule;
import com.programmers.ticketparis.member.enums.MemberRole;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class AuthorizationInterceptor implements HandlerInterceptor {

private final UrlToMemberRuleMatcher urlToMemberRuleMatcher;

//인증은 필터에서 끝났음. 인가 처리. ThreadLocal에는 무조건 멤버 정보가 있는 상황.
//Config에서 매쳐에 등록한 URL만 인가 검사하도록 처리, 등록 안 했으면 다 통과되도록
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
Exception {
if (request.getMethod().equalsIgnoreCase("GET")) {
return true;
}

String requestURI = request.getRequestURI();
MemberRole requestMemberRole = SessionThreadLocal.getSessionValueDto().getMemberRole();
if (!urlToMemberRuleMatcher.isMatch(requestURI, requestMemberRole)) {
throw new AuthException(ExceptionRule.AUTHORIZATION_FAILED);
}

return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
SessionThreadLocal.clear();
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package com.programmers.ticketparis.auth.repository;

import com.programmers.ticketparis.auth.dto.SessionValueDto;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.programmers.ticketparis.auth.dto.Session;

public interface SessionRepository {

void createSession(SessionValueDto value, HttpServletResponse httpServletResponse);
String createSession(Session value);

SessionValueDto getSessionOrNull(HttpServletRequest httpServletRequest);
Session getSession(String sessionId);

void expire(HttpServletRequest httpServletRequest);
void expire(String sessionId);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,48 @@

import org.springframework.stereotype.Component;

import com.programmers.ticketparis.auth.dto.SessionValueDto;
import com.programmers.ticketparis.auth.dto.Session;
import com.programmers.ticketparis.auth.exception.AuthException;
import com.programmers.ticketparis.common.exception.ExceptionRule;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Getter;

@Getter
@Component
public class localCashSessionRepository implements SessionRepository {

private Map<String, SessionValueDto> sessionLocalCash = new ConcurrentHashMap<>();
private Map<String, Session> sessionLocalCash = new ConcurrentHashMap<>();

public void createSession(SessionValueDto value, HttpServletResponse httpServletResponse) {
public String createSession(Session session) {
String sessionId = UUID.randomUUID().toString();
sessionLocalCash.put(sessionId, value);
sessionLocalCash.put(sessionId, session);

httpServletResponse.addCookie(new Cookie(SESSION_COOKIE_NAME, sessionId));
return sessionId;
}

public SessionValueDto getSessionOrNull(HttpServletRequest httpServletRequest) {
Cookie sessionCookie = findCookieOrNull(httpServletRequest);
if (sessionCookie == null) {
return null;
public Session getSession(String sessionId) {
if (!sessionLocalCash.containsKey(sessionId)) {
throw new AuthException(ExceptionRule.AUTHENTICATION_FAILED);
}

return sessionLocalCash.get(sessionCookie.getValue());
Session currentSession = sessionLocalCash.get(sessionId);
currentSession.updateLastAccessTime();

return currentSession;
}

public void expire(HttpServletRequest httpServletRequest) {
Cookie sessionCookie = findCookieOrNull(httpServletRequest);
if (sessionCookie == null || (!sessionLocalCash.containsKey(sessionCookie.getValue()))) {
public void expire(String sessionId) {
if (!sessionLocalCash.containsKey(sessionId)) {
throw new AuthException(ExceptionRule.LOGOUT_FAILED);
}

sessionLocalCash.remove(sessionCookie.getValue());
sessionLocalCash.remove(sessionId);
}

private Cookie findCookieOrNull(HttpServletRequest httpServletRequest) {
//1. 요청에 SESSION_COOKIE_NAME 쿠키가 담겨 있는지 체크 후, 가져온다.
private Cookie findSessionCookieFromRequestOrNull(HttpServletRequest httpServletRequest) {
if (httpServletRequest.getCookies() == null) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package com.programmers.ticketparis.auth.service;

import static com.programmers.ticketparis.common.exception.ExceptionRule.*;
import static com.programmers.ticketparis.common.util.SessionConst.*;

import java.util.Arrays;
import java.util.Optional;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.programmers.ticketparis.auth.dto.LoginRequest;
import com.programmers.ticketparis.auth.dto.SessionValueDto;
import com.programmers.ticketparis.auth.dto.Session;
import com.programmers.ticketparis.auth.exception.AuthException;
import com.programmers.ticketparis.auth.repository.SessionRepository;
import com.programmers.ticketparis.common.exception.ExceptionRule;
import com.programmers.ticketparis.member.domain.Customer;
import com.programmers.ticketparis.member.domain.Seller;
import com.programmers.ticketparis.member.enums.MemberRule;
import com.programmers.ticketparis.member.enums.MemberRole;
import com.programmers.ticketparis.member.exception.CustomerException;
import com.programmers.ticketparis.member.exception.SellerException;
import com.programmers.ticketparis.member.service.CustomerService;
import com.programmers.ticketparis.member.service.SellerService;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
Expand All @@ -34,8 +42,8 @@ public void customerLogin(LoginRequest loginRequest, HttpServletResponse httpSer
throw new CustomerException(ExceptionRule.LOGIN_FAILED_PASSWORD_INVALID, loginRequest.getPassword());
}

sessionRepository.createSession(SessionValueDto.of(MemberRule.CUSTOMER, customer.getCustomerId()),
httpServletResponse);
String sessionId = sessionRepository.createSession(Session.of(MemberRole.CUSTOMER, customer.getCustomerId()));
setCookie(httpServletResponse, sessionId);
}

public void sellerLogin(LoginRequest loginRequest, HttpServletResponse httpServletResponse) {
Expand All @@ -44,16 +52,37 @@ public void sellerLogin(LoginRequest loginRequest, HttpServletResponse httpServl
throw new SellerException(ExceptionRule.LOGIN_FAILED_PASSWORD_INVALID, loginRequest.getPassword());
}

sessionRepository.createSession(SessionValueDto.of(MemberRule.SELLER, seller.getSellerId()),
httpServletResponse);
String sessionId = sessionRepository.createSession(Session.of(MemberRole.SELLER, seller.getSellerId()));
setCookie(httpServletResponse, sessionId);
}

public void logout(HttpServletRequest httpServletRequest) {
Cookie sessionId = findSessionIdCookieFromRequestOrNull(httpServletRequest)
.orElseThrow(() -> new AuthException(LOGOUT_FAILED));

sessionRepository.expire(sessionId.getValue());
}

//세션 조회 기능 : 컨트롤러가 아닌 필터, 인터셉터에서 호출
public SessionValueDto getSessionOrNull(HttpServletRequest httpServletRequest) {
return sessionRepository.getSessionOrNull(httpServletRequest);
public Session getAuthenticatedSession(HttpServletRequest httpServletRequest) {
Cookie sessionIdCookie = findSessionIdCookieFromRequestOrNull(httpServletRequest)
.orElseThrow(() -> new AuthException(AUTHENTICATION_FAILED));

return sessionRepository.getSession(sessionIdCookie.getValue());
}

public void logout(HttpServletRequest httpServletRequest) {
sessionRepository.expire(httpServletRequest);
private void setCookie(HttpServletResponse httpServletResponse, String sessionId) {
httpServletResponse.addCookie(new Cookie(SESSION_COOKIE_NAME, sessionId));
}

private Optional<Cookie> findSessionIdCookieFromRequestOrNull(HttpServletRequest httpServletRequest) {
Cookie[] cookies = httpServletRequest.getCookies();
if (cookies == null) {
return Optional.empty();
}

return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(SESSION_COOKIE_NAME))
.findAny();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.programmers.ticketparis.auth.service;

import java.time.Duration;
import java.time.LocalDateTime;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.programmers.ticketparis.auth.repository.localCashSessionRepository;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class SessionExpiryScheduler {

private final localCashSessionRepository sessionRepository;

@Scheduled(cron = "0 * * * * *") // 매 분마다 실행(정확히 0초가 될 때마다 실행되는 방식)
public void expireSessions() {
LocalDateTime now = LocalDateTime.now();
sessionRepository.getSessionLocalCash().values().removeIf
(
session -> Duration.between(session.getLastAccessTime(), now).toMinutes() > 30
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.programmers.ticketparis.auth.util;

import com.programmers.ticketparis.auth.dto.Session;

public class SessionThreadLocal {

private static final ThreadLocal<Session> sessionValueThreadLocal = new ThreadLocal<>();

public static Session getSessionValueDto() {
return sessionValueThreadLocal.get();
}

public static void setSessionValueDto(Session loggedInMemberInfo) {
sessionValueThreadLocal.set(loggedInMemberInfo);
}

public static void clear() {
sessionValueThreadLocal.remove();
}
}
Loading

0 comments on commit 430fea5

Please sign in to comment.