Skip to content

Commit

Permalink
Merge pull request #15 from mju-likelion/feature/spring-security-#12
Browse files Browse the repository at this point in the history
Feature/#12 Spring Security 적용
  • Loading branch information
Dh3356 authored Feb 25, 2024
2 parents 65eaf27 + 09b9001 commit 03e09fc
Show file tree
Hide file tree
Showing 23 changed files with 637 additions and 19 deletions.
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ dependencies {
compileOnly 'org.projectlombok:lombok'//Lombok
annotationProcessor 'org.projectlombok:lombok'//Lombok
implementation 'org.springframework.boot:spring-boot-starter-validation'//Validation(Dto)
implementation 'org.springframework.boot:spring-boot-starter-security'//Spring Security
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'//JWT
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'//JWT
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'//JWT
implementation 'javax.xml.bind:jaxb-api:2.3.1'//JAXB
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
34 changes: 34 additions & 0 deletions src/main/java/org/mjulikelion/baker/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.mjulikelion.baker.config;

import static org.mjulikelion.baker.constant.SecurityConstant.ALL;
import static org.mjulikelion.baker.constant.SecurityConstant.ALL_PATH;

import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
@Value("${client.additional.host}")
private List<String> clientAdditionalHost;
@Value("${client.host}")
private String clientHost;

@Bean
public CorsFilter corsFilter() {
List<String> clientHosts = clientAdditionalHost;
clientHosts.add(clientHost);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(clientHosts);
config.addAllowedHeader(ALL);
config.addAllowedMethod(ALL);
source.registerCorsConfiguration(ALL_PATH, config);
return new CorsFilter(source);
}
}
106 changes: 106 additions & 0 deletions src/main/java/org/mjulikelion/baker/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.mjulikelion.baker.config;

import static org.hibernate.type.descriptor.java.IntegerJavaType.ZERO;
import static org.mjulikelion.baker.constant.SecurityConstant.ACCESS_TOKEN;
import static org.mjulikelion.baker.constant.SecurityConstant.ALL_PATH;
import static org.mjulikelion.baker.constant.SecurityConstant.CONTENT_TYPE;
import static org.mjulikelion.baker.errorcode.ErrorCode.ACCESS_DENIED_ERROR;
import static org.mjulikelion.baker.errorcode.ErrorCode.UNAUTHORIZED_ERROR;
import static org.mjulikelion.baker.model.Role.ROLE_ADMIN;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.mjulikelion.baker.dto.response.ResponseDto;
import org.mjulikelion.baker.errorcode.ErrorCode;
import org.mjulikelion.baker.filter.JwtAuthenticationExceptionFilter;
import org.mjulikelion.baker.filter.JwtFilter;
import org.mjulikelion.baker.util.security.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {
private final JwtAuthenticationExceptionFilter jwtAuthenticationExceptionFilter;
private final JwtTokenProvider jwtTokenProvider;
private final CorsConfig corsConfig;
@Value("${security.permit-all.url}")
private String[] permitAllUrl;
@Value("${security.logout.url}")
private String logoutUrl;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(permitAllUrl).permitAll()
.requestMatchers(ALL_PATH).hasRole(ROLE_ADMIN.getRoleName())
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilter(corsConfig.corsFilter())
.addFilterBefore(new JwtFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationExceptionFilter, JwtFilter.class)
.exceptionHandling(exceptionHandling ->
exceptionHandling.authenticationEntryPoint(
(request, response, authException) -> makeResponse(response, UNAUTHORIZED_ERROR)))
.exceptionHandling(exceptionHandling ->
exceptionHandling.accessDeniedHandler(
(request, response, accessDeniedException) -> makeResponse(response,
ACCESS_DENIED_ERROR)))
.logout(logout -> {
logout
.logoutUrl(logoutUrl)
.logoutSuccessHandler((request, response, authentication) -> {
Cookie cookie = new Cookie(ACCESS_TOKEN, null);
cookie.setMaxAge(ZERO);
cookie.setHttpOnly(true);
cookie.setPath(ALL_PATH);
response.addCookie(cookie);

this.makeResponse(response, HttpStatus.OK, "로그아웃 되었습니다.");
})
.invalidateHttpSession(true);
})
.build();
}

private void makeResponse(HttpServletResponse response, HttpStatus status, String message)
throws IOException {
String jsonResponse = new ObjectMapper().writeValueAsString(
ResponseDto.res(status, message));
response.setStatus(status.value());
response.setContentType(CONTENT_TYPE);
response.getWriter().write(jsonResponse);
response.getWriter().flush();
}

private void makeResponse(HttpServletResponse response, ErrorCode errorCode)
throws IOException {
String jsonResponse = new ObjectMapper().writeValueAsString(
ResponseDto.res(errorCode.getCode(), errorCode.getMessage()));
response.setStatus(Integer.parseInt(errorCode.getCode().substring(0, 3)));
response.setContentType(CONTENT_TYPE);
response.getWriter().write(jsonResponse);
response.getWriter().flush();
}
}
8 changes: 8 additions & 0 deletions src/main/java/org/mjulikelion/baker/constant/EtcConstant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.mjulikelion.baker.constant;

public class EtcConstant {
public static final String COLON = " : ";
public static final String COMMA = ",";
public static final String BLANK = "";
public static final String WHITE_SPACE = " ";
}
12 changes: 12 additions & 0 deletions src/main/java/org/mjulikelion/baker/constant/SecurityConstant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.mjulikelion.baker.constant;

public class SecurityConstant {
public static final String AUTH = "auth";
public static final String BEARER = "Bearer ";
public static final String BEARER_WITHOUT_SPACE = "Bearer";
public static final String ACCESS_TOKEN = "accessToken";
public static final String ALL_PATH = "/**";
public static final String ROOT_PATH = "/";
public static final String CONTENT_TYPE = "application/json;charset=UTF-8";
public static final String ALL = "*";
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.mjulikelion.baker.controller;

import lombok.AllArgsConstructor;
import org.mjulikelion.baker.Service.ApplicationService;
import org.mjulikelion.baker.dto.response.ResponseDto;
import org.mjulikelion.baker.dto.response.application.ApplicationPageGetResponseData;
import org.mjulikelion.baker.service.application.ApplicationQueryService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -14,12 +14,12 @@
@RestController
@RequestMapping("application")
public class ApplicationController {
private final ApplicationService applicationService;
private final ApplicationQueryService applicationQueryService;

@GetMapping()
public ResponseEntity<ResponseDto<ApplicationPageGetResponseData>> getApplications(
@RequestParam(value = "part") String part,
@RequestParam(value = "pageNum") int pageNum) {
return applicationService.getApplications(part, --pageNum);// pageNum은 1부터 시작하니 0부터 시작하는 페이지로 변환
return applicationQueryService.getApplications(part, --pageNum);// pageNum은 1부터 시작하니 0부터 시작하는 페이지로 변환
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package org.mjulikelion.baker.controller;

import static org.mjulikelion.baker.constant.EtcConstant.COLON;
import static org.mjulikelion.baker.dto.response.ResponseDto.getFromCustomException;

import lombok.extern.slf4j.Slf4j;
import org.mjulikelion.baker.dto.response.ResponseDto;
import org.mjulikelion.baker.errorcode.ErrorCode;
import org.mjulikelion.baker.errorcode.ValidationErrorCode;
import org.mjulikelion.baker.exception.AuthenticationException;
import org.mjulikelion.baker.exception.InvalidDataException;
import org.mjulikelion.baker.exception.JwtException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand All @@ -30,7 +35,7 @@ public ResponseEntity<ResponseDto<Void>> handleMissingServletRequestParameterExc
log.error("MissingServletRequestParameterException: {}", missingServletRequestParameterException.getMessage());
ErrorCode errorCode = ErrorCode.PARAM_NOT_FOUND_ERROR;
String code = errorCode.getCode();
String message = errorCode.getMessage() + " : " + missingServletRequestParameterException.getParameterName();
String message = errorCode.getMessage() + COLON + missingServletRequestParameterException.getParameterName();
return new ResponseEntity<>(ResponseDto.res(code, message), HttpStatus.BAD_REQUEST);
}

Expand All @@ -48,7 +53,7 @@ public ResponseEntity<ResponseDto<Void>> handleMethodArgumentNotValidException(
}
ValidationErrorCode validationErrorCode = ValidationErrorCode.resolveAnnotation(fieldError.getCode());
String code = validationErrorCode.getCode();
String message = validationErrorCode.getMessage() + " : " + fieldError.getDefaultMessage();
String message = validationErrorCode.getMessage() + COLON + fieldError.getDefaultMessage();
return new ResponseEntity<>(ResponseDto.res(code, message), HttpStatus.BAD_REQUEST);
}

Expand All @@ -60,7 +65,7 @@ public ResponseEntity<ResponseDto<Void>> handleHandlerMethodValidationException(
log.error("HandlerMethodValidationException: {}", handlerMethodValidationException.getMessage());
ValidationErrorCode errorCode = ValidationErrorCode.VALIDATION;
String code = errorCode.getCode();
String message = errorCode.getMessage() + " : "
String message = errorCode.getMessage() + COLON
+ handlerMethodValidationException.getDetailMessageArguments()[0].toString();
return new ResponseEntity<>(ResponseDto.res(code, message), HttpStatus.BAD_REQUEST);
}
Expand All @@ -73,7 +78,7 @@ public ResponseEntity<ResponseDto<Void>> handleHttpMessageNotReadableException(
log.error("HttpMessageNotReadableException: {}", httpMessageNotReadableException.getMessage());
ErrorCode errorCode = ErrorCode.INVALID_REQUEST_FORMAT_ERROR;
String code = errorCode.getCode();
String message = errorCode.getMessage() + " : " + httpMessageNotReadableException.getMessage();
String message = errorCode.getMessage() + COLON + httpMessageNotReadableException.getMessage();
return new ResponseEntity<>(ResponseDto.res(code, message), HttpStatus.BAD_REQUEST);
}

Expand Down Expand Up @@ -108,7 +113,7 @@ public ResponseEntity<ResponseDto<Void>> handleException(Exception exception) {
log.error("InternalServerException: {}", exception.getMessage());
ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR;
String code = errorCode.getCode();
String message = errorCode.getMessage() + " : " + exception.getClass();
String message = errorCode.getMessage() + COLON + exception.getClass();
return new ResponseEntity<>(ResponseDto.res(code, message), HttpStatus.INTERNAL_SERVER_ERROR);
}

Expand All @@ -117,9 +122,26 @@ public ResponseEntity<ResponseDto<Void>> handleException(Exception exception) {
@ExceptionHandler(InvalidDataException.class)
public ResponseEntity<ResponseDto<Void>> handleInvalidDataException(InvalidDataException invalidDataException) {
log.error("InvalidDataException: {}", invalidDataException.getMessage());
ErrorCode errorCode = invalidDataException.getErrorCode();
String code = errorCode.getCode();
String message = errorCode.getMessage();
return new ResponseEntity<>(ResponseDto.res(code, message), HttpStatus.BAD_REQUEST);
ResponseDto<Void> responseDto = getFromCustomException(invalidDataException);
return new ResponseEntity<>(responseDto, HttpStatus.BAD_REQUEST);
}

// JwtException 예외를 처리하는 핸들러(Jwt 토큰이 유효하지 않은 경우)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(JwtException.class)
public ResponseEntity<ResponseDto<Void>> handleJwtException(JwtException jwtException) {
log.error("JwtException: {}", jwtException.getMessage());
ResponseDto<Void> responseDto = getFromCustomException(jwtException);
return new ResponseEntity<>(responseDto, HttpStatus.UNAUTHORIZED);
}

// AuthenticationException 예외를 처리하는 핸들러(인증에 실패한 경우)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ResponseDto<Void>> handleAuthenticationException(
AuthenticationException authenticationException) {
log.error("AuthenticationException: {}", authenticationException.getMessage());
ResponseDto<Void> responseDto = getFromCustomException(authenticationException);
return new ResponseEntity<>(responseDto, HttpStatus.UNAUTHORIZED);
}
}
11 changes: 11 additions & 0 deletions src/main/java/org/mjulikelion/baker/dto/response/ResponseDto.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package org.mjulikelion.baker.dto.response;

import static org.mjulikelion.baker.constant.EtcConstant.COLON;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.mjulikelion.baker.exception.CustomException;
import org.springframework.http.HttpStatusCode;

@Data
Expand Down Expand Up @@ -46,4 +49,12 @@ public static <T> ResponseDto<T> res(final HttpStatusCode statusCode, final Stri
.message(resultMsg)
.build();
}

public static ResponseDto<Void> getFromCustomException(CustomException e) {
if (e.getMessage() == null) {
return ResponseDto.res(e.getErrorCode().getCode(), e.getErrorCode().getMessage());
}
return ResponseDto.res(e.getErrorCode().getCode(),
e.getErrorCode().getMessage() + COLON + e.getMessage());
}
}
17 changes: 15 additions & 2 deletions src/main/java/org/mjulikelion/baker/errorcode/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,21 @@ public enum ErrorCode {
INVALID_REQUEST_FORMAT_ERROR("4000", "유효하지 않은 Body 형식입니다."),//요청 형식이 잘못됨
INVALID_PART_ERROR("4001", "유효하지 않은 파트입니다."),//유효하지 않은 파트
INVALID_PAGE_ERROR("4002", "페이지가 유효하지 않습니다."),//페이지가 유효하지 않음
PARAM_NOT_FOUND_ERROR("4003", "필수 파라미터가 누락되었습니다.");//필수 파라미터가 누락됨

PARAM_NOT_FOUND_ERROR("4003", "필수 파라미터가 누락되었습니다."),//필수 파라미터가 누락됨
//인증/권한 오류들
AUTHENTICATION_ERROR("4010", "인증 오류입니다."),//인증 오류
AUTHENTICATION_NOT_FOUND_ERROR("4011", "인증 정보를 찾을 수 없습니다."),//인증 정보를 찾을 수 없음
UNAUTHORIZED_ERROR("4030", "권한이 없습니다."),//권한이 없음
ACCESS_DENIED_ERROR("4031", "접근이 거부되었습니다."),//접근이 거부됨
//JWT 토큰 에러
JWT_TOKEN_ERROR("8000", "JWT 권한 정보 검증 오류."),//JWT 토큰 에러
JWT_TOKEN_PROVIDER_ERROR("8001", "JWT 토큰 제공자 오류."),//JWT 토큰 제공자 에러
JWT_INVALID_TOKEN_ERROR("8002", "유효하지 않은 JWT 토큰입니다."),//유효하지 않은 JWT 토큰
JWT_EXPIRED_TOKEN_ERROR("8003", "만료된 JWT 토큰입니다."),//만료된 JWT 토큰
JWT_UNSUPPORTED_TOKEN_ERROR("8004", "지원되지 않는 JWT 토큰입니다."),//지원되지 않는 JWT 토큰
JWT_CLAIMS_STRING_ERROR("8005", "JWT 클레임 문자열이 비어있습니다."),//JWT 클레임 문자열이 비어있음
JWT_UNKNOWN_ERROR("8006", "알 수 없는 JWT 토큰 에러입니다."),//알 수 없는 JWT 토큰 에러
JWT_NOT_FOUND_ERROR("8007", "JWT 토큰을 찾을 수 없습니다.");//JWT 토큰을 찾을 수 없음

private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.mjulikelion.baker.exception;

import org.mjulikelion.baker.errorcode.ErrorCode;

public class AuthenticationException extends CustomException {
public AuthenticationException(ErrorCode errorCode, String message) {
super(errorCode, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.mjulikelion.baker.exception;

import org.mjulikelion.baker.errorcode.ErrorCode;

public class JwtException extends CustomException {
public JwtException(ErrorCode errorCode, String message) {
super(errorCode, message);
}
}
Loading

0 comments on commit 03e09fc

Please sign in to comment.