Skip to content

Commit 0fc03ce

Browse files
authored
Merge pull request #27 from FitTheMan/feature/security-23
feat: 시큐리티 기본 설정 및 핸들러 구성
2 parents 16c3344 + 1b5f6e9 commit 0fc03ce

File tree

7 files changed

+206
-20
lines changed

7 files changed

+206
-20
lines changed

build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ dependencies {
4141
implementation 'org.springframework.boot:spring-boot-starter-validation'
4242

4343
// Security
44-
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
4544
implementation 'org.springframework.boot:spring-boot-starter-security'
4645
implementation 'org.springframework.session:spring-session-data-redis'
4746

src/main/java/com/ftm/server/infrastructure/security/.gitkeep

Whitespace-only changes.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.ftm.server.infrastructure.security;
2+
3+
import com.ftm.server.infrastructure.security.handler.PermissionDeniedHandler;
4+
import com.ftm.server.infrastructure.security.handler.UnauthenticatedAccessHandler;
5+
import java.util.List;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.http.HttpMethod;
10+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
11+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
12+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
13+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
14+
import org.springframework.security.crypto.password.PasswordEncoder;
15+
import org.springframework.security.web.SecurityFilterChain;
16+
import org.springframework.web.cors.CorsConfiguration;
17+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
18+
19+
@Configuration
20+
@EnableWebSecurity(debug = true)
21+
@RequiredArgsConstructor
22+
public class SecurityConfig {
23+
24+
private final UnauthenticatedAccessHandler unauthenticatedAccessHandler;
25+
private final PermissionDeniedHandler permissionDeniedHandler;
26+
27+
// CORS 에서 허용할 HTTP 메서드 목록
28+
public static final List<HttpMethod> CORS_ALLOWED_METHODS =
29+
List.of(
30+
HttpMethod.GET,
31+
HttpMethod.POST,
32+
HttpMethod.PUT,
33+
HttpMethod.PATCH,
34+
HttpMethod.DELETE,
35+
HttpMethod.HEAD);
36+
37+
// CORS 에서 허용할 도메인 목록
38+
public static final List<String> CORS_ALLOWED_ORIGINS =
39+
List.of(
40+
"http://localhost:8080", // 로컬 환경 서버 도메인
41+
"https://dev-api.fittheman.site", // 개발 환경 서버 도메인
42+
"https://fittheman.site"); // 개발 환경 클라이언트 도메인
43+
44+
@Bean
45+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
46+
http
47+
// csrf 비활성화
48+
.csrf(AbstractHttpConfigurer::disable)
49+
// http basic 비활성화
50+
.httpBasic(AbstractHttpConfigurer::disable)
51+
// 폼로그인 비활성화
52+
.formLogin(AbstractHttpConfigurer::disable)
53+
// 로그아웃 비활성화
54+
.logout(AbstractHttpConfigurer::disable)
55+
// 세션 관리
56+
.sessionManagement(
57+
session ->
58+
session.sessionFixation()
59+
.migrateSession() // 세션 고정 보호
60+
.maximumSessions(1) // 동시 로그인 1개 제한
61+
.maxSessionsPreventsLogin(false)) // 기존 세션 만료 후 새 로그인 허용
62+
// 예외 핸들링
63+
.exceptionHandling(
64+
exception ->
65+
exception
66+
// 인증되지 않은 요청 예외 처리
67+
.authenticationEntryPoint(unauthenticatedAccessHandler)
68+
// 접근 권한 부족 예외 처리
69+
.accessDeniedHandler(permissionDeniedHandler))
70+
// cors 설정
71+
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
72+
// 경로 인가 설정
73+
.authorizeHttpRequests(
74+
authorize -> {
75+
authorize
76+
// 정적 리소스 경로 허용
77+
.requestMatchers("/docs/**")
78+
.permitAll();
79+
80+
// TODO: 요청 허용 특정 API 추가 (회원가입, 로그인 등)
81+
82+
// 그 외 모든 요청은 인증 필요
83+
authorize.anyRequest().authenticated();
84+
});
85+
86+
return http.build();
87+
}
88+
89+
// Password 암호화 설정
90+
@Bean
91+
public PasswordEncoder passwordEncoder() {
92+
return new BCryptPasswordEncoder();
93+
}
94+
95+
// CORS 설정
96+
@Bean
97+
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
98+
CorsConfiguration config = new CorsConfiguration();
99+
config.setAllowCredentials(true);
100+
config.addAllowedHeader("*");
101+
CORS_ALLOWED_ORIGINS.forEach(config::addAllowedOriginPattern);
102+
CORS_ALLOWED_METHODS.forEach(config::addAllowedMethod);
103+
104+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
105+
source.registerCorsConfiguration("/**", config);
106+
107+
return source;
108+
}
109+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.ftm.server.infrastructure.security.handler;
2+
3+
import com.ftm.server.common.response.ApiResponse;
4+
import com.ftm.server.common.response.enums.ErrorResponseCode;
5+
import jakarta.servlet.ServletException;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import java.io.IOException;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.security.access.AccessDeniedException;
11+
import org.springframework.security.web.access.AccessDeniedHandler;
12+
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
13+
import org.springframework.stereotype.Component;
14+
15+
/** 인증은 되었지만 접근 권한이 없는 경우 예외처리하는 핸들러 */
16+
@Component
17+
@RequiredArgsConstructor
18+
public class PermissionDeniedHandler implements AccessDeniedHandler {
19+
20+
private final SecurityResponseHandler securityResponseHandler;
21+
22+
@Override
23+
public void handle(
24+
HttpServletRequest request,
25+
HttpServletResponse response,
26+
AccessDeniedException accessDeniedException)
27+
throws IOException, ServletException {
28+
securityResponseHandler.sendResponse(
29+
response, ApiResponse.fail(ErrorResponseCode.NOT_AUTHORIZATION));
30+
31+
SecurityContextPersistenceFilter filter = new SecurityContextPersistenceFilter();
32+
}
33+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.ftm.server.infrastructure.security.handler;
2+
3+
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
4+
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.ftm.server.common.response.ApiResponse;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import java.io.IOException;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.stereotype.Component;
11+
12+
/** 시큐리티 필터단에서 응답을 처리하는 핸들러 */
13+
@Component
14+
@RequiredArgsConstructor
15+
public class SecurityResponseHandler {
16+
17+
private final ObjectMapper objectMapper;
18+
19+
public <T> void sendResponse(HttpServletResponse response, ApiResponse<T> apiResponse)
20+
throws IOException {
21+
String jsonResponse = objectMapper.writeValueAsString(apiResponse);
22+
23+
response.setStatus(apiResponse.getStatus());
24+
response.setContentType(APPLICATION_JSON_VALUE);
25+
response.setCharacterEncoding("UTF-8");
26+
response.getWriter().write(jsonResponse);
27+
}
28+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.ftm.server.infrastructure.security.handler;
2+
3+
import com.ftm.server.common.response.ApiResponse;
4+
import com.ftm.server.common.response.enums.ErrorResponseCode;
5+
import jakarta.servlet.ServletException;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import java.io.IOException;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.security.core.AuthenticationException;
11+
import org.springframework.security.web.AuthenticationEntryPoint;
12+
import org.springframework.stereotype.Component;
13+
14+
/** 인증이 필요한 요청에서 인증되지 않은 유저가 요청할 경우 예외처리하는 핸들러 */
15+
@Component
16+
@RequiredArgsConstructor
17+
public class UnauthenticatedAccessHandler implements AuthenticationEntryPoint {
18+
19+
private final SecurityResponseHandler securityResponseHandler;
20+
21+
@Override
22+
public void commence(
23+
HttpServletRequest request,
24+
HttpServletResponse response,
25+
AuthenticationException authException)
26+
throws IOException, ServletException {
27+
securityResponseHandler.sendResponse(
28+
response, ApiResponse.fail(ErrorResponseCode.NOT_AUTHENTICATED));
29+
}
30+
}

src/main/resources/application-security.yml

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,9 @@ spring:
33
activate:
44
on-profile: "security"
55

6-
security:
7-
oauth2:
8-
client:
9-
registration:
10-
kakao:
11-
client-id: ${KAKAO_CLIENT_ID}
12-
client-secret: ${KAKAO_CLIENT_SECRET}
13-
client-authentication-method: client_secret_post
14-
authorization-grant-type: authorization_code
15-
redirect-uri: "{baseUrl}/login/oauth2/code/kakao"
16-
client-name: Kakao
17-
provider: kakao
18-
provider:
19-
kakao:
20-
authorization-uri: https://kauth.kakao.com/oauth/authorize
21-
token-uri: https://kauth.kakao.com/oauth/token
22-
user-info-uri: https://kapi.kakao.com/v2/user/me
23-
user-info-authentication-method: header
24-
user-name-attribute: id
6+
kakao:
7+
clinet-id: ${KAKAO_CLIENT_ID}
8+
client-secret: ${KAKAO_CLIENT_SECRET}
9+
redirect-uri: "${BASE_URL}/api/auth/kakao/callback"
10+
token-uri: "https://kauth.kakao.com/oauth/token"
11+
user-info-uri: "https://kapi.kakao.com/v2/user/me"

0 commit comments

Comments
 (0)