
这是一个完整的Java Web后端与Vue前端跨域通信的实现方案。以下是完整的代码和配置说明:
1. 完整的ApiFilter实现
package org.example;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class ApiFilter implements Filter {
private ConcurrentHashMap<String, AtomicInteger> requestCounts;
private static final int RATE_LIMIT = 100; // 每分钟最多100次请求
private static final long TIME_WINDOW = 60000; // 1分钟时间窗口
private static final int SC_TOO_MANY_REQUESTS = 429;
// CORS配置
private static final String ALLOWED_ORIGINS = "http://localhost:8082";
private static final String ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS, PATCH";
private static final String ALLOWED_HEADERS = "Authorization, Content-Type, X-Requested-With, Accept, Origin";
private static final String EXPOSE_HEADERS = "Location, Content-Disposition, Authorization";
private static final boolean ALLOW_CREDENTIALS = true;
private static final long MAX_AGE = 3600; // 1小时
// 不需要Token验证的白名单路径
private static final String[] WHITELIST_PATHS = {
"/api/login",
"/api/register",
"/api/auth/login",
"/api/auth/register",
"/api/public/"
};
@Override
public void init(FilterConfig filterConfig) throws ServletException {
requestCounts = new ConcurrentHashMap<>();
// 启动清理线程,定期清理过期计数
Thread cleaner = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(TIME_WINDOW);
requestCounts.clear();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
cleaner.setDaemon(true);
cleaner.start();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 1. 添加CORS响应头(必须在任何响应之前)
addCorsHeaders(httpRequest, httpResponse);
// 2. 处理OPTIONS预检请求
if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
httpResponse.setStatus(HttpServletResponse.SC_OK);
return; // 直接返回,不继续处理
}
// 3. 获取请求路径并检查是否为白名单
String requestPath = httpRequest.getRequestURI();
String contextPath = httpRequest.getContextPath();
// 移除上下文路径
if (contextPath != null && !"/".equals(contextPath) && requestPath.startsWith(contextPath)) {
requestPath = requestPath.substring(contextPath.length());
}
// 检查是否为白名单路径
boolean isWhitelisted = isWhitelistedPath(requestPath);
// 4. Token验证(白名单路径跳过)
if (!isWhitelisted) {
String token = httpRequest.getHeader("Authorization");
if (!isValidToken(token)) {
// 返回JSON格式的错误信息
sendJsonErrorResponse(httpResponse, HttpServletResponse.SC_UNAUTHORIZED,
"未授权访问,请先登录");
return;
}
}
// 5. 记录日志
logRequest(httpRequest);
// 6. 限流检查
if (isRateLimited(httpRequest)) {
sendJsonErrorResponse(httpResponse, SC_TOO_MANY_REQUESTS,
"请求频率过高,请稍后再试");
return;
}
// 放行请求
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 释放资源
}
/**
* 添加CORS响应头
*/
private void addCorsHeaders(HttpServletRequest request, HttpServletResponse response) {
String origin = request.getHeader("Origin");
// 允许指定源的跨域请求
if (origin != null && (origin.contains("localhost") || origin.contains("127.0.0.1"))) {
response.setHeader("Access-Control-Allow-Origin", origin);
} else {
// 生产环境应该配置具体的域名
response.setHeader("Access-Control-Allow-Origin", ALLOWED_ORIGINS);
}
response.setHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
response.setHeader("Access-Control-Allow-Headers", ALLOWED_HEADERS);
response.setHeader("Access-Control-Expose-Headers", EXPOSE_HEADERS);
response.setHeader("Access-Control-Max-Age", String.valueOf(MAX_AGE));
if (ALLOW_CREDENTIALS) {
response.setHeader("Access-Control-Allow-Credentials", "true");
}
// 对于预检请求,添加额外的头
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setHeader("Access-Control-Allow-Headers",
"Authorization, Content-Type, X-Requested-With, Accept, Origin, " +
"Access-Control-Request-Method, Access-Control-Request-Headers, X-Requested-With");
}
}
/**
* 检查是否为白名单路径
*/
private boolean isWhitelistedPath(String path) {
for (String whitelistPath : WHITELIST_PATHS) {
if (path.equals(whitelistPath) || path.startsWith(whitelistPath)) {
return true;
}
}
return false;
}
/**
* Token验证逻辑
*/
private boolean isValidToken(String token) {
if (token == null || token.trim().isEmpty()) {
return false;
}
// 移除Bearer前缀(如果存在)
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
// 这里应该实现真正的Token验证逻辑
// 例如:验证JWT Token的有效性、过期时间等
// 暂时返回true,实际项目中需要根据业务逻辑实现
return validateJwtToken(token);
}
/**
* 验证JWT Token(示例)
*/
private boolean validateJwtToken(String token) {
// TODO: 实现JWT Token验证逻辑
// 这里应该是实际的Token验证,例如:
// 1. 解析JWT
// 2. 验证签名
// 3. 检查过期时间
// 4. 验证用户信息
// 暂时返回true
return true;
}
/**
* 发送JSON格式的错误响应
*/
private void sendJsonErrorResponse(HttpServletResponse response, int statusCode, String message)
throws IOException {
response.setStatus(statusCode);
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
String jsonResponse = String.format(
"{\"success\": false, \"code\": %d, \"message\": \"%s\", \"timestamp\": %d}",
statusCode, message, System.currentTimeMillis()
);
response.getWriter().write(jsonResponse);
response.getWriter().flush();
}
private void logRequest(HttpServletRequest request) {
String clientIP = getClientIP(request);
String userAgent = request.getHeader("User-Agent");
String method = request.getMethod();
String uri = request.getRequestURI();
String queryString = request.getQueryString();
String logMessage = String.format(
"API请求: %s %s%s | IP: %s | User-Agent: %s",
method,
uri,
queryString != null ? "?" + queryString : "",
clientIP,
userAgent != null ? userAgent : "Unknown"
);
System.out.println(logMessage);
}
private boolean isRateLimited(HttpServletRequest request) {
String clientIP = getClientIP(request);
AtomicInteger count = requestCounts.computeIfAbsent(clientIP, k -> new AtomicInteger(0));
int currentCount = count.incrementAndGet();
// 简单限流:每分钟最多RATE_LIMIT次请求
boolean limited = currentCount > RATE_LIMIT;
if (limited) {
System.out.println("限流客户端: " + clientIP + ", 当前计数: " + currentCount);
}
return limited;
}
private String getClientIP(HttpServletRequest request) {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader == null) {
return request.getRemoteAddr();
}
// X-Forwarded-For可能包含多个IP,取第一个
return xfHeader.split(",")[0].trim();
}
}
2. Vue前端配置
2.1 Axios全局配置
// src/utils/axios.js
import axios from 'axios';
import { Message } from 'element-ui'; // 如果你使用Element UI
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API || 'http://localhost:8081',
timeout: 10000, // 请求超时时间
withCredentials: true, // 允许携带cookie
});
// 请求拦截器
service.interceptors.request.use(
config => {
// 从本地存储获取token
const token = localStorage.getItem('access_token') ||
sessionStorage.getItem('access_token');
// 如果token存在,将其添加到请求头
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 设置Content-Type
if (!config.headers['Content-Type']) {
config.headers['Content-Type'] = 'application/json';
}
return config;
},
error => {
console.error('请求错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data;
// 如果返回的code不是200,则判断为错误
if (res.code && res.code !== 200) {
Message({
message: res.message || '请求失败',
type: 'error',
duration: 5 * 1000
});
// 401: 未登录
if (res.code === 401) {
// 清除token并跳转到登录页
localStorage.removeItem('access_token');
sessionStorage.removeItem('access_token');
window.location.href = '/login';
}
// 429: 请求频率过高
if (res.code === 429) {
Message({
message: '操作过于频繁,请稍后再试',
type: 'warning',
duration: 3 * 1000
});
}
return Promise.reject(new Error(res.message || 'Error'));
} else {
return res;
}
},
error => {
console.error('响应错误:', error);
// 处理HTTP错误
if (error.response) {
switch (error.response.status) {
case 401:
Message({
message: '登录已过期,请重新登录',
type: 'error',
duration: 5 * 1000
});
localStorage.removeItem('access_token');
sessionStorage.removeItem('access_token');
window.location.href = '/login';
break;
case 403:
Message({
message: '没有权限访问',
type: 'error',
duration: 5 * 1000
});
break;
case 404:
Message({
message: '请求的资源不存在',
type: 'error',
duration: 5 * 1000
});
break;
case 429:
Message({
message: '请求频率过高,请稍后再试',
type: 'warning',
duration: 5 * 1000
});
break;
case 500:
Message({
message: '服务器内部错误',
type: 'error',
duration: 5 * 1000
});
break;
default:
Message({
message: error.response.data?.message || '请求失败',
type: 'error',
duration: 5 * 1000
});
}
} else if (error.request) {
Message({
message: '网络连接失败,请检查网络设置',
type: 'error',
duration: 5 * 1000
});
} else {
Message({
message: error.message || '请求失败',
type: 'error',
duration: 5 * 1000
});
}
return Promise.reject(error);
}
);
export default service;
2.2 Vue环境配置
// vue.config.js
module.exports = {
devServer: {
port: 8082, // 前端开发服务器端口
proxy: {
'/api': {
target: 'http://localhost:8081', // 后端地址
changeOrigin: true, // 允许跨域
ws: true, // 代理websockets
pathRewrite: {
'^/api': '' // 重写路径
}
}
}
},
// 其他配置...
};
2.3 Vue登录组件示例
<template>
<div class="login-container">
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
class="login-form"
label-position="left"
>
<h3 class="title">系统登录</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="请输入用户名"
prefix-icon="el-icon-user"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="请输入密码"
prefix-icon="el-icon-lock"
@keyup.enter.native="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="rememberMe">记住密码</el-checkbox>
</el-form-item>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
type="primary"
style="width:100%;"
@click.native.prevent="handleLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { login } from '@/api/user';
import { setToken } from '@/utils/auth';
export default {
name: 'Login',
data() {
return {
loginForm: {
username: '',
password: ''
},
loginRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
},
rememberMe: false,
loading: false
};
},
mounted() {
// 从本地存储获取记住的密码
const rememberedUsername = localStorage.getItem('remembered_username');
if (rememberedUsername) {
this.loginForm.username = rememberedUsername;
this.rememberMe = true;
}
},
methods: {
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
// 调用登录接口
login({
username: this.loginForm.username,
password: this.loginForm.password
})
.then(response => {
// 登录成功
const token = response.data.token;
// 保存token
if (this.rememberMe) {
// 长期存储
localStorage.setItem('access_token', token);
localStorage.setItem('remembered_username', this.loginForm.username);
} else {
// 会话存储
sessionStorage.setItem('access_token', token);
localStorage.removeItem('remembered_username');
}
// 设置axios默认header
this.$axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// 跳转到首页
this.$router.push('/dashboard');
})
.catch(error => {
console.error('登录失败:', error);
this.$message.error(error.message || '登录失败');
})
.finally(() => {
this.loading = false;
});
} else {
console.log('表单验证失败');
return false;
}
});
}
}
};
</script>
<style scoped>
.login-container {
height: 100vh;
background-color: #2d3a4b;
display: flex;
justify-content: center;
align-items: center;
}
.login-form {
width: 400px;
padding: 35px;
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.title {
margin: 0 auto 40px;
text-align: center;
color: #707070;
font-size: 26px;
font-weight: 700;
}
</style>
2.4 API接口封装
// src/api/user.js
import request from '@/utils/axios';
export function login(data) {
return request({
url: '/api/login',
method: 'post',
data
});
}
export function logout() {
return request({
url: '/api/logout',
method: 'post'
});
}
export function getUserInfo() {
return request({
url: '/api/user/info',
method: 'get'
});
}
export function register(data) {
return request({
url: '/api/register',
method: 'post',
data
});
}
3. 后端登录API实现(JSON版本)
package org.example;
import com.google.gson.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.stream.Collectors;
@WebServlet("/api/login")
public class LoginApiServlet extends HttpServlet {
private UserDAO userDAO;
private Gson gson = new Gson();
@Override
public void init() {
userDAO = new UserDAO();
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置响应头
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
// 设置CORS头
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8082");
response.setHeader("Access-Control-Allow-Credentials", "true");
PrintWriter out = response.getWriter();
JsonObject jsonResponse = new JsonObject();
try {
// 读取请求体中的JSON数据
String requestBody = request.getReader().lines()
.collect(Collectors.joining(System.lineSeparator()));
// 解析JSON
JsonObject jsonObject = gson.fromJson(requestBody, JsonObject.class);
if (jsonObject == null) {
jsonResponse.addProperty("success", false);
jsonResponse.addProperty("message", "请求格式错误");
jsonResponse.addProperty("code", 400);
response.setStatus(400);
out.print(gson.toJson(jsonResponse));
return;
}
// 获取用户名和密码
String username = jsonObject.has("username") ?
jsonObject.get("username").getAsString() : null;
String password = jsonObject.has("password") ?
jsonObject.get("password").getAsString() : null;
// 验证参数
if (username == null || username.trim().isEmpty() ||
password == null || password.trim().isEmpty()) {
jsonResponse.addProperty("success", false);
jsonResponse.addProperty("message", "用户名和密码不能为空");
jsonResponse.addProperty("code", 400);
response.setStatus(400);
out.print(gson.toJson(jsonResponse));
return;
}
// 创建用户对象进行验证
User user = new User();
user.setUsername(username);
user.setPassword(password);
// 验证用户凭据
if (userDAO.validate(user)) {
// 生成Token(实际应该使用JWT)
String token = generateToken(username);
// 获取用户信息
User fullUser = userDAO.selectUserByUsername(username);
// 构建响应数据
jsonResponse.addProperty("success", true);
jsonResponse.addProperty("message", "登录成功");
jsonResponse.addProperty("code", 200);
jsonResponse.addProperty("token", token);
JsonObject userObj = new JsonObject();
userObj.addProperty("id", fullUser.getId());
userObj.addProperty("username", fullUser.getUsername());
userObj.addProperty("email", fullUser.getEmail());
jsonResponse.add("data", userObj);
response.setStatus(200);
out.print(gson.toJson(jsonResponse));
} else {
jsonResponse.addProperty("success", false);
jsonResponse.addProperty("message", "用户名或密码错误");
jsonResponse.addProperty("code", 401);
response.setStatus(401);
out.print(gson.toJson(jsonResponse));
}
} catch (JsonSyntaxException e) {
jsonResponse.addProperty("success", false);
jsonResponse.addProperty("message", "JSON格式错误");
jsonResponse.addProperty("code", 400);
response.setStatus(400);
out.print(gson.toJson(jsonResponse));
} catch (Exception e) {
e.printStackTrace();
jsonResponse.addProperty("success", false);
jsonResponse.addProperty("message", "服务器内部错误");
jsonResponse.addProperty("code", 500);
response.setStatus(500);
out.print(gson.toJson(jsonResponse));
} finally {
out.flush();
out.close();
}
}
@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 处理OPTIONS预检请求
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8082");
response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
response.setStatus(HttpServletResponse.SC_OK);
}
private String generateToken(String username) {
// 实际应该生成JWT token
// 这里只是示例,应该使用更安全的方式
return "jwt_token_" + username + "_" + System.currentTimeMillis();
}
}
4. 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_3_1.xsd"
version="3.1">
<!-- 字符编码过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.example.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- API过滤器(处理CORS、限流、Token验证) -->
<filter>
<filter-name>ApiFilter</filter-name>
<filter-class>org.example.ApiFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ApiFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
<!-- API Servlet -->
<servlet>
<servlet-name>LoginApiServlet</servlet-name>
<servlet-class>org.example.LoginApiServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginApiServlet</servlet-name>
<url-pattern>/api/login</url-pattern>
</servlet-mapping>
<!-- 其他Servlet配置... -->
<!-- 欢迎页面 -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
5. 字符编码过滤器
package org.example;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class CharacterEncodingFilter implements Filter {
private String encoding = "UTF-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String encodingParam = filterConfig.getInitParameter("encoding");
if (encodingParam != null) {
encoding = encodingParam;
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setCharacterEncoding(encoding);
response.setCharacterEncoding(encoding);
response.setContentType("text/html;charset=" + encoding);
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 清理资源
}
}
6. 关键问题解决
6.1 跨域问题
-
预检请求(OPTIONS)处理:过滤器需要正确处理OPTIONS请求
-
CORS头设置:必须在响应中正确设置CORS头
-
凭证(Credentials):如果前端需要发送Cookie,必须设置
Access-Control-Allow-Credentials: true
6.2 认证流程
-
登录接口:不需要Token验证,返回Token
-
其他接口:需要携带Token验证
-
Token存储:前端将Token存储在localStorage或sessionStorage中
6.3 限流策略
-
基于IP限流:防止恶意请求
-
白名单例外:登录等接口不限流
-
错误响应:返回429状态码和友好提示
7. 测试步骤
-
启动后端:在8081端口启动Java Web应用
-
启动前端:在8082端口启动Vue应用
-
登录测试:前端调用
/api/login接口 -
Token验证:登录成功后,后续请求自动携带Token
-
跨域测试:确保前端可以正常访问后端API
这个方案实现了完整的Java Web + Vue前后端分离跨域通信,包含了安全认证、限流、错误处理等功能。
145

被折叠的 条评论
为什么被折叠?



