Commit 2bd4442cd49e76f92e3868842160eadba65feaa5

Authored by zichun
1 parent a6ef4aa1

feat(mod): jwt filter + security config (role stub) REQ-MOD-001

backend/src/main/java/com/xly/erp/common/security/JwtAuthenticationFilter.java 0 → 100644
  1 +package com.xly.erp.common.security;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import com.xly.erp.common.exception.BizException;
  5 +import com.xly.erp.common.response.Result;
  6 +import jakarta.servlet.FilterChain;
  7 +import jakarta.servlet.ServletException;
  8 +import jakarta.servlet.http.HttpServletRequest;
  9 +import jakarta.servlet.http.HttpServletResponse;
  10 +import org.springframework.beans.factory.annotation.Autowired;
  11 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  12 +import org.springframework.security.core.context.SecurityContextHolder;
  13 +import org.springframework.stereotype.Component;
  14 +import org.springframework.web.filter.OncePerRequestFilter;
  15 +
  16 +import java.io.IOException;
  17 +import java.nio.charset.StandardCharsets;
  18 +import java.util.Collections;
  19 +
  20 +@Component
  21 +public class JwtAuthenticationFilter extends OncePerRequestFilter {
  22 +
  23 + private static final String HEADER = "Authorization";
  24 + private static final String PREFIX = "Bearer ";
  25 +
  26 + private final JwtUtil jwtUtil;
  27 + private final ObjectMapper objectMapper;
  28 +
  29 + @Autowired
  30 + public JwtAuthenticationFilter(JwtUtil jwtUtil, ObjectMapper objectMapper) {
  31 + this.jwtUtil = jwtUtil;
  32 + this.objectMapper = objectMapper;
  33 + }
  34 +
  35 + @Override
  36 + protected void doFilterInternal(HttpServletRequest request,
  37 + HttpServletResponse response,
  38 + FilterChain chain) throws ServletException, IOException {
  39 + String header = request.getHeader(HEADER);
  40 + if (header == null || !header.startsWith(PREFIX)) {
  41 + chain.doFilter(request, response);
  42 + return;
  43 + }
  44 +
  45 + String token = header.substring(PREFIX.length());
  46 + String userNo;
  47 + try {
  48 + userNo = jwtUtil.parse(token);
  49 + } catch (BizException e) {
  50 + writeJsonResult(response, e.getCode(), e.getMessage());
  51 + return;
  52 + }
  53 +
  54 + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
  55 + userNo, null, Collections.emptyList());
  56 + SecurityContextHolder.getContext().setAuthentication(auth);
  57 + chain.doFilter(request, response);
  58 + }
  59 +
  60 + private void writeJsonResult(HttpServletResponse response, int code, String msg) throws IOException {
  61 + response.setStatus(HttpServletResponse.SC_OK);
  62 + response.setContentType("application/json;charset=UTF-8");
  63 + response.setCharacterEncoding(StandardCharsets.UTF_8.name());
  64 + response.getWriter().write(objectMapper.writeValueAsString(Result.fail(code, msg)));
  65 + }
  66 +}
... ...
backend/src/main/java/com/xly/erp/common/security/SecurityConfig.java 0 → 100644
  1 +package com.xly.erp.common.security;
  2 +
  3 +import org.springframework.context.annotation.Bean;
  4 +import org.springframework.context.annotation.Configuration;
  5 +import org.springframework.http.HttpMethod;
  6 +import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  7 +import org.springframework.security.config.http.SessionCreationPolicy;
  8 +import org.springframework.security.web.SecurityFilterChain;
  9 +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  10 +
  11 +@Configuration
  12 +public class SecurityConfig {
  13 +
  14 + private final JwtAuthenticationFilter jwtFilter;
  15 +
  16 + public SecurityConfig(JwtAuthenticationFilter jwtFilter) {
  17 + this.jwtFilter = jwtFilter;
  18 + }
  19 +
  20 + @Bean
  21 + SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  22 + http.csrf(csrf -> csrf.disable())
  23 + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  24 + .authorizeHttpRequests(auth -> auth
  25 + // REQ-MOD-001 stub: see USR-004 follow-up — 角色硬校验在 USR-004 完成后回填为 hasAuthority('SUPER_ADMIN')
  26 + .requestMatchers(HttpMethod.POST, "/api/mod/modules").permitAll()
  27 + .anyRequest().authenticated()
  28 + )
  29 + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
  30 + return http.build();
  31 + }
  32 +}
... ...
backend/src/main/java/com/xly/erp/common/security/SecurityContextHelper.java 0 → 100644
  1 +package com.xly.erp.common.security;
  2 +
  3 +import org.springframework.security.core.Authentication;
  4 +import org.springframework.security.core.context.SecurityContextHolder;
  5 +
  6 +public final class SecurityContextHelper {
  7 +
  8 + private SecurityContextHelper() {
  9 + }
  10 +
  11 + public static String currentUserNo() {
  12 + Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  13 + if (auth == null || auth.getPrincipal() == null) {
  14 + return null;
  15 + }
  16 + Object p = auth.getPrincipal();
  17 + return p instanceof String s ? s : null;
  18 + }
  19 +}
... ...
backend/src/test/java/com/xly/erp/common/security/JwtAuthenticationFilterTest.java 0 → 100644
  1 +package com.xly.erp.common.security;
  2 +
  3 +import com.fasterxml.jackson.databind.JsonNode;
  4 +import com.fasterxml.jackson.databind.ObjectMapper;
  5 +import jakarta.servlet.FilterChain;
  6 +import org.junit.jupiter.api.AfterEach;
  7 +import org.junit.jupiter.api.BeforeEach;
  8 +import org.junit.jupiter.api.Test;
  9 +import org.springframework.mock.web.MockHttpServletRequest;
  10 +import org.springframework.mock.web.MockHttpServletResponse;
  11 +import org.springframework.security.core.Authentication;
  12 +import org.springframework.security.core.context.SecurityContextHolder;
  13 +
  14 +import static org.assertj.core.api.Assertions.assertThat;
  15 +import static org.mockito.Mockito.mock;
  16 +import static org.mockito.Mockito.never;
  17 +import static org.mockito.Mockito.times;
  18 +import static org.mockito.Mockito.verify;
  19 +
  20 +class JwtAuthenticationFilterTest {
  21 +
  22 + private static final String SECRET = "f8d4be76bff13bf32fa33ca0b14a4b152ad01ca5719f57df18ec4ecf2370b235";
  23 +
  24 + private JwtUtil jwtUtil;
  25 + private JwtAuthenticationFilter filter;
  26 +
  27 + @BeforeEach
  28 + void setUp() {
  29 + jwtUtil = new JwtUtil(SECRET);
  30 + filter = new JwtAuthenticationFilter(jwtUtil, new ObjectMapper());
  31 + }
  32 +
  33 + @AfterEach
  34 + void clearContext() {
  35 + SecurityContextHolder.clearContext();
  36 + }
  37 +
  38 + @Test
  39 + void validJwt_setsPrincipalInSecurityContext_andCallsChain() throws Exception {
  40 + String token = jwtUtil.sign("USER001");
  41 + MockHttpServletRequest req = new MockHttpServletRequest("POST", "/api/anything");
  42 + req.addHeader("Authorization", "Bearer " + token);
  43 + MockHttpServletResponse resp = new MockHttpServletResponse();
  44 + FilterChain chain = mock(FilterChain.class);
  45 +
  46 + filter.doFilter(req, resp, chain);
  47 +
  48 + Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  49 + assertThat(auth).isNotNull();
  50 + assertThat(auth.getPrincipal()).isEqualTo("USER001");
  51 + verify(chain, times(1)).doFilter(req, resp);
  52 + }
  53 +
  54 + @Test
  55 + void tamperedJwt_writesJsonResultWith20001_andDoesNotCallChain() throws Exception {
  56 + MockHttpServletRequest req = new MockHttpServletRequest("POST", "/api/anything");
  57 + req.addHeader("Authorization", "Bearer not.a.real.jwt");
  58 + MockHttpServletResponse resp = new MockHttpServletResponse();
  59 + FilterChain chain = mock(FilterChain.class);
  60 +
  61 + filter.doFilter(req, resp, chain);
  62 +
  63 + verify(chain, never()).doFilter(req, resp);
  64 + assertThat(resp.getContentType()).contains("application/json");
  65 + JsonNode body = new ObjectMapper().readTree(resp.getContentAsString());
  66 + assertThat(body.get("code").asInt()).isEqualTo(20001);
  67 + }
  68 +
  69 + @Test
  70 + void noJwtHeader_passesChainThrough_withoutSettingContext() throws Exception {
  71 + MockHttpServletRequest req = new MockHttpServletRequest("POST", "/api/anything");
  72 + MockHttpServletResponse resp = new MockHttpServletResponse();
  73 + FilterChain chain = mock(FilterChain.class);
  74 +
  75 + filter.doFilter(req, resp, chain);
  76 +
  77 + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
  78 + verify(chain, times(1)).doFilter(req, resp);
  79 + }
  80 +}
... ...