解决springboot+vue静态资源刷新后无法访问的问题

server/2024/9/20 13:38:14/ 标签: spring boot, 后端, vue3

一、背景

原项目是有前后端分离设计,测试环境是centos系统,采用nginx代理和转发,项目正常运行。
项目近期上线到正式环境,结果更换了系统环境,需要放到一台windows系统中,前后端打成一个jar包,然后做成系统服务。这台服务器中已经有很多其他服务,都是采用一样的部署方式,所以没办法只能对这个项目进行修改。

二、修改过程

2.1 首先看项目结构

在这里插入图片描述
admin是后端代码,使用的是springboot,使用 spring security 权限控制;UI是前端,使用的是vue3+vite

admin的结构
在这里插入图片描述
ui的结构
在这里插入图片描述

2.2 打包静态资源

修改前端打包配置vite.config.js

import { defineConfig, loadEnv } from 'vite'
import path from 'path'
import createVitePlugins from './vite/plugins'// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {const env = loadEnv(mode, process.cwd())const { VITE_APP_ENV } = envreturn {// 部署生产环境和开发环境下的URL。// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上base: VITE_APP_ENV === 'production' ? '/' : '/',build: {outDir: '../admin/src/main/resources/static'},plugins: createVitePlugins(env, command === 'build'),resolve: {// https://cn.vitejs.dev/config/#resolve-aliasalias: {// 设置路径'~': path.resolve(__dirname, './'),// 设置别名'@': path.resolve(__dirname, './src')},// https://cn.vitejs.dev/config/#resolve-extensionsextensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']},// vite 相关配置server: {port: 888,host: true,open: true,proxy: {// https://cn.vitejs.dev/config/#server-proxy'/': {target: 'http://localhost:8080',changeOrigin: true,// rewrite: (p) => p.replace(/^\/api/, '')}}},//fix:error:stdin>:7356:1: warning: "@charset" must be the first rule in the filecss: {postcss: {plugins: [{postcssPlugin: 'internal:charset-removal',AtRule: {charset: (atRule) => {if (atRule.name === 'charset') {atRule.remove();}}}}]}}}
})

增加下面代码

build: {outDir: '../admin/src/main/resources/static'
},

指定编译后的静态文件存放目录,默认的是在ui/dist目录,然后执行生产环境打包命令 npm run prod / npm run build,成功后会在admin/resource目录下生成一个static文件夹
在这里插入图片描述
在这里插入图片描述

同时,把前端路径与后端路径冲突的修改一下。

2.3 修改后端权限控制

修改SecurityConfig,增加静态资源的访问权限

/*** spring security配置** @author admin*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 自定义用户认证逻辑*/@Resourceprivate UserDetailsService userDetailsService;/*** 认证失败处理类*/@Resourceprivate AuthenticationEntryPointImpl unauthorizedHandler;/*** 退出处理类*/@Resourceprivate LogoutSuccessHandlerImpl logoutSuccessHandler;/*** token认证过滤器*/@Resourceprivate JwtAuthenticationTokenFilter authenticationTokenFilter;/*** 跨域过滤器*/@Resourceprivate CorsFilter corsFilter;/*** 允许匿名访问的地址*/@Resourceprivate PermitAllUrlProperties permitAllUrl;/*** 鉴权*/@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {// 注解标记允许匿名访问的urlExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());httpSecurity// CSRF禁用,因为不使用session.csrf().disable()// 禁用HTTP响应标头.headers().cacheControl().disable().and()// 认证失败处理类.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()// 基于token,所以不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 过滤请求.authorizeRequests()// 对于登录login 验证码captchaImage 允许匿名访问.antMatchers("/login", "/sendSmsCode/*", "/captchaImage").permitAll()// 静态资源,可匿名访问.antMatchers(HttpMethod.GET, "/", "/*.html", "/*.html.gz", "/assets/**", "/favicon.ico", "/profile/**").permitAll().antMatchers("/webjars/**", "/druid/**").permitAll()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated().and().headers().frameOptions().disable();// 添加Logout filterhttpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);// 添加JWT filterhttpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 添加CORS filterhttpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);}
}

静态文件处理类ResourcesConfig

@Configuration
public class ResourcesConfig implements WebMvcConfigurer {@Resourceprivate RepeatSubmitInterceptor repeatSubmitInterceptor;@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {/** 本地文件上传路径,FileConfig.getPath() 为本地磁盘目录 */registry.addResourceHandler("/profile/**").addResourceLocations("file:" + FileConfig.getPath() + "/");/** 静态资源 */registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");}/*** 自定义拦截规则*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");}/*** 跨域配置*/@Beanpublic CorsFilter corsFilter() {CorsConfiguration config = new CorsConfiguration();config.setAllowCredentials(true);// 设置访问源地址config.addAllowedOriginPattern("*");// 设置访问源请求头config.addAllowedHeader("*");// 设置访问源请求方法config.addAllowedMethod("*");// 有效期 1800秒config.setMaxAge(1800L);// 添加映射路径,拦截一切请求UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", config);// 返回新的CorsFilterreturn new CorsFilter(source);}
}

认证失败处理类AuthenticationEntryPointImpl,解决退出成功后无法跳转到登录页的问题


/*** 认证失败处理类 返回未授权** @author admin*/
@Slf4j
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {private static final long serialVersionUID = -8970718410437077606L;/*** 向调用者提供有关哪些HTTP端口与系统上的哪些HTTPS端口相关联的信息*/private PortMapper portMapper = new PortMapperImpl();/*** 端口解析器,基于请求解析出端口*/private PortResolver portResolver = new PortResolverImpl();/*** 登陆页面URL*/private String loginFormUrl;/*** 默认为false,即不强制Https转发或重定向*/private boolean forceHttps = false;/*** 默认为false,即不是转发到登陆页面,而是进行重定向*/private boolean useForward = false;/*** 重定向策略*/private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();public String getLoginFormUrl() {return loginFormUrl;}public void setLoginFormUrl(String loginFormUrl) {this.loginFormUrl = loginFormUrl;}/*** 允许子类修改成适用于给定请求的登录表单URL*/protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {return getLoginFormUrl();}@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {String redirectUrl = null;if (useForward) {if (forceHttps && HttpScheme.HTTP.name().equals(request.getScheme())) {redirectUrl = buildHttpsRedirectUrlForRequest(request);}if (redirectUrl == null) {String loginForm = determineUrlToUseForThisRequest(request, response, authException);RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);dispatcher.forward(request, response);return;}} else {redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);}redirectStrategy.sendRedirect(request, response, redirectUrl);}/*** 构建重定向URL** @param request* @param response* @param authException* @return*/protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {// 通过determineUrlToUseForThisRequest方法获取URLString loginForm = determineUrlToUseForThisRequest(request, response, authException);// 如果是绝对URL,直接返回if (UrlUtils.isAbsoluteUrl(loginForm)) {return loginForm;}// 如果是相对URL// 构造重定向URLint serverPort = portResolver.getServerPort(request);String scheme = request.getScheme();RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();urlBuilder.setScheme(scheme);urlBuilder.setServerName(request.getServerName());urlBuilder.setPort(serverPort);urlBuilder.setContextPath(request.getContextPath());urlBuilder.setPathInfo(loginForm);if (forceHttps && HttpScheme.HTTP.name().equals(scheme)) {Integer httpsPort = portMapper.lookupHttpsPort(serverPort);if (httpsPort != null) {// 覆盖重定向URL中的scheme和porturlBuilder.setScheme("https");urlBuilder.setPort(httpsPort);} else {log.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port " + serverPort);}}return urlBuilder.getUrl();}/*** 构建一个URL以将提供的请求重定向到HTTPS* 用于在转发到登录页面之前将当前请求重定向到HTTPS*/protected String buildHttpsRedirectUrlForRequest(HttpServletRequest request) throws IOException, ServletException {int serverPort = portResolver.getServerPort(request);Integer httpsPort = portMapper.lookupHttpsPort(serverPort);if (httpsPort != null) {RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();urlBuilder.setScheme("https");urlBuilder.setServerName(request.getServerName());urlBuilder.setPort(httpsPort);urlBuilder.setContextPath(request.getContextPath());urlBuilder.setServletPath(request.getServletPath());urlBuilder.setPathInfo(request.getPathInfo());urlBuilder.setQuery(request.getQueryString());return urlBuilder.getUrl();}// 通过警告消息进入服务器端转发log.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port " + serverPort);return null;}/*** 设置为true以强制通过https访问登录表单* 如果此值为true(默认为false),并且触发拦截器的请求还不是https* 则客户端将首先重定向到https URL,即使serverSideRedirect(服务器端转发)设置为true*/public void setForceHttps(boolean forceHttps) {this.forceHttps = forceHttps;}/*** 是否要使用RequestDispatcher转发到loginFormUrl,而不是302重定向*/public void setUseForward(boolean useForward) {this.useForward = useForward;}
}

增加一个刷新跳转处理类 ServletConfig,很关键。这个方案由博主 @云散不过浅浅一下(原出处https://blog.csdn.net/twinkle2star/article/details/105191782) 提供

@Configuration
public class ServletConfig {@Beanpublic WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {return factory -> {// 404时跳转到首页ErrorPage errorPage = new ErrorPage(HttpStatus.NOT_FOUND, "/index.html");factory.addErrorPages(errorPage);};}
}

修改一下TokenService类,增加从前端页面请求中获取Cookie,并且获取token

/*** 获取用户身份信息** @return 用户信息*/
public LoginUser getLoginUser(HttpServletRequest request) {// 获取请求携带的令牌String token = getToken(request);if (StringUtils.isBlank(token)) {// 增加从前端页面请求中获取Cookie,并且获取tokenCookie cookie = Arrays.stream(request.getCookies()).filter(item -> "Admin-Token".equals(item.getName())).findFirst().orElse(null);if (cookie != null) {token = cookie.getValue();}}if (StringUtils.isNotEmpty(token)) {try {Claims claims = parseToken(token);// 解析对应的权限以及用户信息,Constants.LOGIN_USER_KEY为自定义redis keyString uuid = (String) claims.get(Constants.LOGIN_USER_KEY);String userKey = getTokenKey(uuid);return redisCache.getCacheObject(userKey);} catch (Exception e) {}}return null;
}

大概的修改步骤就是这样,启动后顺利运行,跟前后端分离没有什么区别。


http://www.ppmy.cn/server/41839.html

相关文章

Ubuntu24安装搜狗输入法,修复闪屏问题

下载deb安装包&#xff1a;搜狗输入法linux-首页 安装&#xff1a;sudo dpkg -i 1.deb 搜狗输入法linux-安装指导 重启&#xff0c;但是完成后闪烁。按以下步骤更改桌面配置。 sudo gedit /etc/gdm3/custom.conf 取消WaylandEnable的注释即可

免费WordPress主题可以商用吗?不可以

免费WordPress主题可以商用吗?不可以商用。 使用WordPress免费主题进行商业活动确实存在一些风险和不推荐的原因&#xff0c;以下是一些额外的不能商用的理由&#xff1a; 缺乏专业性&#xff1a;免费主题往往是由个人开发者或爱好者制作的&#xff0c;可能缺乏专业设计和开…

深⼊理解指针(5)

目录 1. 回调函数是什么&#xff1f;1.1 使用回调函数修改 2. qsort使⽤举例2.1 使⽤qsort函数排序整型数2.2 使⽤qsort排序结构数据按年龄排序2.3 使⽤qsort排序结构数据按名字排序2.4整体代码 3. qsort函数的模拟实现3.1 整型数组的实现3.2 结构体按名字排序实现3.3 结构体按…

机器学习-监督学习

监督学习是机器学习和人工智能中的一个重要分支&#xff0c;它涉及使用已标记的数据集来训练算法&#xff0c;以便对数据进行分类或准确预测结果。监督学习的核心在于通过输入数据&#xff08;特征&#xff09;和输出数据&#xff08;标签或类别&#xff09;之间的关系&#xf…

【LLM 论文】Least-to-Most Prompting 让 LLM 实现复杂推理

论文&#xff1a;Least-to-Most Prompting Enables Complex Reasoning in Large Language Models ⭐⭐⭐ Google Research, ICLR 2023 论文速读 Chain-of-Thought&#xff08;CoT&#xff09; prompting 的方法通过结合 few-show prompt 的思路&#xff0c;让 LLM 能够挑战更具…

【脚本】使用脚本备份docker中部署的mysql数据库

v1版本明文密码方式&#xff1a; #!/bin/bash# 定义 MySQL 容器名称和数据库信息 container_name"mysql_container" db_user"root" db_password"your_password"# 定义要备份的数据库列表 databases("database1" "database2"…

【Go】Go Swagger 生成和转 openapi 3.0.3

本文档主要描述在 gin 框架下用 gin-swagger 生成 swagger.json 的内容&#xff0c;中间猜的坑。以及&#xff0c;如何把 swagger 2.0 转成 openapi 3.0.3 下面操作均在项目根目录下执行 生成 swagger 2.0 import swagger go get -u github.com/swaggo/gin-swagger go get …

【STM32 |新建一个工程】基于标准库(库函数)新建工程

目录 STM32开发方式 库函数文件夹 建工程步骤 库函数工程建立 建立工程总结 STM32开发方式 目前stm32的开发方式主要有基于寄存器的方式、基于标准库的方式&#xff08;库函数的方式&#xff09;、基于HAL库的方式基于库函数的方式是使用ST官方提供的封装好的函数&…

QT---day5,通信

1、思维导图 2、TCp 服务器 #ifndef MYWIDGET_H #define MYWIDGET_H #include <QWidget> #include <QTcpServer> #include <QList> #include <QTcpSocket> #include <QMessageBox> #include <QDebug> #include <QTcpServer> QT_B…

学习MySQL(二):库表的操作

库的增删改查 增 -- 创建库 create database 库名 charset 字符编码; 删 -- 删除库 drop database 库名; 改 -- 修改字符编码 alter database 库名 charset 字符编码; # 注&#xff1a;一般只改字符编码&#xff0c;数据库名称是不能改的 查 -- 查询当前账户下所有的库…

AppBuilder低代码体验:构建雅思大作文组件

AppBuilder低代码体验&#xff1a;构建雅思大作文组件 ​ 在4月14日&#xff0c;AppBuilder赢来了一次大更新&#xff0c;具体更新内容见&#xff1a;AppBuilder 2024.04.14发版上线公告 。本次更新最大的亮点就是**新增了工作流&#xff0c;低代码制作组件。**具体包括&#x…

正点原子FreeRTOS学习笔记——列表与列表项

目录 一、什么是列表和列表项 1、概念 2、FreeRTOS代码 &#xff08;1&#xff09;列表 &#xff08;2&#xff09;列表项 &#xff08;3&#xff09;迷你列表项 二、列表与列表项初始化 1、列表初始化 2、列表项初始化 三、列表插入与删除列表项 1、原理解释 2、…

AD域服务器巡检指南

Active Directory (AD) 域服务器的巡检对于确保企业网络的安全性和高效运行至关重要。以下是针对AD域服务器巡检的关键活动和其重要性的优化描述&#xff1a; 保证系统安全&#xff1a; AD域服务器储存大量敏感数据&#xff0c;包括用户账户信息、策略和访问权限数据。定期巡检…

构建NFS远程共享存储

nfs-server:10.1.59.237 nfs-web:10..159.218 centos7,服务端和客户端都关闭防火墙和selinux内核防火墙&#xff0c;如果公司要求开启防火墙&#xff0c;那需要放行几个端口 firewall-cmd --add-port2049/tcp --permanent firewall-cmd --add-port111/tcp --permanent firew…

知识付费系统怎么操作的,培训机构怎么用老带新招生呢?

随着暑假竞争的日益剧烈&#xff0c;各类教育培训机构早已准备着各种招生活动&#xff0c;打算进一步进步学校的招生数量。但是也有很多招生问题在搅扰着学校的招生教师&#xff0c;其实一切的招生活动&#xff0c;都必需效劳于重生报名和老生续报!培训机构如何做好老带新招生?…

7集成学习评分卡

集成学习评分卡 学习目标 知道LightGBM基本原理掌握使用lightGBM进行特征筛选的方法1 Gradient Boosting算法回顾 Gradient Boosting 基本原理 训练一个模型m1,产生错误e1针对e1训练一个模型m2,产生错误e2针对e2训练第三个模型m3,产生错误e3 …最终预测结果是:m1+m2+m3+…GB…

Oracle21c数据库普通用户创建及授权,建表,创建存储过程、序列、触发器

一、Oracle数据库错误 ORA-65096 表示你尝试在多租户容器数据库&#xff08;CDB&#xff09;环境中创建一个公共用户&#xff08;common user&#xff09;或角色&#xff0c;但没有使用正确的前缀。在多租户架构中&#xff0c;公共用户的用户名必须以 C## 或 c## 开头。 若想…

【AI智能体】零代码构建AI应用,全网都在喊话歌手谁能应战,一键AI制作歌手信息查询应用

欢迎来到《小5讲堂》 这是《文心智能体平台》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 文心智能体大赛背景创建应用平台地址快速构建【基础配置】…

【Web后端】servlet基本概念

1.ServletAPI架构 HttpServlet继承GenericServletGenericServlet实现了Servlet接口&#xff0c;ServletConfig接口,Serializable接口自定义Servlet继承HttpServlet 2.Servlet生命周期 第一步&#xff1a;容器加载Servlet第二步&#xff1a;调用Servlet的无参构造方法&#xf…

Android 获取已安装应用、包名、应用名、版本号、版本名

1、相关代码 List<ApplicationInfo> installedApps getPackageManager().getInstalledApplications(0);for (ApplicationInfo appInfo : installedApps) {CharSequence getAppName getPackageManager().getApplicationLabel(appInfo);String appNamegetAppName.toStrin…