|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- package com.ffii.fpsms.config.security;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Qualifier;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.Lazy;
- import org.springframework.core.annotation.Order;
- import org.springframework.http.HttpStatus;
- import org.springframework.ldap.core.support.BaseLdapPathContextSource;
- import org.springframework.security.authentication.AuthenticationManager;
- import org.springframework.security.config.Customizer;
- import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
- import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
- import org.springframework.security.config.annotation.web.builders.HttpSecurity;
- import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
- import org.springframework.security.config.http.SessionCreationPolicy;
- import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
- import org.springframework.security.web.SecurityFilterChain;
- import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
-
- import com.ffii.fpsms.config.security.jwt.JwtRequestFilter;
-
- import org.springframework.http.HttpMethod;
-
- import jakarta.servlet.http.HttpServletResponse;
- import java.io.IOException;
- import java.nio.charset.StandardCharsets;
-
- @Configuration
- @EnableWebSecurity
- @EnableMethodSecurity
- public class SecurityConfig {
-
- public static final String INDEX_URL = "/";
- public static final String LOGIN_URL = "/login";
- public static final String LDAP_LOGIN_URL = "/ldap-login";
-
- public static final String[] URL_WHITELIST = {
- INDEX_URL,
- LOGIN_URL,
- LDAP_LOGIN_URL,
- "/refresh-token",
- "/py/**"
- };
-
- public static final String[] CORS_ALLOWED_METHODS = {
- "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"
- };
-
- @Lazy
- @Autowired
- private JwtRequestFilter jwtRequestFilter;
-
- @Bean
- @Qualifier("AuthenticationManager")
- public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
- throws Exception {
- return authenticationConfiguration.getAuthenticationManager();
- }
-
- @Bean
- @Qualifier("LdapAuthenticationManager")
- public AuthenticationManager ldapAuthenticationManager(BaseLdapPathContextSource contextSource) {
- LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource);
- factory.setUserSearchFilter("cn={0}");
- return factory.createAuthenticationManager();
- }
-
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
-
- /**
- * FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13
- * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.1 | 2026-08-06
- * FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.7 | 2026-09-08
- * (stockAdjustment/submit and GET /latestRemarks → INVENTORY_ADJUST)
- */
- @Bean
- @Order(1)
- public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
- return http
- .cors(Customizer.withDefaults()).csrf(csrf -> csrf.disable())
- .requestCache(requestCache -> requestCache.disable())
- .authorizeHttpRequests(
- authRequest -> authRequest
- .requestMatchers(URL_WHITELIST).permitAll()
- .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
- /* PO stock-in nav alerts: TESTING / ADMIN / STOCK (no @PreAuthorize on Kotlin controllers). */
- .requestMatchers(HttpMethod.GET, "/stockInLine/alerts/purchase-incomplete-count")
- .hasAnyAuthority("TESTING", "ADMIN", "STOCK")
- .requestMatchers(HttpMethod.GET, "/stockInLine/alerts/purchase-incomplete")
- .hasAnyAuthority("TESTING", "ADMIN", "STOCK")
- .requestMatchers(HttpMethod.GET, "/product-process/Demo/Process/alerts/fg-qc-putaway")
- .hasAuthority("TESTING")
- .requestMatchers(HttpMethod.GET, "/device-presence/ping").authenticated()
- .requestMatchers(HttpMethod.POST, "/device-presence/heartbeat").authenticated()
- .requestMatchers(HttpMethod.GET, "/device-presence/active")
- .hasAnyAuthority("TESTING", "ADMIN")
- .requestMatchers(HttpMethod.GET, "/device-presence/history")
- .hasAnyAuthority("TESTING", "ADMIN")
- .requestMatchers(HttpMethod.GET, "/printer-monitor/status")
- .hasAnyAuthority("TESTING", "ADMIN")
- .requestMatchers(HttpMethod.GET, "/printer-monitor/history")
- .hasAnyAuthority("TESTING", "ADMIN")
- .requestMatchers(HttpMethod.POST, "/printer-monitor/check")
- .hasAnyAuthority("TESTING", "ADMIN")
- .requestMatchers(HttpMethod.GET, "/label-printer-monitor/status")
- .hasAnyAuthority("TESTING", "ADMIN")
- .requestMatchers(HttpMethod.POST, "/label-printer-monitor/check")
- .hasAnyAuthority("TESTING", "ADMIN")
- .requestMatchers(HttpMethod.GET, "/label-printer-monitor/label-stats")
- .hasAnyAuthority("TESTING", "ADMIN")
- .requestMatchers(HttpMethod.GET, "/label-printer-monitor/odometer-stats")
- .hasAnyAuthority("TESTING", "ADMIN")
- .requestMatchers(HttpMethod.POST, "/stockAdjustment/submit")
- .hasAuthority("INVENTORY_ADJUST")
- .requestMatchers(HttpMethod.GET, "/stockAdjustment/latestRemarks")
- .hasAuthority("INVENTORY_ADJUST")
- .requestMatchers(HttpMethod.GET, "/inventoryLotLine/trace")
- .hasAuthority("ITEM_TRACING")
- .requestMatchers(HttpMethod.GET, "/inventoryLotLine/trace/location/**")
- .hasAuthority("ITEM_TRACING")
- /* 工單 生產流程 完成工單:僅 ADMIN */
- .requestMatchers(HttpMethod.POST, "/product-process/Demo/ProcessLine/complete/**")
- .hasAuthority("ADMIN")
- /* 工序「已完成」(Just Pass):僅 ADMIN */
- .requestMatchers(HttpMethod.POST, "/product-process/Demo/ProcessLine/pass/**")
- .hasAuthority("ADMIN")
- /* M18 手動同步頁:ADMIN 或 M18_SYNC。po-by-code 另允許 PURCHASE(採購單搜尋自動同步)。 */
- .requestMatchers(HttpMethod.GET, "/m18/test/po-by-code")
- .hasAnyAuthority("ADMIN", "M18_SYNC", "PURCHASE")
- .requestMatchers(HttpMethod.GET, "/m18/test/do-by-code")
- .hasAnyAuthority("ADMIN", "M18_SYNC")
- .requestMatchers(HttpMethod.GET, "/m18/test/do-by-code-extra")
- .hasAnyAuthority("ADMIN", "M18_SYNC")
- .requestMatchers(HttpMethod.GET, "/m18/test/product-by-code")
- .hasAnyAuthority("ADMIN", "M18_SYNC")
- .anyRequest().authenticated())
- .httpBasic(httpBasic -> httpBasic.authenticationEntryPoint(
- (request, response, authException) -> sendUnauthorizedJson(response, "Unauthorized", "UNAUTHORIZED")))
- .sessionManagement(
- sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
- .build();
- }
-
- /** Send 401 with JSON body so frontend can consistently handle session timeout / missing token. */
- private static void sendUnauthorizedJson(HttpServletResponse response, String message, String code) throws IOException {
- response.setStatus(HttpStatus.UNAUTHORIZED.value());
- response.setContentType("application/json");
- response.setCharacterEncoding(StandardCharsets.UTF_8.name());
- String body = String.format("{\"message\":\"%s\",\"code\":\"%s\"}", message.replace("\"", "\\\""), code);
- response.getWriter().write(body);
- }
- }
|