Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 대기열 구축 #150

Merged
merged 22 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/api-booking/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

//TODO: security 의존성 core-security 에서 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.pgms.apibooking.common.exception;

import java.io.IOException;

import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.pgms.coredomain.response.ErrorResponse;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class BookingAuthEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;

public BookingAuthEntryPoint() {
this.objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
}

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
BookingErrorCode errorCode = BookingErrorCode.BOOKING_TOKEN_NOT_EXIST;
response.setStatus(errorCode.getStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);

ErrorResponse errorResponse = new ErrorResponse(errorCode.getCode(), errorCode.getMessage());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pgms.apibooking.exception;
package com.pgms.apibooking.common.exception;

import org.springframework.http.HttpStatus;

Expand Down Expand Up @@ -31,7 +31,12 @@ public enum BookingErrorCode {
ACCOUNT_TRANSFER_ERROR(HttpStatus.BAD_REQUEST, "ACCOUNT_TRANSFER_ERROR", "계좌 송금 오류가 발생했습니다. 확인 후 다시 시도해주세요."),
TOSS_PAYMENTS_ERROR(HttpStatus.BAD_REQUEST, "TOSS_PAYMENTS_CLIENT_ERROR", "Toss Payments API 오류가 발생했습니다."),

BOOKING_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOKING_NOT_FOUND", "존재하지 않는 예매입니다.");
BOOKING_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOKING_NOT_FOUND", "존재하지 않는 예매입니다."),

BOOKING_SESSION_ID_NOT_EXIST(HttpStatus.BAD_REQUEST, "BOOKING_SESSION_ID_NOT_EXIST", "예매 세션 ID가 존재하지 않습니다."),
BOOKING_TOKEN_NOT_EXIST(HttpStatus.UNAUTHORIZED, "BOOKING_TOKEN_NOT_EXIST", "예매 토큰이 존재하지 않습니다."),
INVALID_BOOKING_TOKEN(HttpStatus.UNAUTHORIZED, "INVALID_BOOKING_TOKEN", "올바르지 않은 예매 토큰입니다."),
OUT_OF_ORDER(HttpStatus.BAD_REQUEST, "OUT_OF_ORDER", "예매 순서가 아닙니다.");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pgms.apibooking.exception;
package com.pgms.apibooking.common.exception;

import lombok.Getter;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pgms.apibooking.exception;
package com.pgms.apibooking.common.exception;

import java.util.Objects;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.pgms.apibooking.common.exception;

import java.io.IOException;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.pgms.coredomain.response.ErrorResponse;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class BookingExceptionHandlerFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper;

public BookingExceptionHandlerFilter() {
this.objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (BookingException e) {
BookingErrorCode bookingErrorCode = e.getErrorCode();
sendFailResponse(response, bookingErrorCode);
} catch (Exception e) {
log.error(e.getMessage(), e);
BookingErrorCode bookingErrorCode = BookingErrorCode.INTERNAL_SERVER_ERROR;
sendFailResponse(response, bookingErrorCode);
}
}

private void sendFailResponse(HttpServletResponse response, BookingErrorCode bookingErrorCode) throws IOException {
response.setStatus(bookingErrorCode.getStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);

ErrorResponse errorResponse = new ErrorResponse(bookingErrorCode.getCode(), bookingErrorCode.getMessage());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.pgms.apibooking.common.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import com.pgms.apibooking.common.exception.BookingErrorCode;
import com.pgms.apibooking.common.exception.BookingException;

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

@Component
public class BookingSessionInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
Exception {
String bookingSessionId = request.getHeader("Booking-Session-Id");
if (bookingSessionId == null || bookingSessionId.isBlank()) {
throw new BookingException(BookingErrorCode.BOOKING_SESSION_ID_NOT_EXIST);
}

request.setAttribute("bookingSessionId", bookingSessionId);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.pgms.apibooking.common.jwt;

import org.springframework.security.authentication.AbstractAuthenticationToken;

public class BookingAuthToken extends AbstractAuthenticationToken {

private final String sessionId;

public BookingAuthToken(String sessionId) {
super(null);
this.sessionId = sessionId;
super.setAuthenticated(true);
}

@Override
public Object getCredentials() {
return null;
}

@Override
public Object getPrincipal() {
return this.sessionId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.pgms.apibooking.common.jwt;

import java.io.IOException;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.pgms.apibooking.common.exception.BookingErrorCode;
import com.pgms.apibooking.common.exception.BookingException;

import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
@RequiredArgsConstructor
public class BookingJwtAuthFilter extends OncePerRequestFilter {

private final BookingJwtProvider bookingJwtProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = parseToken(request);

if (token != null) {
try {
BookingJwtPayload authentication = bookingJwtProvider.validateAndParsePayload(token);
BookingAuthToken bookingAuthToken = new BookingAuthToken(authentication.getSessionId());
SecurityContextHolder.getContext().setAuthentication(bookingAuthToken);
} catch (JwtException | BookingException e) {
log.warn(e.getMessage(), e);
throw new BookingException(BookingErrorCode.INVALID_BOOKING_TOKEN);
}
}

filterChain.doFilter(request, response);
}

private String parseToken(HttpServletRequest request) {
String headerAuth = request.getHeader("Booking-Authorization");
if (headerAuth != null && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.pgms.apibooking.common.jwt;

import java.util.HashMap;
import java.util.Map;

import jakarta.validation.Payload;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class BookingJwtPayload implements Payload {

private final String sessionId;

public Map<String, String> toMap() {
return new HashMap<>() {{
put("sessionId", sessionId);
}};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.pgms.apibooking.common.jwt;

import java.util.Date;

import javax.crypto.SecretKey;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.AllArgsConstructor;

@AllArgsConstructor
public class BookingJwtProvider {
private final String issuer;
private final SecretKey secretKey;
private final Long expirySeconds;

public String generateToken(BookingJwtPayload payload) {
Long now = System.currentTimeMillis();
Date expirationMilliSeconds = new Date(now + expirySeconds * 1000);

return Jwts.builder()
.issuer(issuer)
.issuedAt(new Date(now))
.expiration(expirationMilliSeconds)
.claims(payload.toMap())
.signWith(secretKey)
.compact();
}

public BookingJwtPayload validateAndParsePayload(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
return new BookingJwtPayload(claims.get("sessionId", String.class));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pgms.apibooking.util;
package com.pgms.apibooking.common.util;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.pgms.apibooking.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.pgms.apibooking.common.jwt.BookingJwtProvider;

import io.jsonwebtoken.security.Keys;
import lombok.Getter;

@Getter
@Configuration
public class JwtConfig {

@Value("${jwt.issuer}")
private String issuer;

@Value("${jwt.secret-key}")
private String secretKey;

@Value("${jwt.expiry-seconds}")
private Long expirySeconds;

@Bean
public BookingJwtProvider jwtProvider() {
return new BookingJwtProvider(
issuer,
Keys.hmacShaKeyFor(secretKey.getBytes()),
expirySeconds
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.pgms.apibooking.config;

import java.util.List;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import com.pgms.apibooking.common.exception.BookingExceptionHandlerFilter;
import com.pgms.apibooking.common.exception.BookingAuthEntryPoint;
import com.pgms.apibooking.common.jwt.BookingJwtAuthFilter;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final BookingJwtAuthFilter bookingJwtAuthFilter;
private final BookingExceptionHandlerFilter bookingExceptionHandlerFilter;
private final BookingAuthEntryPoint bookingAuthEntryPoint;

@Bean
public SecurityFilterChain bookingFilterChain(HttpSecurity http) throws Exception {
List<RequestMatcher> permitAllMatchers = List.of(
new AntPathRequestMatcher("/api/*/bookings/enter-queue", HttpMethod.POST.toString()),
new AntPathRequestMatcher("/api/*/bookings/order-in-queue", HttpMethod.GET.toString()),
new AntPathRequestMatcher("/api/*/bookings/issue-token", HttpMethod.GET.toString())
);

return http
.csrf(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(PathRequest.toH2Console()).permitAll()
.requestMatchers(permitAllMatchers.toArray(new RequestMatcher[0])).permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(bookingAuthEntryPoint)
)
.sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(bookingJwtAuthFilter, BasicAuthenticationFilter.class)
.addFilterBefore(bookingExceptionHandlerFilter, BookingJwtAuthFilter.class)
.build();
}
}
Loading