Skip to content

Commit

Permalink
feat: Global Configuration & Spring Security Set Up (#1)
Browse files Browse the repository at this point in the history
* chore: Init Configuration Setting By Submodule

* feat: update submodule

* feat: Global Configuration & Spring Security Set Up
  • Loading branch information
h-beeen authored Feb 12, 2024
1 parent c385227 commit ba2ec91
Show file tree
Hide file tree
Showing 48 changed files with 1,477 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "A11-Config"]
path = A11-Config
url = [email protected]:h-beeen/A11-Config.git
1 change: 1 addition & 0 deletions A11-Config
Submodule A11-Config added at 339325
41 changes: 41 additions & 0 deletions src/main/java/com/partybbangbbang/global/auditing/BaseEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.partybbangbbang.global.auditing;

import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

import static com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING;

@Getter
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public abstract class BaseEntity {

@Column(
nullable = false,
insertable = false,
updatable = false,
columnDefinition = "datetime default CURRENT_TIMESTAMP")
@CreatedDate
@JsonFormat(
shape = STRING,
pattern = "yyyy-MM-dd a HH:mm")
private LocalDateTime createdDate;

@Column(
nullable = false,
insertable = false,
columnDefinition = "datetime default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP")
@LastModifiedDate
@JsonFormat(
shape = STRING,
pattern = "yyyy-MM-dd a HH:mm")
private LocalDateTime updatedDate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.partybbangbbang.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class AuditingConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.partybbangbbang.global.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class QueryDslConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(this.entityManager);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.partybbangbbang.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.partybbangbbang.global.exception;

import com.partybbangbbang.global.exception.error.ErrorCode;
import com.partybbangbbang.global.exception.error.ErrorResponse;
import com.partybbangbbang.global.exception.error.GlobalError;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class ApiExceptionHandler {

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
return ErrorResponse.of(ex.getFieldErrors());
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
public ErrorResponse missingServletRequestParameterException() {
return ErrorResponse.of(GlobalError.INVALID_REQUEST_PARAM);
}

@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException exception) {
logBusinessException(exception);
return convert(exception.getErrorCode());
}

private ResponseEntity<ErrorResponse> convert(ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getStatus())
.body(ErrorResponse.of(errorCode));
}

private void logBusinessException(BusinessException exception) {
if (exception.getErrorCode().getStatus().is5xxServerError()) {
log.error("", exception);
} else {
log.error("error message = {}", exception.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.partybbangbbang.global.exception;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.partybbangbbang.global.exception.error.ErrorResponse;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class ApiExceptionHandlingFilter extends OncePerRequestFilter {

private final ObjectMapper om;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws ServletException, IOException {
try {
chain.doFilter(request, response);
} catch (BusinessException e) {
setErrorResponse(response, e);
}
}

private void setErrorResponse(
HttpServletResponse response,
BusinessException e
) {
try {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
om.writeValue(response.getOutputStream(), ErrorResponse.of(e.getErrorCode()));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.partybbangbbang.global.exception;

import com.partybbangbbang.global.exception.error.ErrorCode;
import lombok.Getter;

@Getter
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;

public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public static BusinessException of(ErrorCode errorCode) {
return new BusinessException(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.partybbangbbang.global.exception.error;

import org.springframework.http.HttpStatus;

public interface ErrorCode {

String getMessage();

HttpStatus getStatus();

String getCode();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.partybbangbbang.global.exception.error;

import lombok.Getter;
import org.springframework.validation.FieldError;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.partybbangbbang.global.exception.error.GlobalError.INVALID_REQUEST_PARAM;

@Getter
public class ErrorResponse {

private final String timeStamp;

private final String errorCode;

private final String errorMessage;

private final Object details;

private ErrorResponse(String errorCode, String errorMessage, Object details) {
this.timeStamp = LocalDateTime.now().toString();
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.details = details;
}

public static ErrorResponse of(ErrorCode errorCode) {
return new ErrorResponse(errorCode.getCode(), errorCode.getMessage(), null);
}

public static ErrorResponse of(
ErrorCode errorCode,
Object details
) {
return new ErrorResponse(errorCode.getCode(), errorCode.getMessage(), details);
}

public static ErrorResponse of(Optional<FieldError> fieldError) {
return fieldError.map(error -> new ErrorResponse(error.getCode(), error.getDefaultMessage(), null))
.orElseGet(() -> ErrorResponse.of(INVALID_REQUEST_PARAM));
}

public static ErrorResponse of(List<FieldError> fieldErrors) {
Map<String, String> errors = fieldErrors.stream()
.collect(Collectors.toMap(FieldError::getField, err -> err.getDefaultMessage() == null ? "null" : err.getDefaultMessage()));

return ErrorResponse.of(INVALID_REQUEST_PARAM, errors);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.partybbangbbang.global.exception.error;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;

@Getter
@RequiredArgsConstructor
public enum GlobalError implements ErrorCode {

GLOBAL_NOT_FOUND("리소스가 존재하지 않습니다.", NOT_FOUND, "G_001"),
INVALID_REQUEST_PARAM("요청 파라미터가 유효하지 않습니다.", BAD_REQUEST, "G_002");

private final String message;
private final HttpStatus status;
private final String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.partybbangbbang.global.security;

import com.partybbangbbang.global.exception.ApiExceptionHandlingFilter;
import com.partybbangbbang.global.security.matcher.CustomRequestMatcher;
import com.partybbangbbang.global.security.filter.CustomAuthorizationFilter;
import com.partybbangbbang.global.security.provider.CustomAuthenticationProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
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.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final ApiExceptionHandlingFilter apiExceptionHandlingFilter;
private final CustomAuthorizationFilter customAuthorizationFilter;
private final CustomRequestMatcher customRequestMatcher;

@Bean
@Order(0)
public SecurityFilterChain authFilterChain(HttpSecurity http) throws Exception {
http.securityMatchers(matcher -> matcher.requestMatchers(
customRequestMatcher.authEndpoints(),
customRequestMatcher.errorEndpoints(),
customRequestMatcher.userEndpoints()
))
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.addFilterBefore(apiExceptionHandlingFilter, UsernamePasswordAuthenticationFilter.class);

return commonHttpSecurity(http).build();
}

@Bean
@Order(1)
public SecurityFilterChain anyRequestFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/api/v1/tteokguk/find/**")).hasAnyRole("ANONYMOUS", "USER")
.anyRequest().hasRole("USER"))
.addFilterAfter(customAuthorizationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(apiExceptionHandlingFilter, UsernamePasswordAuthenticationFilter.class);

return commonHttpSecurity(http).build();
}

private HttpSecurity commonHttpSecurity(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(configurer -> corsConfigurationSource())
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable);
}

@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(CustomAuthenticationProvider authenticationProvider) {
ProviderManager providerManager = new ProviderManager(authenticationProvider);
providerManager.setEraseCredentialsAfterAuthentication(false);
return providerManager;
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(List.of("HEAD", "POST", "GET", "DELETE", "PUT"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Loading

0 comments on commit ba2ec91

Please sign in to comment.