diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d9d0e3d4..a3ba271d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -153,6 +153,8 @@ jobs: echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env echo "MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}" >> .env echo "MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}" >> .env + echo "SBA_ADMIN_NAME=${{ secrets.SBA_ADMIN_NAME }}" >> .env + echo "SBA_ADMIN_PASSWORD=${{ secrets.SBA_ADMIN_PASSWORD }}" >> .env # 필요하다면 풀 사이즈 변수도 추가 # echo "DB_POOL_MAX=30" >> .env diff --git a/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java index a9c02b56..539412ac 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/org/sejongisc/backend/common/auth/filter/JwtAuthenticationFilter.java @@ -74,6 +74,14 @@ protected void doFilterInternal(@NotNull HttpServletRequest request, @Override protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); + + boolean isAdminZone = Arrays.stream(SecurityConstants.ADMIN_URLS) + .anyMatch(pattern -> pathMatcher.match(pattern, path)); + + if (isAdminZone) { + return true; + } + boolean excluded = Arrays.stream(SecurityConstants.WHITELIST_URLS) .anyMatch(pattern -> pathMatcher.match(pattern, path)); diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/security/AdminSecurityConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/security/AdminSecurityConfig.java new file mode 100644 index 00000000..120e4e94 --- /dev/null +++ b/backend/src/main/java/org/sejongisc/backend/common/config/security/AdminSecurityConfig.java @@ -0,0 +1,78 @@ +package org.sejongisc.backend.common.config.security; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class AdminSecurityConfig { + + private final AdminServerProperties adminServerProperties; + private final PasswordEncoder passwordEncoder; + + @Value("${spring.security.user.name}") + private String adminUsername; + + @Value("${spring.security.user.password}") + private String adminPassword; + + public AdminSecurityConfig(AdminServerProperties adminServerProperties, PasswordEncoder passwordEncoder) { + this.adminServerProperties = adminServerProperties; + this.passwordEncoder = passwordEncoder; + } + + @Bean + @Order(1) // 1순위로 체크: /admin 및 /actuator 경로는 이 설정이 우선 적용됨 + public SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception { + String adminContextPath = adminServerProperties.getContextPath(); + + // 관리자 계정 설정 + UserDetails adminUser = User.withUsername(adminUsername) + .password(passwordEncoder.encode(adminPassword)) + .roles("ADMIN") + .build(); + + InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager(adminUser); + + // 관리자 전용 인증 프로바이더 설정 + DaoAuthenticationProvider adminAuthenticationProvider = new DaoAuthenticationProvider(); + adminAuthenticationProvider.setUserDetailsService(userDetailsService); + adminAuthenticationProvider.setPasswordEncoder(passwordEncoder); + + http + .securityMatcher(SecurityConstants.ADMIN_URLS) + .authenticationProvider(adminAuthenticationProvider) + .csrf(csrf -> csrf.ignoringRequestMatchers( + adminContextPath + "/instances", + adminContextPath + "/instances/**", + "/actuator/**" + )) + .authorizeHttpRequests(auth -> auth + // 무인증 허용 리스트 + .requestMatchers(SecurityConstants.ADMIN_PUBLIC_URLS).permitAll() + + // SBA 클라이언트 등록 엔드포인트 보호 + .requestMatchers(adminContextPath + "/instances", adminContextPath + "/instances/**").authenticated() + + // 나머지는 관리자 인증 필수 + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginPage(adminContextPath + "/login") + .defaultSuccessUrl(adminContextPath + "/", true) + ) + .httpBasic(Customizer.withDefaults()); + + return http.build(); + } +} diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java index 05056b04..f4399f7c 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java @@ -64,6 +64,12 @@ public RoleHierarchy roleHierarchy() { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + .securityMatcher(request -> { + String requestUri = request.getRequestURI(); + boolean isAdminRequest = requestUri.startsWith("/admin"); + boolean isActuatorRequest = requestUri.startsWith("/actuator"); + return !isAdminRequest && !isActuatorRequest; + }) .cors(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) diff --git a/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java index 78db3965..866f299a 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java +++ b/backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConstants.java @@ -9,7 +9,6 @@ public class SecurityConstants { "/api/auth/reissue", "/api/user/password/reset/**", "/api/email/**", - "/actuator/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", @@ -37,4 +36,17 @@ public class SecurityConstants { //"/api/attendance/**" }; + + public static final String[] ADMIN_URLS = { + "/admin/**", "/actuator/**" + }; + + public static final String[] ADMIN_PUBLIC_URLS = { + "/admin/assets/**", + "/admin/login", + "/favicon.ico", + "/admin/favicon.ico", + "/actuator/health", + "/actuator/info" + }; } \ No newline at end of file diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 7cdbc2e9..cdcf2575 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -54,4 +54,26 @@ spring: auth: true starttls: enable: true - required: true \ No newline at end of file + required: true + + security: + user: + name: ${SBA_ADMIN_NAME} + password: ${SBA_ADMIN_PASSWORD} + + boot: + admin: + context-path: /admin + client: + url: "http://localhost:8080/admin" + username: ${SBA_ADMIN_NAME} + password: ${SBA_ADMIN_PASSWORD} + + instance: + name: sisc-backend + service-base-url: "http://localhost:8080" + management-base-url: "http://localhost:8080" + metadata: + user: + name: ${SBA_ADMIN_NAME} + password: ${SBA_ADMIN_PASSWORD} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 036eae7d..360dac91 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -11,4 +11,28 @@ spring: dialect: org.hibernate.dialect.PostgreSQLDialect show_sql: false format_sql: false # 가로로 너무 길어지는 게 싫다면 이것도 false - use_sql_comments: false # 부가 설명 제거 \ No newline at end of file + use_sql_comments: false # 부가 설명 제거 + + boot: + admin: + context-path: /admin + notify: + mail: + enabled: false + +management: + endpoints: + web: + base-path: /actuator + exposure: + include: "health,info,metrics,logfile,httpexchanges,threaddump" + endpoint: + health: + show-details: when_authorized + health: + mail: + enabled: false + +logging: + file: + name: ./logs/application.log \ No newline at end of file