Skip to content

Commit

Permalink
Merge pull request #90 from prgrms-be-devcourse/feature/#73
Browse files Browse the repository at this point in the history
[#73] 사용자 인증 기능 구현 - 2
  • Loading branch information
charlesuu committed Sep 21, 2023
2 parents 86fe0e0 + 9fe6b86 commit 606293f
Show file tree
Hide file tree
Showing 32 changed files with 628 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.programmers.ticketparis.auth.controller;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.programmers.ticketparis.auth.dto.LoginRequest;
import com.programmers.ticketparis.auth.dto.LoginSuccessResponse;
import com.programmers.ticketparis.auth.dto.LogoutSuccessResponse;
import com.programmers.ticketparis.auth.service.AuthService;

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

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class AuthController {

private final AuthService authService;

@PostMapping("/customers/login")
@ResponseStatus(HttpStatus.CREATED)
public LoginSuccessResponse customerLogin(@Valid @RequestBody LoginRequest loginRequest,
HttpServletResponse httpServletResponse) {
authService.customerLogin(loginRequest, httpServletResponse);

return new LoginSuccessResponse();
}

@PostMapping("/sellers/login")
@ResponseStatus(HttpStatus.CREATED)
public LoginSuccessResponse sellerLogin(@Valid @RequestBody LoginRequest loginRequest,
HttpServletResponse httpServletResponse) {
authService.sellerLogin(loginRequest, httpServletResponse);

return new LoginSuccessResponse();
}

@PostMapping("/logout")
@ResponseStatus(HttpStatus.OK)
public LogoutSuccessResponse logout(HttpServletRequest httpServletRequest) {
authService.logout(httpServletRequest);

return new LogoutSuccessResponse();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.programmers.ticketparis.member.dto;
package com.programmers.ticketparis.auth.dto;

import com.programmers.ticketparis.member.dto.validator.PasswordValid;
import com.programmers.ticketparis.member.dto.validator.UsernameValid;
Expand All @@ -9,11 +9,20 @@

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CustomerLoginForm {
public class LoginRequest {

@UsernameValid(message = "아이디는 8자 이상 15자 이하(영어, 숫자, 공백불가)")
private String username;

@PasswordValid(message = "비밀번호는 8자 이상 20자 이하 (<영어, 숫자, 특수문자> 포함, 공백불가)")
private String password;

private LoginRequest(String username, String password) {
this.username = username;
this.password = password;
}

public static LoginRequest of(String username, String password) {
return new LoginRequest(username, password);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.programmers.ticketparis.auth.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class LoginSuccessResponse {

private String login = "로그인 성공";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.programmers.ticketparis.auth.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class LogoutSuccessResponse {

private String logout = "로그아웃 성공";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.programmers.ticketparis.auth.dto;

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

import lombok.Getter;

@Getter
public class SessionValueDto {

private MemberRule memberRule;
private Long memberId;

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

public static SessionValueDto of(MemberRule memberRule, Long memberId) {
return new SessionValueDto(memberRule, memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.programmers.ticketparis.auth.exception;

import com.programmers.ticketparis.common.exception.BusinessException;
import com.programmers.ticketparis.common.exception.ExceptionRule;

public class AuthException extends BusinessException {

public AuthException(ExceptionRule rule, Object... rejectedValues) {
super(rule, rejectedValues);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.programmers.ticketparis.auth.mvc.filter;

import java.io.IOException;

import org.springframework.util.PatternMatchUtils;

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

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class AuthenticationFilter implements Filter {

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

private final AuthService authService;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
IOException, ServletException {

HttpServletRequest httpServletRequest = (HttpServletRequest)request;

String requestURI = httpServletRequest.getRequestURI();

if (isLoginCheckPath(requestURI)) {
SessionValueDto sessionValueDto = authService.getSessionOrNull(httpServletRequest);
if (sessionValueDto == null) {
throw new AuthException(ExceptionRule.AUTHENTICATION_FAILED);
}
}

chain.doFilter(request, response);
}

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

import java.io.IOException;

import org.springframework.http.MediaType;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.programmers.ticketparis.auth.exception.AuthException;
import com.programmers.ticketparis.common.dto.ApiResponse;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class ExceptionHandlerFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
IOException,
ServletException {
try {
chain.doFilter(request, response);
} catch (AuthException e) {
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;

setAuthExceptionResponse(httpServletRequest, httpServletResponse, e);
}
}

private void setAuthExceptionResponse(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, AuthException e) throws
IOException {
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setStatus(e.getExceptionRule().getStatus().value());

try (ServletOutputStream outputStream = httpServletResponse.getOutputStream()) {
ApiResponse<Object> apiResponse = ApiResponse.builder()
.path(httpServletRequest.getRequestURI())
.message(e.getExceptionRule().getMessage())
.build();

ObjectMapper objectMapper = new ObjectMapper();
//(ApiResponse 필드인 TimeStamp 해석을 위한 모듈 등록)
objectMapper.registerModule(new JavaTimeModule());
objectMapper.writeValue(outputStream, apiResponse);

outputStream.flush();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.programmers.ticketparis.auth.repository;

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

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

public interface SessionRepository {

void createSession(SessionValueDto value, HttpServletResponse httpServletResponse);

SessionValueDto getSessionOrNull(HttpServletRequest httpServletRequest);

void expire(HttpServletRequest httpServletRequest);

}


Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.programmers.ticketparis.auth.repository;

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

import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.stereotype.Component;

import com.programmers.ticketparis.auth.dto.SessionValueDto;
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;

@Component
public class localCashSessionRepository implements SessionRepository {

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

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

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

public SessionValueDto getSessionOrNull(HttpServletRequest httpServletRequest) {
Cookie sessionCookie = findCookieOrNull(httpServletRequest);
if (sessionCookie == null) {
return null;
}

return sessionLocalCash.get(sessionCookie.getValue());
}

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

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

private Cookie findCookieOrNull(HttpServletRequest httpServletRequest) {
if (httpServletRequest.getCookies() == null) {
return null;
}

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

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.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.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.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

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

private final CustomerService customerService;
private final SellerService sellerService;
private final SessionRepository sessionRepository;

public void customerLogin(LoginRequest loginRequest, HttpServletResponse httpServletResponse) {
Customer customer = customerService.findCustomerByUsername(loginRequest.getUsername());
if (!customer.checkPassword(loginRequest.getPassword())) {
throw new CustomerException(ExceptionRule.LOGIN_FAILED_PASSWORD_INVALID, loginRequest.getPassword());
}

sessionRepository.createSession(SessionValueDto.of(MemberRule.CUSTOMER, customer.getCustomerId()),
httpServletResponse);
}

public void sellerLogin(LoginRequest loginRequest, HttpServletResponse httpServletResponse) {
Seller seller = sellerService.findSellerByUsername(loginRequest.getUsername());
if (!seller.checkPassword(loginRequest.getPassword())) {
throw new SellerException(ExceptionRule.LOGIN_FAILED_PASSWORD_INVALID, loginRequest.getPassword());
}

sessionRepository.createSession(SessionValueDto.of(MemberRule.SELLER, seller.getSellerId()),
httpServletResponse);
}

//세션 조회 기능 : 컨트롤러가 아닌 필터, 인터셉터에서 호출
public SessionValueDto getSessionOrNull(HttpServletRequest httpServletRequest) {
return sessionRepository.getSessionOrNull(httpServletRequest);
}

public void logout(HttpServletRequest httpServletRequest) {
sessionRepository.expire(httpServletRequest);
}
}
Loading

0 comments on commit 606293f

Please sign in to comment.