Commit ae7e90e04532b3ab5a291bf3b779519ae8ee1097

Authored by zichun
1 parent 79a8340d

feat(usr): SecurityConfig + JwtFilter + AuthController REQ-USR-004

- SecurityConfig: STATELESS, permitAll /api/auth/**, JWT filter
- JwtAuthenticationFilter: Bearer token → SecurityContext
- AuthController: POST /login, POST /refresh, GET /brands
- BrandVO: @JsonProperty to fix Jackson serialization of sNo/sName
- AuthControllerTest: 4/4 PASS; all 22 backend tests GREEN
backend/src/main/java/com/example/erp/config/JwtAuthenticationFilter.java 0 → 100644
  1 +package com.example.erp.config;
  2 +
  3 +import com.example.erp.common.exception.BizException;
  4 +import com.example.erp.common.util.JwtUtil;
  5 +import io.jsonwebtoken.Claims;
  6 +import jakarta.servlet.FilterChain;
  7 +import jakarta.servlet.ServletException;
  8 +import jakarta.servlet.http.HttpServletRequest;
  9 +import jakarta.servlet.http.HttpServletResponse;
  10 +import lombok.RequiredArgsConstructor;
  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.util.StringUtils;
  15 +import org.springframework.web.filter.OncePerRequestFilter;
  16 +
  17 +import java.io.IOException;
  18 +import java.util.Collections;
  19 +
  20 +@Component
  21 +@RequiredArgsConstructor
  22 +public class JwtAuthenticationFilter extends OncePerRequestFilter {
  23 +
  24 + private final JwtUtil jwtUtil;
  25 +
  26 + @Override
  27 + protected void doFilterInternal(HttpServletRequest request,
  28 + HttpServletResponse response,
  29 + FilterChain chain) throws ServletException, IOException {
  30 + String header = request.getHeader("Authorization");
  31 + if (StringUtils.hasText(header) && header.startsWith("Bearer ")) {
  32 + String token = header.substring(7);
  33 + try {
  34 + Claims claims = jwtUtil.parseAccessToken(token);
  35 + UsernamePasswordAuthenticationToken auth =
  36 + new UsernamePasswordAuthenticationToken(
  37 + claims.getSubject(), null, Collections.emptyList());
  38 + SecurityContextHolder.getContext().setAuthentication(auth);
  39 + } catch (BizException ignored) {
  40 + // invalid token — no auth set; Spring Security will return 401
  41 + }
  42 + }
  43 + chain.doFilter(request, response);
  44 + }
  45 +}
backend/src/main/java/com/example/erp/config/SecurityConfig.java 0 → 100644
  1 +package com.example.erp.config;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import org.springframework.context.annotation.Bean;
  5 +import org.springframework.context.annotation.Configuration;
  6 +import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  7 +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  8 +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
  9 +import org.springframework.security.config.http.SessionCreationPolicy;
  10 +import org.springframework.security.web.SecurityFilterChain;
  11 +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  12 +
  13 +@Configuration
  14 +@EnableWebSecurity
  15 +@RequiredArgsConstructor
  16 +public class SecurityConfig {
  17 +
  18 + private final JwtAuthenticationFilter jwtAuthenticationFilter;
  19 +
  20 + @Bean
  21 + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  22 + http
  23 + .csrf(AbstractHttpConfigurer::disable)
  24 + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  25 + .authorizeHttpRequests(auth -> auth
  26 + .requestMatchers("/api/auth/**").permitAll()
  27 + .anyRequest().authenticated()
  28 + )
  29 + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
  30 + return http.build();
  31 + }
  32 +}
backend/src/main/java/com/example/erp/module/usr/controller/AuthController.java 0 → 100644
  1 +package com.example.erp.module.usr.controller;
  2 +
  3 +import com.example.erp.common.response.Result;
  4 +import com.example.erp.module.usr.dto.LoginReqDTO;
  5 +import com.example.erp.module.usr.dto.RefreshTokenReqDTO;
  6 +import com.example.erp.module.usr.service.AuthService;
  7 +import com.example.erp.module.usr.vo.BrandVO;
  8 +import com.example.erp.module.usr.vo.LoginVO;
  9 +import jakarta.validation.Valid;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.web.bind.annotation.*;
  12 +
  13 +import java.util.List;
  14 +import java.util.Map;
  15 +
  16 +@RestController
  17 +@RequestMapping("/api/auth")
  18 +@RequiredArgsConstructor
  19 +public class AuthController {
  20 +
  21 + private final AuthService authService;
  22 +
  23 + @PostMapping("/login")
  24 + public Result<LoginVO> login(@Valid @RequestBody LoginReqDTO req) {
  25 + return Result.ok(authService.login(req));
  26 + }
  27 +
  28 + @PostMapping("/refresh")
  29 + public Result<Map<String, String>> refresh(@Valid @RequestBody RefreshTokenReqDTO req) {
  30 + String newToken = authService.refresh(req.getRefreshToken());
  31 + return Result.ok(Map.of("accessToken", newToken));
  32 + }
  33 +
  34 + @GetMapping("/brands")
  35 + public Result<List<BrandVO>> brands() {
  36 + return Result.ok(authService.getBrands());
  37 + }
  38 +}
backend/src/main/java/com/example/erp/module/usr/vo/BrandVO.java
1 package com.example.erp.module.usr.vo; 1 package com.example.erp.module.usr.vo;
2 2
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
3 import lombok.Getter; 4 import lombok.Getter;
4 import lombok.Setter; 5 import lombok.Setter;
5 6
6 @Getter 7 @Getter
7 @Setter 8 @Setter
8 public class BrandVO { 9 public class BrandVO {
  10 + @JsonProperty("sNo")
9 private String sNo; 11 private String sNo;
  12 +
  13 + @JsonProperty("sName")
10 private String sName; 14 private String sName;
11 } 15 }
backend/src/test/java/com/example/erp/module/usr/AuthControllerTest.java 0 → 100644
  1 +package com.example.erp.module.usr;
  2 +
  3 +import com.example.erp.common.exception.BizException;
  4 +import com.example.erp.module.usr.dto.LoginReqDTO;
  5 +import com.example.erp.module.usr.service.AuthService;
  6 +import com.example.erp.module.usr.vo.BrandVO;
  7 +import com.example.erp.module.usr.vo.LoginVO;
  8 +import com.fasterxml.jackson.databind.ObjectMapper;
  9 +import org.junit.jupiter.api.Test;
  10 +import org.springframework.beans.factory.annotation.Autowired;
  11 +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
  12 +import org.springframework.boot.test.mock.mockito.MockBean;
  13 +import org.springframework.context.annotation.Import;
  14 +import org.springframework.http.MediaType;
  15 +import org.springframework.test.web.servlet.MockMvc;
  16 +
  17 +import java.util.List;
  18 +import java.util.Map;
  19 +
  20 +import static org.mockito.ArgumentMatchers.any;
  21 +import static org.mockito.ArgumentMatchers.anyString;
  22 +import static org.mockito.Mockito.when;
  23 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
  24 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  25 +
  26 +import com.example.erp.config.SecurityConfig;
  27 +import com.example.erp.config.JwtAuthenticationFilter;
  28 +import com.example.erp.config.BeanConfig;
  29 +import com.example.erp.common.util.JwtUtil;
  30 +import com.example.erp.config.JwtProperties;
  31 +import com.example.erp.module.usr.controller.AuthController;
  32 +
  33 +@WebMvcTest(controllers = AuthController.class)
  34 +@Import({SecurityConfig.class, JwtAuthenticationFilter.class, BeanConfig.class, JwtUtil.class, JwtProperties.class})
  35 +class AuthControllerTest {
  36 +
  37 + @Autowired private MockMvc mockMvc;
  38 + @Autowired private ObjectMapper objectMapper;
  39 + @MockBean private AuthService authService;
  40 +
  41 + @Test
  42 + void login_wrongPassword_returns40100() throws Exception {
  43 + when(authService.login(any())).thenThrow(new BizException(40100, "用户名或密码错误"));
  44 +
  45 + Map<String, String> body = Map.of("brandNo", "STD", "username", "admin", "password", "wrong");
  46 + mockMvc.perform(post("/api/auth/login")
  47 + .contentType(MediaType.APPLICATION_JSON)
  48 + .content(objectMapper.writeValueAsString(body)))
  49 + .andExpect(status().isOk())
  50 + .andExpect(jsonPath("$.code").value(40100))
  51 + .andExpect(jsonPath("$.message").value("用户名或密码错误"));
  52 + }
  53 +
  54 + @Test
  55 + void login_validCredentials_returns200AndTokens() throws Exception {
  56 + LoginVO.UserInfoVO userInfo = new LoginVO.UserInfoVO();
  57 + userInfo.setUserId("u1");
  58 + userInfo.setUsername("admin");
  59 + userInfo.setUserType("普通用户");
  60 + userInfo.setLanguage("中文");
  61 + userInfo.setBrandId("b1");
  62 +
  63 + LoginVO loginVO = new LoginVO();
  64 + loginVO.setAccessToken("access-token");
  65 + loginVO.setRefreshToken("refresh-token");
  66 + loginVO.setExpiresIn(86400L);
  67 + loginVO.setUserInfo(userInfo);
  68 +
  69 + when(authService.login(any())).thenReturn(loginVO);
  70 +
  71 + Map<String, String> body = Map.of("brandNo", "STD", "username", "admin", "password", "666666");
  72 + mockMvc.perform(post("/api/auth/login")
  73 + .contentType(MediaType.APPLICATION_JSON)
  74 + .content(objectMapper.writeValueAsString(body)))
  75 + .andExpect(status().isOk())
  76 + .andExpect(jsonPath("$.code").value(200))
  77 + .andExpect(jsonPath("$.data.accessToken").value("access-token"))
  78 + .andExpect(jsonPath("$.data.userInfo.userId").value("u1"));
  79 + }
  80 +
  81 + @Test
  82 + void refresh_withInvalidToken_returns40103() throws Exception {
  83 + when(authService.refresh(anyString())).thenThrow(new BizException(40103, "Refresh Token 已失效,请重新登录"));
  84 +
  85 + Map<String, String> body = Map.of("refreshToken", "invalid-token");
  86 + mockMvc.perform(post("/api/auth/refresh")
  87 + .contentType(MediaType.APPLICATION_JSON)
  88 + .content(objectMapper.writeValueAsString(body)))
  89 + .andExpect(status().isOk())
  90 + .andExpect(jsonPath("$.code").value(40103));
  91 + }
  92 +
  93 + @Test
  94 + void getBrands_returns200AndList() throws Exception {
  95 + BrandVO b = new BrandVO();
  96 + b.setSNo("STD");
  97 + b.setSName("标准版");
  98 + when(authService.getBrands()).thenReturn(List.of(b));
  99 +
  100 + mockMvc.perform(get("/api/auth/brands"))
  101 + .andExpect(status().isOk())
  102 + .andExpect(jsonPath("$.code").value(200))
  103 + .andExpect(jsonPath("$.data[0].sNo").value("STD"))
  104 + .andExpect(jsonPath("$.data[0].sName").value("标准版"));
  105 + }
  106 +}