SpringBoot+Vue靜態(tài)資源刷新后無法訪問的問題解決方案
一、背景
原項目是有前后端分離設(shè)計,測試環(huán)境是centos系統(tǒng),采用nginx代理和轉(zhuǎn)發(fā),項目正常運行。
項目近期上線到正式環(huán)境,結(jié)果更換了系統(tǒng)環(huán)境,需要放到一臺windows系統(tǒng)中,前后端打成一個jar包,然后做成系統(tǒng)服務。這臺服務器中已經(jīng)有很多其他服務,都是采用一樣的部署方式,所以沒辦法只能對這個項目進行修改。
二、修改過程
2.1 首先看項目結(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。 // 默認情況下,vite 會假設(shè)你的應用是被部署在一個域名的根路徑上 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)文件存放目錄,默認的是在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 { /** * 自定義用戶認證邏輯 */ @Resource private UserDetailsService userDetailsService; /** * 認證失敗處理類 */ @Resource private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出處理類 */ @Resource private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * token認證過濾器 */ @Resource private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 跨域過濾器 */ @Resource private CorsFilter corsFilter; /** * 允許匿名訪問的地址 */ @Resource private PermitAllUrlProperties permitAllUrl; /** * 鑒權(quán) */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { // 注解標記允許匿名訪問的url ExpressionUrlAuthorizationConfigurer<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() // 靜態(tài)資源,可匿名訪問 .antMatchers(HttpMethod.GET, "/", "/*.html", "/*.html.gz", "/assets/**", "/favicon.ico", "/profile/**").permitAll() .antMatchers("/webjars/**", "/druid/**").permitAll() // 除上面外的所有請求全部需要鑒權(quá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); } }
認證失敗處理類AuthenticationEntryPointImpl
,解決退出成功后無法跳轉(zhuǎ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; /** * 默認為false,即不強制Https轉(zhuǎn)發(fā)或重定向 */ private boolean forceHttps = false; /** * 默認為false,即不是轉(zhuǎn)發(fā)到登陸頁面,而是進行重定向 */ 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ā)到登錄頁面之前將當前請求重定向到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(); } // 通過警告消息進入服務器端轉(zhuǎn)發(fā) log.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port " + serverPort); return null; } /** * 設(shè)置為true以強制通過https訪問登錄表單 * 如果此值為true(默認為false),并且觸發(fā)攔截器的請求還不是https * 則客戶端將首先重定向到https URL,即使serverSideRedirect(服務器端轉(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); // 解析對應的權(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; }
大概的修改步驟就是這樣,啟動后順利運行,跟前后端分離沒有什么區(qū)別。
以上就是SpringBoot+Vue靜態(tài)資源刷新后無法訪問的問題解決方案的詳細內(nèi)容,更多關(guān)于SpringBoot Vue靜態(tài)資源無法訪問的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot調(diào)用支付寶第三方接口(沙箱環(huán)境)
這篇文章主要介紹了springboot+調(diào)用支付寶第三方接口(沙箱環(huán)境),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10