解决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 } = env
  return {
    // 部署生产环境和开发环境下的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-alias
      alias: {
        // 设置路径
        '~': path.resolve(__dirname, './'),
        // 设置别名
        '@': path.resolve(__dirname, './src')
      },
      // https://cn.vitejs.dev/config/#resolve-extensions
      extensions: ['.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 file
    css: {
      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 {
    /**
     * 自定义用户认证逻辑
     */
    @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;
    
    /**
     * 鉴权
     */
    @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()
                // 静态资源,可匿名访问
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/*.html.gz", "/assets/**", "/favicon.ico", "/profile/**").permitAll()
                .antMatchers("/webjars/**", "/druid/**").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .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);
    }
}

静态文件处理类ResourcesConfig

@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
    @Resource
    private RepeatSubmitInterceptor repeatSubmitInterceptor;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        /** 本地文件上传路径,FileConfig.getPath() 为本地磁盘目录 */
        registry.addResourceHandler("/profile/**")
                .addResourceLocations("file:" + FileConfig.getPath() + "/");
        /** 静态资源 */
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }

    /**
     * 自定义拦截规则
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
    }

    /**
     * 跨域配置
     */
    @Bean
    public 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);
        // 返回新的CorsFilter
        return 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();
    }

    @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);
    }

    /**
     * 构建重定向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
        // 构造重定向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();
    }

    /**
     * 构建一个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 {
    @Bean
    public 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,并且获取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);
            // 解析对应的权限以及用户信息,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;
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/626164.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用vue3+ts+vite从零开始搭建bolg(五):layout(持续更新中)

五、layout搭建 5.1静态搭建 在src下创建如图文件夹 这里用logo举例&#xff0c;在scripts里export <script lang"ts">export default {name: Logo,}</script> 然后在layout里引入 //引入左侧菜单顶部用户信息 import Logo from ./logo/index.vue 接…

跨境必看|TikTok账号运营的八大秘籍

国内的传统生意都是可以在抖音上做&#xff0c;那么也可以在TikTok 上重新做一遍。那该如何才能把握住这片巨大的蓝海&#xff0c;TikTok 账号的运营就成为了主要的关键了&#xff0c;对于TikTok账号运营的八大秘籍&#xff0c;大家一起看看是如何做的&#xff1f; 一、固定节…

算法_前缀和

DP34 【模板】前缀和 import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别int n in.nextInt(),q in.ne…

NSSCTF Web方向的例题和相关知识点(二)

[SWPUCTF 2021 新生赛]Do_you_know_http 解题&#xff1a; 点击打开环境&#xff0c;是 提示说请使用wLLm浏览器访问 我们可以更改浏览器信息&#xff0c;在burp重放器中发包后发现是302重定向&#xff0c;但是提示说success成功&#xff0c;说明 我们修改是成功的&#xff…

如何写好设计文档

一、明确目的 在编写设计文档之前&#xff0c;首先要明确为什么需要写这份文档。设计文档是软件开发过程中的重要沟通工具&#xff0c;它有助于确保团队成员对项目有共同的理解&#xff0c;促进协作&#xff0c;便于变更管理&#xff0c;并提供历史记录。 二、编写方法 为目…

动手学深度学习17 使用和购买gpu

动手学深度学习16 Pytorch神经网络基础&#xff09; 5. GPUcolabNVIDIA GPUQA显存 5. GPU 课件&#xff1a; https://zh-v2.d2l.ai/chapter_deep-learning-computation/use-gpu.html 有GPU装cuda。 把模型参数放到指定设备上。 # 5.6. GPU # !nvidia-smi # 在命令行中&…

VictoriaMetrics

概念 介绍 VictoriaMetrics&#xff0c;是一个快速高效、经济并且可扩展的监控解决方案和时序数据库 本文均用VM简称VictoriaMetric 作用 用于作为prometheus的长期储存方案&#xff0c;代替prometheus存储监控采集的数据 优点 远程存储&#xff1a;可作为单一或多个Pro…

matlab使用1-基础

matlab使用1-基础 文章目录 matlab使用1-基础1. 界面介绍2. matlab变量3. matlab数据类型4. matlab矩阵操作5. matlab程序结构5.1 顺序结构5.2 循环结构5.3 分支结构 1. 界面介绍 命令行窗口输入&#xff1a;clc 可清除命令行窗口command window的内容 clc命令行窗口输入&…

C++ 多态性

一 多态性的分类 编译时的多态 函数重载 运算符重载 运行时的多态 虚函数 1 运算符重载的引入 使用C编写程序时&#xff0c;我们不仅要使用基本数据类型&#xff0c;还要设计新的数据类型-------类类型。 一般情况下&#xff0c;基本数据类型的运算都是运算符来表达&#x…

10G UDP协议栈 IP层设计-(6)IP TX模块

一、模块功能 1、上层数据封装IP报文头部 2、计算首部校验和 二、首部校验和计算方法 在发送方&#xff0c;先把IP数据报首部划分为许多16位字的序列&#xff0c;并把检验和字段置零。用反码算术运算把所有16位字相加后&#xff0c;将得到的和的反码写入检验和字段。接收方收…

Docker安装Redis,并在 Visual Studio Code 中使用它

Docker安装Redis 查找Redis docker search Redis完整结果 PS C:\Users\cheng> docker search Redis NAME DESCRIPTION STARS OFFICIAL redis Redis is an open …

【强化学习-Mode-Free DRL】深度强化学习如何选择合适的算法?DQN、DDPG、A3C等经典算法Mode-Free DRL算法的四个核心改进方向

【强化学习-DRL】深度强化学习如何选择合适的算法&#xff1f; 引言&#xff1a;本文第一节先对DRL的脉络进行简要介绍&#xff0c;引出Mode-Free DRL。第二节对Mode-Free DRL的两种分类进行简要介绍&#xff0c;并对三种经典的DQL算法给出其交叉分类情况&#xff1b;第三节对…

Excel如何设置密码保护【图文详情】

文章目录 前言一、Excel如何设置密码保护&#xff1f;二、Excel如何取消密码保护&#xff1f;总结 前言 在软件项目开发过程中&#xff0c;会输出很多技术文档&#xff0c;其中也包括保密级别很高的服务器账号Excel文档。为了确保服务器账号相关的Excel文档的安全性&#xff0…

超级简单的地图操作工具开发可疑应急,地图画点,画线,画区域,获取地图经纬度等

使用echars的地图画点,画线,画区域,获取地图经纬度等 解压密码:10086007 地图也是用临时的bmap.js和china.js纯离线二选一 一共就这么多文件 画点,画线,画区域 点击地图获取经纬度-打印到控制台,这样就能渲染航迹,多变形,结合其他算法算圆等等操作 下载资源:https://download…

C# OpenCvSharp DNN 黑白老照片上色

C# OpenCvSharp DNN 黑白老照片上色 目录 效果 项目 代码 下载 参考 效果 项目 代码 using OpenCvSharp; using OpenCvSharp.Extensions; using System; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropS…

CVPR2022人脸识别Partial FC论文及代码学习笔记

论文链接&#xff1a;https://openaccess.thecvf.com/content/CVPR2022/papers/An_Killing_Two_Birds_With_One_Stone_Efficient_and_Robust_Training_CVPR_2022_paper.pdf 代码链接&#xff1a;insightface/recognition/arcface_torch at master deepinsight/insightface G…

leetcode——链表的中间节点

876. 链表的中间结点 - 力扣&#xff08;LeetCode&#xff09; 链表的中间节点是一个简单的链表OJ。我们要返回中间节点有两种情况&#xff1a;节点数为奇数和节点数是偶数。如果是奇数则直接返回中间节点&#xff0c;如果是偶数则返回第二个中间节点。 这道题的解题思路是&a…

【JS面试题】this

this取什么值&#xff0c;是在函数执行的时候确定的&#xff0c;不是在函数定义的时候确定的&#xff01; this的6种使用场景&#xff1a; ① 在普通函数中使用&#xff1a;返回window对象 ② 使用call apply bind 调用&#xff1a;绑定的是哪个对象就返回哪个对象 ③ 在对象…

LeetCode2390从字符串中移除星号

题目描述 给你一个包含若干星号 * 的字符串 s 。在一步操作中&#xff0c;你可以&#xff1a;选中 s 中的一个星号。移除星号 左侧 最近的那个 非星号 字符&#xff0c;并移除该星号自身。返回移除 所有 星号之后的字符串。注意&#xff1a;生成的输入保证总是可以执行题面中描…

电子邮箱是什么?怎么申请一个电子邮箱?

电子邮箱是我们沟通的工具&#xff0c;细分为免费版电子邮箱和付费版电子邮箱。怎么申请一个属于自己的电子邮箱&#xff1f;今天小编就分享一下电子邮箱注册教程&#xff0c;手把手教您注册一个电子邮箱。 一、电子邮箱的定义 电子邮箱&#xff0c;简称邮箱&#xff0c;是一…