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

一、背景

原项目是有前后端分离设计,测试环境是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.mzph.cn/news/838671.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python重复文件清理小工具

针对电脑长期使用产生的重复文件&#xff0c;尤其是微信电脑版每转发一次生成一个重复文件的问题&#xff0c;用python写了一个批量清理重复文件的小工具&#xff0c;记录备用。 import shutil import tkinter from tkinter import filedialog import os import threading imp…

美港通正规股票交易市场人民币突然拉升,市场开启“大风车”模式?

查查配今天上午,市场又开启了“大风车”模式,多个热点轮番拉升。 一则关于地产行业利好的小作文流出,地产产业链上午爆发,租售同权、房地产服务、房地产开发等板块大涨,光大嘉宝、天地源等个股涨停。万科A涨超4%。 美港通证券以其专业的服务和较低的管理费用在市场中受到不少…

如何在Sui智能合约中验证是否为多签地址

通过多签合约实现多个用户可访问的安全账户。多签&#xff08;multi-sig&#xff09;钱包和账户通过允许多个用户在预定义条件下访问共享资产&#xff0c;或让单个用户实施额外的安全措施&#xff0c;从而增强密钥管理。例如&#xff0c;多签钱包可以用于管理去中心化自治组织&…

智慧教育平台:选课系统的Spring Boot实现

作者介绍&#xff1a;✌️大厂全栈码农|毕设实战开发&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 &#x1f345;获取源码联系方式请查看文末&#x1f345; 推荐订阅精彩专栏 &#x1f447;&#x1f3fb; 避免错过下次更新 Springboot项目精选实战案例 更多项目…

【Hive】窗口函数(移位函数案例、排序函数案例)(四)

Hive 窗口函数 分析函数 聚合函数&#xff0c;例如sum&#xff0c;avg&#xff0c;max&#xff0c;min等移位函数 lag(colName, n)&#xff1a;以当前行为基础&#xff0c;来处理第前n行的数据lead(colName, n)&#xff1a;以当前行为基础&#xff0c;来处理第后n行的数据nt…

Verilog基础语法——条件语句if-else与case

Verilog基础语法——条件语句case、if-else 写在前面一、if-else语句二、case语句2.1 case语句2.2 casez语句2.3 casex语句 写在后面 写在前面 在Verilog语法中&#xff0c;常用的条件语句有if-else语句和case语句&#xff0c;用于判断条件是否为真&#xff0c;并执行判断条件后…

第 N 个泰波那契数

题目链接 第 N 个泰波那契数 题目描述 注意点 0 < n < 37答案保证是一个 32 位整数 解答思路 动态规划根据前三个数字推出新的泰波那契数 代码 class Solution {public int tribonacci(int n) {if (n 0) {return 0;}if (n 1 || n 2) {return 1;}int x 0;int x…

JSON格式化输出html——数组+对象+JSON字符串+汉字——基础积累——@pgrabovets/json-view

昨天写了一篇关于JSON格式化输出到页面上——数组对象JSON字符串汉字——基础积累的文章&#xff0c;效果是可以实现的 但是如果要实现右侧部分的展开/折叠&#xff0c;则可以使用到下面的插件了pgrabovets/json-view github链接&#xff1a;https://github.com/pgrabovets/j…

软考笔记随记

原码:(0正1负) 原码是最直观的编码方式,符号位用0表示正数,用1表示负数,其余位表示数值的大小。 例如,+7的原码为00000111,-7的原码为10000111。 原码虽然直观,但直接用于加减运算会导致计算复杂,且0有两种表示(+0和-0),不唯一。 反码: 反码是在原码的基础上得…

如何在VS Code中安装插件并进行中文化。

相关文章推荐: 如何下载和安装Visual Studio Code&#xff08;VSCode&#xff09; 在使用Visual Studio Code&#xff08;简称VS Code&#xff09;进行开发时&#xff0c;安装插件可以极大地提升开发效率和使用体验。而将VS Code插件界面进行中文化&#xff0c;则能更好地满足中…

实战使用Java代码操作Redis

实战使用Java代码操作Redis 1. 背景说明2. 单连接方式3. 连接池方式1. 背景说明 在工作中, 如果有一批数据需要初始化, 最方便的方法是使用代码操作Redis进行初始化。 Redis提供了多种语言的API交互方式, 这里以Java代码为例进行分析。    使用Java代码操作 Redis 需要借助…

积极向上的态度

非常欣赏您这种积极向上的态度&#xff01;以下是一些具体的建议&#xff0c;帮助您实现这些目标&#xff1a; 设定明确的目标&#xff1a; 将长期目标分解为短期、中期和长期的小目标。为每个小目标设定具体的完成时间和衡量标准。制定计划&#xff1a; 根据目标制定详细的工…

深度学习二分类任务之随机分配数据集

import os import random import shutildef random_sample_images(source_folders, output_folders, num_images_per_folder=4000):for source_folder, output_folder in zip(source_folders, output_folders):

十四、Redis Cluster集群

Redis Cluster是Redis提供的一个分布式解决方案&#xff0c;在3.0推出。Redis Cluster可以自动将数据分片分布到不同的master节点上&#xff0c;同时提供了高可用的支持&#xff0c;当某个master节点挂了之后&#xff0c;整个集群还是可以正常工作。1、为什么要用Redis Cluster…

智慧景区AR导览手绘地图小程序系统开发源码搭建

智慧景区AR导览手绘地图小程序系统开发源码搭建需要以下步骤&#xff1a; 1. 确定系统需求和功能&#xff1a;了解智慧景区AR导览手绘地图小程序系统的需求和功能&#xff0c;包括地图绘制、AR导览、用户交互、数据管理等。 2. 选择开发平台和工具&#xff1a;选择适合的编程…

KMP 算法JavaScript代码实现

LeetCode 28. 找出字符串中第一个匹配项的下标 给定一个 haystack 字符串和一个 needle 字符串&#xff0c;在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在&#xff0c;则返回 -1。 示例 1: 输入: haystack "hello", needle &quo…

浅说文心一言

文心一言&#xff08;ERNIE Bot&#xff09;是一个基于Transformer结构的知识增强大语言模型&#xff0c;它可以根据用户的指令和输入&#xff0c;生成相应的回答或文本。以下是一些常见的指令示例&#xff0c;你可以根据需要进行调整&#xff1a; 问答指令&#xff1a; "…

rocketmq的流程

生产过程 消费过程 存储 在RocketMQ中&#xff0c;一个Broker的所有Topic的消息都会被写入到同一个CommitLog文件中。 每个队列&#xff08;Queue&#xff09;都有对应的ConsumeQueue文件。 ConsumeQueue每个记录定长&#xff0c;20字节&#xff0c;消息在commitlog中的偏移量…

2024 年第四届长三角高校数学建模竞赛赛题浅析

一图流 赛道 题目难度 数据处理难度 模型难度 备注 A 高 低 高 需要物理模型和优化算法来预测物体在水中的行为和搜索策略&#xff0c;数据相对简单&#xff0c;主要挑战在于环境模拟和策略优化。 B 中等 高 中等 涉及大数据处理、特征工程、机器学习模型选择和调…

Linux quotacheck命令教程:如何检查和修复文件系统的磁盘配额(附案例详解和注意事项)

Linux quotacheck命令介绍 quotacheck命令是用于扫描文件系统以检查磁盘配额的一致性。它生成、检查和修复配额文件。这个命令通常在系统引导时运行&#xff0c;或者在手动更改了配额设置后运行。 Linux quotacheck命令适用的Linux版本 quotacheck命令在大多数Linux发行版中…