Skip to content

Commit

Permalink
Merge pull request #16 from Team-Shaka/feat/2
Browse files Browse the repository at this point in the history
✨  Feat : spring security 추가
  • Loading branch information
CYY1007 authored Apr 25, 2024
2 parents e8997f0 + e6febfd commit a56f56d
Show file tree
Hide file tree
Showing 10 changed files with 518 additions and 0 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

testImplementation 'org.springframework.boot:spring-boot-starter-test'

// spring security
implementation 'org.springframework.boot:spring-boot-starter-security'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

tasks.named('bootBuildImage') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@
@RequiredArgsConstructor
public enum GlobalErrorCode implements BaseErrorCode{

// 401 Unauthorized - 권한 없음
TOKEN_EXPIRED(UNAUTHORIZED, "AUTH401_1", "인증 토큰이 만료 되었습니다. 토큰을 재발급 해주세요"),
INVALID_TOKEN(UNAUTHORIZED, "AUTH401_2", "인증 토큰이 유효하지 않습니다."),
INVALID_REFRESH_TOKEN(UNAUTHORIZED, "AUTH401_3", "리프레시 토큰이 유효하지 않습니다."),
REFRESH_TOKEN_EXPIRED(UNAUTHORIZED, "AUTH401_4", "리프레시 토큰이 만료 되었습니다."),
AUTHENTICATION_REQUIRED(UNAUTHORIZED, "AUTH401_5", "인증 정보가 유효하지 않습니다."),
LOGIN_REQUIRED(UNAUTHORIZED, "AUTH401_6", "로그인이 필요한 서비스입니다."),

// 403 Forbidden - 인증 거부
AUTHENTICATION_DENIED(FORBIDDEN, "AUTH403_1", "인증이 거부 되었습니다."),

// 404 Not Found - 찾을 수 없음
REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "AUTH404_1", "리프레시 토큰이 존재하지 않습니다."),

// 500 Server Error
SERVER_ERROR(INTERNAL_SERVER_ERROR, "GLOBAL500_1", "서버 에러, 서버 개발자에게 알려주세요."),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package treehouse.server.global.exception;

import org.springframework.security.core.AuthenticationException;

public class JwtAuthenticationException extends AuthenticationException {

public JwtAuthenticationException(GlobalErrorCode code) {
super(code.name());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package treehouse.server.global.security.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
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;
import org.springframework.web.cors.CorsConfigurationSource;
import treehouse.server.global.security.filter.JwtAuthFilter;
import treehouse.server.global.security.handler.JwtAccessDeniedHandler;
import treehouse.server.global.security.handler.JwtAuthenticationEntryPoint;
import treehouse.server.global.security.handler.JwtAuthenticationExceptionHandler;
import treehouse.server.global.security.provider.TokenProvider;

import java.util.Collections;

import static org.springframework.security.config.Customizer.withDefaults;

@Slf4j
@RequiredArgsConstructor
//@EnableWebSecurity(debug = true)
@Configuration
public class SecurityConfig {

private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint = new JwtAuthenticationEntryPoint();

private final JwtAccessDeniedHandler jwtAccessDeniedHandler = new JwtAccessDeniedHandler();

private final TokenProvider tokenProvider;

private final JwtAuthenticationExceptionHandler jwtAuthenticationExceptionHandler =
new JwtAuthenticationExceptionHandler();

private static final String[] JWT_WHITE_LIST ={
"/users/login-tmp","/users/reissue"
};

/**
* 특정 경로에 대한 보안 설정을 무시하도록 설정
* @return WebSecurityCustomizer
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) ->
web.ignoring()
.requestMatchers(
"/health",
"/schedule",
"/v3/api-docs",
"/v3/api-docs/**",
"/favicon.io",
"/swagger-ui/**",
"/docs/**");
}

@Bean
public SecurityFilterChain JwtFilterChain(HttpSecurity http) throws Exception {
return http.cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfiguration()))
.httpBasic(withDefaults())
.csrf(AbstractHttpConfigurer::disable) // 비활성화
.sessionManagement(
manage ->
manage.sessionCreationPolicy(
SessionCreationPolicy.STATELESS)) // Session 사용 안함
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
authorize -> {
// authorize.requestMatchers("/swagger-ui/**").permitAll();
authorize.requestMatchers("/users/**").permitAll();
authorize.anyRequest().authenticated();
})
.exceptionHandling(
exceptionHandling ->
exceptionHandling
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler))
.addFilterBefore(
new JwtAuthFilter(tokenProvider, JWT_WHITE_LIST),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationExceptionHandler, JwtAuthFilter.class)
.build();
}

public CorsConfigurationSource corsConfiguration() {
return request -> {
org.springframework.web.cors.CorsConfiguration config =
new org.springframework.web.cors.CorsConfiguration();
config.setAllowedHeaders(Collections.singletonList("*")); // 모든 헤더 허용
config.setAllowedMethods(Collections.singletonList("*")); // 모든 메소드 허용
config.setAllowedOriginPatterns(Collections.singletonList("*")); // 모든 Origin 허용
config.setAllowCredentials(true); // 인증정보 허용
return config;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package treehouse.server.global.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
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;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import treehouse.server.global.common.CommonResponse;
import treehouse.server.global.exception.GlobalErrorCode;
import treehouse.server.global.security.provider.TokenProvider;

import java.io.IOException;
import java.util.Arrays;

@Slf4j
@RequiredArgsConstructor
// 들어오는 요청 처리
public class JwtAuthFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;

private final String[] whiteList;


/* 요청이 들어올 때마다 실행.
* 토큰 확인, 토큰 유효성 검사, 토큰에 포함된 정보를 기반으로 인증 수행 */
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// HTTP 요청에서 Authorization헤더를 찾아 토큰 반환
String accessToken = tokenProvider.resolveToken(request, "Access");


// 토큰이 있다면 진행
if(StringUtils.hasText(accessToken) && tokenProvider.validateToken(accessToken)) {

Authentication authentication = tokenProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보를 SecurityContext에 설정

}
else{
SecurityContextHolder.getContext().setAuthentication(null);
}
// 다음 단계 실행 -> 다른 필터 및 컨트롤러 실행
filterChain.doFilter(request,response);
}


private String getRefreshTokenFromRequest(HttpServletRequest request) {
String refreshToken = request.getHeader("Refresh-Token");
if (StringUtils.hasText(refreshToken)) {
return refreshToken;
}
return null;
}


// JWT 인증과 관련된 예외 처리
public void jwtExceptionHandler(HttpServletResponse response, GlobalErrorCode errorCode) {
response.setStatus(errorCode.getHttpStatus().value());
response.setContentType("application/json");

try {
// AuthErrorCode로부터 code와 message 추출
String code = errorCode.getCode();
String message = errorCode.getMessage();
String json = new ObjectMapper().writeValueAsString(CommonResponse.onFailure(code, message, null)); // ApiResponse 객체를 JSON으로 변환
response.getWriter().write(json);
} catch (Exception e) {
log.error(e.getMessage());
}
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String path = request.getRequestURI();
return Arrays.stream(whiteList).anyMatch(path::startsWith);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package treehouse.server.global.security.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import treehouse.server.global.common.CommonResponse;
import treehouse.server.global.exception.GlobalErrorCode;

import java.io.IOException;
import java.io.PrintWriter;

public class JwtAccessDeniedHandler implements AccessDeniedHandler {

private final Logger LOGGER = LoggerFactory.getLogger(JwtAccessDeniedHandler.class);

@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException {

response.setContentType("application/json; charset=UTF-8");
response.setStatus(403);
PrintWriter writer = response.getWriter();

// AuthErrorCode.AUTHENTICATION_DENIED enum에서 코드와 메시지를 얻음
String code = GlobalErrorCode.AUTHENTICATION_DENIED.getCode();
String message = GlobalErrorCode.AUTHENTICATION_DENIED.getMessage();
CommonResponse<String> apiErrorResult = CommonResponse.onFailure(code, message, null);
writer.write(apiErrorResult.toString());
writer.flush();
writer.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package treehouse.server.global.security.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import treehouse.server.global.common.CommonResponse;
import treehouse.server.global.exception.GlobalErrorCode;

import java.io.IOException;
import java.io.PrintWriter;

public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {


private final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
response.setContentType("application/json; charset=UTF-8");
response.setStatus(401);
PrintWriter writer = response.getWriter();

// AuthErrorCode.AUTHENTICATION_REQUIRED enum에서 코드와 메시지를 얻음
String code = GlobalErrorCode.AUTHENTICATION_REQUIRED.getCode();
String message = GlobalErrorCode.AUTHENTICATION_REQUIRED.getMessage();
CommonResponse<String> apiErrorResult = CommonResponse.onFailure(code, message, null);

writer.write(apiErrorResult.toString());
writer.flush();
writer.close();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package treehouse.server.global.security.handler;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
import treehouse.server.global.common.CommonResponse;
import treehouse.server.global.exception.GlobalErrorCode;
import treehouse.server.global.exception.JwtAuthenticationException;

import java.io.IOException;
import java.io.PrintWriter;

public class JwtAuthenticationExceptionHandler extends OncePerRequestFilter {

@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (JwtAuthenticationException authException) {
response.setContentType("application/json; charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());

PrintWriter writer = response.getWriter();
String errorCodeName = authException.getMessage();
GlobalErrorCode errorCode = GlobalErrorCode.valueOf(errorCodeName);
CommonResponse<String> apiErrorResult = CommonResponse.onFailure(errorCode.getCode(),errorCode.getMessage(), null);

writer.write(apiErrorResult.toString());
writer.flush();
writer.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package treehouse.server.global.security.handler.annotation;

public @interface AuthMember {
}
Loading

0 comments on commit a56f56d

Please sign in to comment.