一、背景
原项目是有前后端分离设计,测试环境是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;
}
大概的修改步骤就是这样,启动后顺利运行,跟前后端分离没有什么区别。