传统Spring MVC + RESTful 与 Vue3 结合 JWT Token 验证的示例

以下是针对非Spring Boot项目(传统Spring MVC)的示例


一、项目结构

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           ├── config/          # 配置类目录
│   │           │   ├── SecurityConfig.java
│   │           │   ├── WebMvcConfig.java
│   │           │   └── JwtFilter.java
│   │           ├── controller/      # 控制器
│   │           ├── service/         # 服务层
│   │           ├── util/            # 工具类
│   │           │   └── JwtUtil.java
│   │           └── model/           # 数据模型
│   └── resources/
│       ├── applicationContext.xml   # XML配置(可选)
│       └── web.xml                # Servlet配置

二、核心配置

1.引入相关依赖
 <!-- Spring Security -->
  		<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.30</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>5.7.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>5.7.1</version>
        </dependency>
        <!-- JWT Token -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
2. web.xml 配置(传统部署方式)
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <!--配置DispatcherServlet-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>   
    <!--Spring Security 过滤器-->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>
3. Spring Security Java 配置(替代XML)
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .cors().and() // 显式启用CORS配置
                .csrf().disable()
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(new AntPathRequestMatcher(HttpMethod.OPTIONS.name())).permitAll()// 允许所有OPTIONS请求
                        .requestMatchers(new AntPathRequestMatcher("/login")).permitAll() // 允许登录接口公开
                        .requestMatchers(new AntPathRequestMatcher("/register")).permitAll() // 允许注册接口公开
                        .requestMatchers(new AntPathRequestMatcher("/captcha")).permitAll() // 允许验证码接口公开
                        .anyRequest().authenticated() // 其他接口需要认证
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
4. JWT 过滤器(适配传统项目)
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        String authHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;

        // 直接放行 OPTIONS(预检) 请求(restful风格接口必须放行此处)
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            chain.doFilter(request, response);
            return;
        }

        // 从请求头提取 Token(格式:Bearer <token>)
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
            try {
                username = jwtUtil.extractUsername(token);
            } catch (Exception e) {
                // Token 解析失败直接拦截
                sendUnauthorized(response);
                return;
            }
        }

        // 如果 Token 有效且用户未认证,则设置认证信息
        // 验证 Token 有效性
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            if (!jwtUtil.validateToken(token)) {
                sendUnauthorized(response);
                return;
            }
            // 创建认证信息
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        // 关键拦截点:已通过过滤器但仍未认证的请求
        if (isProtectedPath(request.getRequestURI()) &&
                SecurityContextHolder.getContext().getAuthentication() == null) {
            sendUnauthorized(response);
            return;
        }

        chain.doFilter(request, response);
    }
    private boolean isProtectedPath(String path) {
        return !path.startsWith("/login")
                && !path.startsWith("/register")
                && !path.startsWith("/captcha");
    }

    private void sendUnauthorized(HttpServletResponse response) throws IOException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().write("Unauthorized - Missing or invalid token");
    }
}

三、后端对接要点

1. 跨域配置(CORS)
(1)、方式1:WebMvcConfig
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}
(2)、方式2:applicationConfig.xml配置方式
 <!-- 配置全局跨域 :注意该标签需放在mvc:annotation-driven标签前面-->
    <mvc:cors>
        <mvc:mapping path="/**"
                     allowed-origins="http://localhost:5173, https://example.com"
                     allowed-methods="GET, POST, PUT, DELETE, OPTIONS"
                     allowed-headers="Content-Type, Authorization"
                     allow-credentials="true"
                     max-age="3600"/>
    </mvc:cors>

2. 登录接口示例
@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private JwtUserDetailsService userDetailsService;

    @PostMapping("/login")
    public ResponseEntity<?> createAuthenticationToken(
            @RequestBody JwtRequest authenticationRequest) throws Exception {
        
        authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
        final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails);

        return ResponseEntity.ok(new JwtResponse(jwt));
    }

    private void authenticate(String username, String password) throws Exception {
        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(username, password)
            );
        } catch (BadCredentialsException e) {
            throw new Exception("INVALID_CREDENTIALS", e);
        }
    }
}

四、前端代码示例

1、封装axios

在请求拦截器中获取保存在localStorage或store中的Token并添加到请求头中

// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
import useUserStore from '@/store'
import config from '@/config'

const userStore = useUserStore()
axios.defaults.withCredentials = true; // 全局配置携带凭证(必须配置,否则因为跨域请求默认不携带 Cookie,会导致每次请求生成新 Session)
const service = axios.create({
baseURL:config.baseURL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

// 请求拦截器
service.interceptors.request.use(
  config => {
   const token = userStore.getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 200) {
      if (res.code === 401) {
        console.log("401")
        handleLogout()
      }
      showErrorToast(res.message || '请求失败')
      return Promise.reject(new Error(res.message || 'Error'))
    }
    return res.data
  },
  error => {
    const status = error.response?.status
    const messageMap = {
      400: '请求错误',
      401: '未授权,请重新登录',
      403: '拒绝访问',
      404: '资源不存在',
      500: '服务器错误',
      502: '网关错误',
      503: '服务不可用',
      504: '网关超时'
    }
    const errorMessage = messageMap[status] || error.message
    showErrorToast(errorMessage)
    if (status === 401) {
      handleLogout()
    }
    return Promise.reject(error)
  }
)

// 封装GET请求
export function get(url, params = {}, config = {}) {
  return service.get(url, { params, ...config })
}

// 封装POST请求
export function post(url, data = {}, config = {}) {
  return service.post(url, data, config)
}

// 封装带文件上传的POST请求
export function postFile(url, data = {}, config = {}) {
  return service.post(url, data, {
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    ...config
  })
}

// 错误提示
function showErrorToast(message) {
  ElMessage({
    type: 'error',
    message,
    duration: 3000
  })
}

// 处理登出逻辑
function handleLogout() {
  store.dispatch('user/logout')
  router.push('/login')
}

export default service
2、登录

将获取到的Token保存在localStorage或store中,下述代码是保存在pinia store中

 //登录成功
        showSuccessToast(result.msg);
        // 关键:使用 Pinia 存储 token
        userStore.login(result.token, { username: username.value })
        router.push("/home")
3、访问受保护的接口

因为封装好的axios在发送请求时会自动携带Token,所以访问受保护的接口时无需再额外处理

五、常见问题处理

1. 403 Forbidden 错误
// 检查是否配置了CSRF保护(REST API通常需要禁用)
http.csrf().disable()
2. Token 不生效
// 确保请求头包含:
Authorization: Bearer <your_token>

// 检查CORS配置是否允许Authorization头
.allowedHeaders("Authorization", "Content-Type")
3. 用户认证失败
// 检查UserDetailsService实现
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 必须从数据库或其他存储中加载用户信息
}

六、注意事项

因为跨域请求会提前发送一个Request Method为OPTIONS的预检请求,而此请求是浏览器自动发送的,不会携带Token,所以后端必须放行所有预检请求才行。

### Spring Boot、Vue 3JWT 实现登录功能 为了实现基于 Spring Boot、Vue 3JWT 的用户认证系统,整个流程可以分为前端部分和后端部分。 #### 后端配置(Spring Boot) 创建一个简单的 RESTful API 来处理用户的登录请求。当接收到有效的用户名密码组合时,服务器会返回带有访问令牌的响应给客户端应用[^1]。 ```java @RestController public class AuthController { @PostMapping("/login") public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) { // 验证逻辑... String token = jwtUtils.generateJwtToken(authentication); return ResponseEntity.ok(new JwtResponse(token)); } } ``` 此代码片段展示了如何设置控制器来接收 POST 请求并验证传入的数据。如果一切正常,则调用 `jwtUtils` 中的方法生成一个新的 JSON Web Token 并将其作为响应的一部分发送回去。 #### 前端集成(Vue 3) 在 Vue 应用程序中,可以通过 Axios 或其他 HTTP 客户端库向上述定义的服务发起请求。下面是一个简化版的例子说明怎样通过提交表单来进行身份验证: ```javascript import axios from &#39;axios&#39;; export async function loginUser(credentials) { try { const response = await axios.post(&#39;/api/auth/login&#39;, credentials); console.log(response.data.accessToken); // 存储或使用获取到的token localStorage.setItem("access-admin",response.data.token); return true; } catch (error) { alert(&#39;登录信息失败&#39;); localStorage.removeItem("access-admin"); throw error; } } ``` 这段 JavaScript 函数尝试连接至 `/api/auth/login` 路径下的服务,并传递包含凭证的对象;成功之后它将把获得的 access_token 存储起来以便后续操作使用[^2]。 对于错误情况,如未能正确完成身份验证过程的情况,在这里也加入了相应的异常捕获机制以及清理本地存储中的潜在过期 tokens 的措施。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值