SpringBoot+Vue靜態(tài)資源刷新后無法訪問的問題解決方案
一、背景
原項(xiàng)目是有前后端分離設(shè)計(jì),測試環(huán)境是centos系統(tǒng),采用nginx代理和轉(zhuǎn)發(fā),項(xiàng)目正常運(yùn)行。
項(xiàng)目近期上線到正式環(huán)境,結(jié)果更換了系統(tǒng)環(huán)境,需要放到一臺windows系統(tǒng)中,前后端打成一個jar包,然后做成系統(tǒng)服務(wù)。這臺服務(wù)器中已經(jīng)有很多其他服務(wù),都是采用一樣的部署方式,所以沒辦法只能對這個項(xiàng)目進(jìn)行修改。
二、修改過程
2.1 首先看項(xiàng)目結(jié)構(gòu)

admin是后端代碼,使用的是springboot,使用 spring security 權(quán)限控制;UI是前端,使用的是vue3+vite
admin的結(jié)構(gòu)

ui的結(jié)構(gòu)

2.2 打包靜態(tài)資源
修改前端打包配置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 } = env
return {
// 部署生產(chǎn)環(huán)境和開發(fā)環(huán)境下的URL。
// 默認(rèn)情況下,vite 會假設(shè)你的應(yīng)用是被部署在一個域名的根路徑上
base: VITE_APP_ENV === 'production' ? '/' : '/',
build: {
outDir: '../admin/src/main/resources/static'
},
plugins: createVitePlugins(env, command === 'build'),
resolve: {
// https://cn.vitejs.dev/config/#resolve-alias
alias: {
// 設(shè)置路徑
'~': path.resolve(__dirname, './'),
// 設(shè)置別名
'@': path.resolve(__dirname, './src')
},
// https://cn.vitejs.dev/config/#resolve-extensions
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
// vite 相關(guān)配置
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 file
css: {
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove();
}
}
}
}
]
}
}
}
})
增加下面代碼
build: {
outDir: '../admin/src/main/resources/static'
},
指定編譯后的靜態(tài)文件存放目錄,默認(rèn)的是在ui/dist目錄,然后執(zhí)行生產(chǎn)環(huán)境打包命令 npm run prod / npm run build,成功后會在admin/resource目錄下生成一個static文件夾


同時,把前端路徑與后端路徑?jīng)_突的修改一下。
2.3 修改后端權(quán)限控制
修改SecurityConfig,增加靜態(tài)資源的訪問權(quán)限
/**
* spring security配置
*
* @author admin
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定義用戶認(rèn)證邏輯
*/
@Resource
private UserDetailsService userDetailsService;
/**
* 認(rèn)證失敗處理類
*/
@Resource
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出處理類
*/
@Resource
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token認(rèn)證過濾器
*/
@Resource
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域過濾器
*/
@Resource
private CorsFilter corsFilter;
/**
* 允許匿名訪問的地址
*/
@Resource
private PermitAllUrlProperties permitAllUrl;
/**
* 鑒權(quán)
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 注解標(biāo)記允許匿名訪問的url
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
httpSecurity
// CSRF禁用,因?yàn)椴皇褂胹ession
.csrf().disable()
// 禁用HTTP響應(yīng)標(biāo)頭
.headers().cacheControl().disable().and()
// 認(rèn)證失敗處理類
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 過濾請求
.authorizeRequests()
// 對于登錄login 驗(yàn)證碼captchaImage 允許匿名訪問
.antMatchers("/login", "/sendSmsCode/*", "/captchaImage").permitAll()
// 靜態(tài)資源,可匿名訪問
.antMatchers(HttpMethod.GET, "/", "/*.html", "/*.html.gz", "/assets/**", "/favicon.ico", "/profile/**").permitAll()
.antMatchers("/webjars/**", "/druid/**").permitAll()
// 除上面外的所有請求全部需要鑒權(quán)認(rèn)證
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
}
靜態(tài)文件處理類ResourcesConfig
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
@Resource
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/** 本地文件上傳路徑,F(xiàn)ileConfig.getPath() 為本地磁盤目錄 */
registry.addResourceHandler("/profile/**")
.addResourceLocations("file:" + FileConfig.getPath() + "/");
/** 靜態(tài)資源 */
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
/**
* 自定義攔截規(guī)則
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
/**
* 跨域配置
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 設(shè)置訪問源地址
config.addAllowedOriginPattern("*");
// 設(shè)置訪問源請求頭
config.addAllowedHeader("*");
// 設(shè)置訪問源請求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路徑,攔截一切請求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的CorsFilter
return new CorsFilter(source);
}
}
認(rèn)證失敗處理類AuthenticationEntryPointImpl,解決退出成功后無法跳轉(zhuǎn)到登錄頁的問題
/**
* 認(rèn)證失敗處理類 返回未授權(quán)
*
* @author admin
*/
@Slf4j
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
/**
* 向調(diào)用者提供有關(guān)哪些HTTP端口與系統(tǒng)上的哪些HTTPS端口相關(guān)聯(lián)的信息
*/
private PortMapper portMapper = new PortMapperImpl();
/**
* 端口解析器,基于請求解析出端口
*/
private PortResolver portResolver = new PortResolverImpl();
/**
* 登陸頁面URL
*/
private String loginFormUrl;
/**
* 默認(rèn)為false,即不強(qiáng)制Https轉(zhuǎn)發(fā)或重定向
*/
private boolean forceHttps = false;
/**
* 默認(rèn)為false,即不是轉(zhuǎn)發(fā)到登陸頁面,而是進(jìn)行重定向
*/
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();
}
@Override
public 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);
}
/**
* 構(gòu)建重定向URL
*
* @param request
* @param response
* @param authException
* @return
*/
protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
// 通過determineUrlToUseForThisRequest方法獲取URL
String loginForm = determineUrlToUseForThisRequest(request, response, authException);
// 如果是絕對URL,直接返回
if (UrlUtils.isAbsoluteUrl(loginForm)) {
return loginForm;
}
// 如果是相對URL
// 構(gòu)造重定向URL
int 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和port
urlBuilder.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();
}
/**
* 構(gòu)建一個URL以將提供的請求重定向到HTTPS
* 用于在轉(zhuǎn)發(fā)到登錄頁面之前將當(dāng)前請求重定向到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();
}
// 通過警告消息進(jìn)入服務(wù)器端轉(zhuǎn)發(fā)
log.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port " + serverPort);
return null;
}
/**
* 設(shè)置為true以強(qiáng)制通過https訪問登錄表單
* 如果此值為true(默認(rèn)為false),并且觸發(fā)攔截器的請求還不是https
* 則客戶端將首先重定向到https URL,即使serverSideRedirect(服務(wù)器端轉(zhuǎn)發(fā))設(shè)置為true
*/
public void setForceHttps(boolean forceHttps) {
this.forceHttps = forceHttps;
}
/**
* 是否要使用RequestDispatcher轉(zhuǎn)發(fā)到loginFormUrl,而不是302重定向
*/
public void setUseForward(boolean useForward) {
this.useForward = useForward;
}
}
增加一個刷新跳轉(zhuǎn)處理類 ServletConfig,很關(guān)鍵。
@Configuration
public class ServletConfig {
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
return factory -> {
// 404時跳轉(zhuǎn)到首頁
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,并且獲取token
Cookie 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);
// 解析對應(yīng)的權(quán)限以及用戶信息,Constants.LOGIN_USER_KEY為自定義redis key
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
return redisCache.getCacheObject(userKey);
} catch (Exception e) {
}
}
return null;
}
大概的修改步驟就是這樣,啟動后順利運(yùn)行,跟前后端分離沒有什么區(qū)別。
以上就是SpringBoot+Vue靜態(tài)資源刷新后無法訪問的問題解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Vue靜態(tài)資源無法訪問的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringMvc MultipartFile實(shí)現(xiàn)圖片文件上傳示例
本篇文章主要介紹了SpringMvc MultipartFile實(shí)現(xiàn)圖片文件上傳示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-02-02
jdk17+springboot使用webservice的踩坑實(shí)戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于jdk17+springboot使用webservice踩坑的相關(guān)資料,網(wǎng)上很多教程是基于jdk8的,所以很多在17上面跑不起來,折騰兩天,直接給答案,需要的朋友可以參考下2024-01-01
SpringBoot集成ClickHouse數(shù)據(jù)庫操作過程舉例
本文講解如何在SpringBoot中集成ClickHouse列式數(shù)據(jù)庫,涵蓋依賴配置、實(shí)體映射、CRUD操作及復(fù)雜查詢實(shí)現(xiàn),結(jié)合實(shí)踐項(xiàng)目提升數(shù)據(jù)處理效率,本教程還將說明ClickHouse的數(shù)據(jù)類型、并行處理能力、聚合函數(shù)等特性,并提供一個實(shí)踐項(xiàng)目以加深理解,需要的朋友一起看看吧2025-08-08
SpringCloud之@FeignClient()注解的使用詳解
@FeignClient是SpringCloud中用于聲明一個Feign客戶端的注解,用于解決模塊方法互相調(diào)用的問題,Feign是一個聲明式的WebService客戶端,通過Feign,只需要創(chuàng)建一個接口,并使用注解來描述請求,就可以直接執(zhí)行HTTP請求了2024-11-11
使用jenkins+maven+git發(fā)布jar包過程詳解
這篇文章主要介紹了使用jenkins+maven+git發(fā)布jar包過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-07-07
Mybatis-Plus將字段設(shè)置為null解決方法
MyBatis-Plus是一個MyBatis的增強(qiáng)工具,在MyBatis的基礎(chǔ)上只做增 強(qiáng)不做改變,為簡化開發(fā)、提高效率而生,下面這篇文章主要給大家介紹了關(guān)于Mybatis-Plus將字段設(shè)置為null的解決方法的相關(guān)資料,需要的朋友可以參考下2023-04-04
Java實(shí)現(xiàn)“年-月-日 上午/下午時:分:秒”的簡單代碼
當(dāng)前的日期輸出的方法有很多,本文為大家介紹下在java中是如何實(shí)現(xiàn)“年-月-日 上午/下午時:分:秒”,感興趣的朋友不妨參考下2015-08-08

