Springboot为方法创建单独的线程并后台执行
一般在调用方法操作时，访问后方法会在前台执行，执行结束后才会返回结果，但如果是在web的情况下，如果需要执行的方法耗时较长，必然会影响前台的使用体验，我们可以通过为这个方法创建一个后台线程，让这个线程自己单独执行，执行结束后写入结果，前台需要查询结果时刷新即可，不会影响前台操作。Springboot 内置了 @Async 注解，用于为方法创建一个线程，让这个方法异步执行。要使用这个注解，我们需要在启动类上添加@EnableAsync注解，才可以继续使用。在需要进行异步执行的方法添加@Async注解即可。不过我在普通方法使用@Async注解时并没有正确的执行，在具有@Service注解的类中的方法使用此注解，也就是业务层，可以被正确执行。
运行jar包直接通过java-jar的方式可直接在终端前台运行jar包，也会直接动态显示日志，这个方式可以测试jar包能不能正常的运行。java -jar hgnuman-1.3.0-RELEASE.jar 如果测试莫得问题，那么就可以ctrl+c退出程序，就让这个jar包后台运行了sudo -u www nohup java -jar hgnuman-1.3.0-RELEASE.jar >hgnuman.log 2>&1 &这个命令中，我切换到了www用户来执行，nohup让程序在后台执行，后面的log就是让这个程序的日志输出到当前目录下，当然名称可以随意更改。运行这个命令后会返回程序的PID，等待一会程序就正常运行了。停止jar包运行如果是前台的话，直接ctrl+c即可停止，如果是在后台运行，那就可通过下方的命令来找到当前正在运行jar包的pid，然后通过kill命令干掉这个程序。ps aux|grep hgnuman-1.3.0-RELEASE.jar这里可以看到PID是30268，我们kill掉他kill 30268执行之后如果没有任何输出，那就说明已经kill掉了，因为没有结果就是最好的结果。
Cookie生成与处理首先写一个cookie加密的方法，目前的思路是根据用户名和密码生成加密后的cookie token,加密规则 Base64(username) + "|" + md5(username+password) 生成一段结果字符串public static String encryUserCookieMd5(String username,String password){ String str = username+password; String md5_username_password = DigestUtils.md5DigestAsHex(str.getBytes()); String base64_username = Base64Utils.encodeToString(username.getBytes(StandardCharsets.UTF_8)); return base64_username + "|" + md5_username_password; }然后写一个解密的方法，读取request中的cookie,传入的cokie字符串的值 通过 “|”将cookie字符串分割为两个部分 将第一部分加密的username进行解密，并返回这个username值public static String decryUserCookieMd5(String cookie){ String[] cookies = cookie.split("\\|"); byte[] username = Base64Utils.decode(cookies[0].getBytes(StandardCharsets.UTF_8)); return new String(username); }登录请求拦截注册一个登录拦截器 LoginIntercepter ，对登录请求进行拦截处理，首先判断session中是否以及存在登录了此用户，如果登录则直接返回true，代表登录成功，如果没有的话就获取cookie，根据上方的cookie编码解码规则，得到cookie中的用户名，然后根据用户名从数据库中查找用户，根据查找的这个用户的用户名和密码再次进行编码加密，与cookie原始值进行比对，如果完全想等则认为此用户可以使用cookie登录成功。public class LoginIntercepter implements HandlerInterceptor { @Autowired UserRepository userRepository; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 登录检查逻辑 HttpSession session = request.getSession(); Object loginuser = session.getAttribute("LoginUser"); if (loginuser != null) { return true; } else { // 获得cookie Cookie[] cookies = request.getCookies(); Integer hase_cookie_user = 1; // 没有cookie信息，则重定向到登录界面 if (null != cookies) { // 获取cookie里面的一些用户信息 for (Cookie item : cookies) { // 根据获取的cookie值,将值进行解密获取username // 然后从数据库中获取用户名和密码并进行加密操作 // 如果加密后的字符串和当前传入的字符串相同，则代表此用户可通过cookie登录 if ("cookie_username".equals(item.getName())) { String cookie_username = item.getValue(); String uname = SecMd5.decryUserCookieMd5(cookie_username); log.info("uname:" + uname); User user = userRepository.findByUsernameEquals(uname); if (user != null) { String user_name = user.getUsername(); String user_password = user.getPassword(); if (SecMd5.encryUserCookieMd5(user_name, user_password).equals(cookie_username)) { session.setAttribute("LoginUser", user); log.info("cookie 登录"); return true; } break; } } } } } session.setAttribute("loginmsg", "未登录"); response.sendRedirect("/"); return false; } // ....其他方法省略 }没有cookie的时候，对登录后的账号进行编码加密，生成一段cookie值session.setAttribute("LoginUser", user); // 保存cookie，实现自动登录 Cookie cookie_username = new Cookie("cookie_username", SecMd5.encryUserCookieMd5(uname,upwd)); // 设置cookie的持久化时间，1天 cookie_username.setMaxAge( 24 * 60 * 60); // 设置为当前项目下都携带这个cookie cookie_username.setPath(request.getContextPath()); // 向客户端发送cookie response.addCookie(cookie_username);@Autowired 自动注入失败，结果为null注入失败的原因是因为HandlerInterceptor类在Spring框架中被new过，new 过的对象不会交给Spring容器管理 所以里面的 service或者dao会注入失败。解决方式在WebMvcConfigurer 配置中使用@Bean注解，将登录拦截器注入进去@Configuration public class AdminWebConfig implements WebMvcConfigurer { @Bean public LoginIntercepter setLoginIntercepterBean(){ log.info("注入拦截器"); return new LoginIntercepter(); } @Override public void addInterceptors(InterceptorRegistry registry) { // 全局过滤器 判断是否登录 registry.addInterceptor(setLoginIntercepterBean()) .addPathPatterns("/**") .excludePathPatterns( "/", "/login" // 白名单路径 ); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 映射资源url与本地资源路径 // 路径映射 } }参考文章SpringBoot之Filter/HandlerInterceptor 中注入service为null解决方案_临窗，听雨声的博客-CSDN博客springboot @Autowired 注入为null 的原因与解决方式_Jack2013tong的博客-CSDN博客_autowired注入为null
使用Base64UtilsSpring框架提供了可以进行base64编码解码的工具包，我们可以直接导入使用import org.springframework.util.Base64Utils;base64编码并返回字符串public static String encryStringBase64(String str){ return Base64Utils.encodeToString(str.getBytes(StandardCharsets.UTF_8)); }base64解码并生成字符串public static String decryStringBase64(String base64_str){ byte[] str = Base64Utils.decode(base64_str.getBytes(StandardCharsets.UTF_8)); return new String(str); }这里解码后会返回一个byte类型的数组，需要将它转换成字符串，需要使用new来创建字符串，而不可用直接使用toString()方法，如果使用了toString方法则会返回数组的内存地址字符串Base64Utils源码解析通过查看Base64Utils的源码可以发现Base64Utils也是通过java自带的Base64工具包来实现的方法作用encode(byte[] src)对给定的字节数组进行base64编码。 参数原始字节数组 返回编码后的字节数组decode(byte[] src)对给定的字节数组进行base64解码。 参数编码的字节数组 返回原始字节数组encodeUrlSafe(byte[] src)使用RFC 4648“URL和文件名安全字母表”对给定的字节数组进行base64编码。 参数: SRC -原始字节数组 返回: 编码后的字节数组decodeUrlSafe(byte[] src)base64解码给定的字节数组使用RFC 4648“URL和文件名安全字母表”。 参数: SRC -编码的字节数组 返回: 原始字节数组encodeToString(byte[] src)将给定的字节数组进行base64编码为String。 参数: SRC -原始字节数组 返回: 编码后的字节数组作为UTF-8字符串decodeFromString(String src)从给定的UTF-8字符串字节数组解码。 参数: src -编码的UTF-8字符串 返回: 原始字节数组encodeToUrlSafeString(byte[] src)使用RFC 4648“URL和文件名安全字母表”将给定的字节数组编码为字符串。 参数: SRC -原始字节数组 返回: 编码后的字节数组作为UTF-8字符串decodeFromUrlSafeString(String src)使用RFC 4648“URL和文件名安全字母表”从UTF-8字符串解码给定的字节数组。 参数: src -编码的UTF-8字符串 返回: 原始字节数组源代码/* * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.util; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Base64; /** * A simple utility class for Base64 encoding and decoding. * * Adapts to Java 8's {@link java.util.Base64} in a convenience fashion. * * @author Juergen Hoeller * @author Gary Russell * @since 4.1 * @see java.util.Base64 */ public abstract class Base64Utils { private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; /** * Base64-encode the given byte array. * @param src the original byte array * @return the encoded byte array */ public static byte[] encode(byte[] src) { if (src.length == 0) { return src; } return Base64.getEncoder().encode(src); } /** * Base64-decode the given byte array. * @param src the encoded byte array * @return the original byte array */ public static byte[] decode(byte[] src) { if (src.length == 0) { return src; } return Base64.getDecoder().decode(src); } /** * Base64-encode the given byte array using the RFC 4648 * "URL and Filename Safe Alphabet". * @param src the original byte array * @return the encoded byte array * @since 4.2.4 */ public static byte[] encodeUrlSafe(byte[] src) { if (src.length == 0) { return src; } return Base64.getUrlEncoder().encode(src); } /** * Base64-decode the given byte array using the RFC 4648 * "URL and Filename Safe Alphabet". * @param src the encoded byte array * @return the original byte array * @since 4.2.4 */ public static byte[] decodeUrlSafe(byte[] src) { if (src.length == 0) { return src; } return Base64.getUrlDecoder().decode(src); } /** * Base64-encode the given byte array to a String. * @param src the original byte array * @return the encoded byte array as a UTF-8 String */ public static String encodeToString(byte[] src) { if (src.length == 0) { return ""; } return new String(encode(src), DEFAULT_CHARSET); } /** * Base64-decode the given byte array from an UTF-8 String. * @param src the encoded UTF-8 String * @return the original byte array */ public static byte[] decodeFromString(String src) { if (src.isEmpty()) { return new byte[0]; } return decode(src.getBytes(DEFAULT_CHARSET)); } /** * Base64-encode the given byte array to a String using the RFC 4648 * "URL and Filename Safe Alphabet". * @param src the original byte array * @return the encoded byte array as a UTF-8 String */ public static String encodeToUrlSafeString(byte[] src) { return new String(encodeUrlSafe(src), DEFAULT_CHARSET); } /** * Base64-decode the given byte array from an UTF-8 String using the RFC 4648 * "URL and Filename Safe Alphabet". * @param src the encoded UTF-8 String * @return the original byte array */ public static byte[] decodeFromUrlSafeString(String src) { return decodeUrlSafe(src.getBytes(DEFAULT_CHARSET)); } }
本文转载来源：SpringBoot解决CORS问题在做前后端分离的开发或者前端调用第三方平台的接口时经常会遇到跨域的问题，前端总是希望能够通过各种方法解决跨域的问题。但事实上跨域问题是安全问题。这篇文章将会讲解一些为什么会有跨域问题，并提供一个方便的解决方法。为了阅读的流畅，相关的参考链接均会在文章末尾给出。本文使用的springboot版本为2.1.6.RELEASE，相应的spring版本为5.1.8.RELEASE。跨域问题的产生跨域问题的产生是因为浏览器的同源策略。同源策略将协议+域名+端口构成的三元作为一个整体，只有三者均相同的情况下才属于一个源。跨域问题也就是不同源之间访问导致的问题。同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。浏览器的同源策略 @developer.mozilla.org下表给出了相对http://store.company.com/dir/page.html同源检测的示例:URL结果原因http://store.company.com/dir2/other.html成功只有路径不同http://store.company.com/dir/inner/another.html成功只有路径不同https://store.company.com/secure.html失败不同协议 ( https和http )http://store.company.com:81/dir/etc.html失败不同端口 ( http:// 80是默认的)http://news.company.com/dir/other.html失败不同域名 ( news和store )对于跨域的请求，服务器可以接受到请求，但浏览器不会出来请求的返回结果。在浏览器中打开本地的一个html文件，在客户端中输入一下的内容可以模拟跨域的请求。（为了防止因为https的限制而无法发送请求，可以自己启动一个前端服务，然后再进行试验。）var xhttp = new XMLHttpRequest(); xhttp.open("GET", "http://192.168.20.185:8080/users/12345678", true); xhttp.send();跨域资源共享 CORSCORS是一个W3C标准，全称是"跨域资源共享"（Cross-origin resource sharing）。它允许浏览器向跨源服务器，发出XMLHttpRequest请求，从而克服了AJAX只能同源使用的限制。-- 前端的辅助配置Header值示例描述Access-Control-Allow-Credentialstrue是否允许发送Cookie，默认falseAccess-Control-Allow-HeadersAuthorization,Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers后端接受的请求头。除了Accept，Accept-Language，Content-Language，Last-Event-ID和Content-Type外的附加请求头Access-Control-Allow-MethodsGET,POST,HEAD,OPTIONS,PUT,DELETE,PATCH后端接受的请求方法。除了HEAD，GET，POST外的请求方法Access-Control-Allow-Originhttp://localhost:4000请求的来源，一般为当前页面所在的源。要么为准确值，要么为*.Access-Control-Expose-HeadersAccess-Control-Allow-Origin,Access-Control-Allow-Credentials暴露给客户端的请求头。除了Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma之外的附加响应头Access-Control-Max-Age86400预检请求有效时长，单位为秒当Access-Control-Allow-Credentials为true时，不可以设置Access-Control-Allow-Origin为*减少预检请求（Option） 通过延长预检请求的有效期，可以减少对同一个源的Option请求的数量。如设置为86400，则24小时内无需在对同一个源发送Option请求。Spring Web解决方法通过过滤器处理请求，对origin进行判断，并添加必要的Headers。@CrossOrigin范围：单个类 单个Path@RestController @RequestMapping("/account") public class AccountController { @CrossOrigin @GetMapping("/{id}") public Account retrieve(@PathVariable Long id) { // ... } }可类级别配置，也可方法级别配置。默认：所有origins所有headers所有http方法@CrossOrigin 支持各个值的配置。@CrossOrigin 虽然提供了简单的配置，但需要重复为不同的类和方法进行配置，重复麻烦。如果对于某些请求有特定的配置需要可以使用。注解@CrossOrigin会成为CorsConfiguration的一部分，可与WebMvcConfigurer#addCorsMappings(CorsRegistry)一起使用，为并列关系。WebMvcConfigurer#addCorsMappings(CorsRegistry)范围：全局 单个Path@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://domain1.com, https://domain2.com") .allowedMethods("PUT", "DELETE") .allowedHeaders("header1", "header2", "header3") .exposedHeaders("header1", "header2") .allowCredentials(true).maxAge(3600); // Add more mappings... } }效果相同的XML配置：<mvc:cors> <mvc:mapping path="/api/**" allowed-origins="https://domain1.com, https://domain2.com" allowed-methods="PUT,DELETE" allowed-headers="header1, header2, header3" exposed-headers="header1, header2" allow-credentials="true" max-age="3600" /> </mvc:cors>真的很喜欢用JavaConfig进行配置，灵活方便。在WebConfig.java的实现中，可以设置一个CorsPropertiesList.java类来做将配置移到.properties配置文件中，可以得到如下的实现：@EnableConfigurationProperties @Configuration public class CorsConfig { /** * 可与 @CrossOrigin 联用 */ @Configuration @EnableWebMvc @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "webMvc") public class WebConfig implements WebMvcConfigurer { @Autowired private CorsPropertiesList corsPropertiesList; @Override public void addCorsMappings(CorsRegistry registry) { System.out.println("config cors with " + corsPropertiesList.toString()); for(CorsProperties corsProperties: corsPropertiesList.getList()) { addCorsMappings(registry, corsProperties); } } private void addCorsMappings(CorsRegistry registry, CorsProperties corsProperties) { for(String pathPattern: corsProperties.getPathPatterns()) { CorsRegistration registration = registry.addMapping(pathPattern); registration.allowedOrigins(corsProperties.getAllowedOrigins()); registration.allowedMethods(corsProperties.getAllowedMethods()); registration.allowedHeaders(corsProperties.getAllowedHeaders()); registration.allowCredentials(corsProperties.getAllowedCredentials()); registration.exposedHeaders(corsProperties.getExposedHeaders()); registration.maxAge(corsProperties.getMaxAge()); } } ... } } @Data @NoArgsConstructor @Component @ConfigurationProperties("corses") public class CorsPropertiesList { private List<CorsProperties> list; } @Data public class CorsProperties { // Ant-style path patterns private String[] pathPatterns; private String[] allowedOrigins; private String[] allowedMethods; private String[] allowedHeaders; private Boolean allowedCredentials; private String[] exposedHeaders; private Long maxAge; public void setPathPatterns(String[] pathPatterns) { this.pathPatterns = pathPatterns; } public void setPathPatterns(String pathPatterns) { this.pathPatterns = StringUtils.split(pathPatterns, ","); } public void setAllowedOrigins(String[] allowedOrigins) { this.allowedOrigins = allowedOrigins; } public void setAllowedOrigins(String allowedOrigins) { this.allowedOrigins = StringUtils.split(allowedOrigins, ","); } public void setAllowedMethods(String[] allowedMethods) { this.allowedMethods = allowedMethods; } public void setAllowedMethods(String allowedMethods) { this.allowedMethods = StringUtils.split(allowedMethods, ","); } public void setAllowedHeaders(String[] allowedHeaders) { this.allowedHeaders = allowedHeaders; } public void setAllowedHeaders(String allowedHeaders) { this.allowedHeaders = StringUtils.split(allowedHeaders, ","); } public void setExposedHeaders(String[] exposedHeaders) { this.exposedHeaders = exposedHeaders; } public void setExposedHeaders(String exposedHeaders) { this.exposedHeaders = StringUtils.split(exposedHeaders, ","); } }application.yml# web.config.cors: sourceConfig # web.config.cors: customFilter # web.config.cors: corsFilterRegistration # web.config.cors: corsFilter web.config.cors: webMvc corses.list: - path-patterns: - /** allowed-origins: - http://localhost:* allowed-methods: GET,POST,HEAD,OPTIONS,PUT,DELETE,PATCH allowed-headers: - Authorization - Content-Type - X-Requested-With - accept,Origin - Access-Control-Request-Method - Access-Control-Request-Headers allowed-credentials: true exposed-headers: Access-Control-Allow-Origin,Access-Control-Allow-Credentials max-age: 86400通过该方法的配置，可以实现全局的跨域设置，但无法修改到对origin的判断规则，比如无法实现实现对一个域名的子域名或者一个ip的任意端口的检验。使用Spring的CrosFilter@Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // Possibly... // config.applyPermitDefaultValues() config.setAllowCredentials(true); config.addAllowedOrigin("https://domain1.com"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); CorsFilter filter = new CorsFilter(source); }到这里已经可以完成全局CORS的配置了。为了能够使用AntPathMatcher匹配origin，可以重写CorsConfiguration#checkOrigin(String)方法。package io.gitlab.donespeak.tutorial.cors.config.support; import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; import org.springframework.util.ObjectUtils; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.cors.CorsConfiguration; /** * @date 2019/12/03 00:04 */ public class AntPathMatcherCorsConfiguration extends CorsConfiguration { private PathMatcher pathMatcher = new AntPathMatcher(); @Nullable @Override public String checkOrigin(@Nullable String requestOrigin) { System.out.println(requestOrigin); if (!StringUtils.hasText(requestOrigin)) { return null; } if (ObjectUtils.isEmpty(this.getAllowedOrigins())) { return null; } if (this.getAllowedOrigins().contains(ALL)) { if (!Boolean.TRUE.equals(this.getAllowCredentials())) { // ALL 和 TRUE不是不能同时出现吗？ return ALL; } else { return requestOrigin; } } String lowcaseRequestOrigin = requestOrigin.toLowerCase(); for (String allowedOrigin : this.getAllowedOrigins()) { System.out.println(allowedOrigin + ": " + pathMatcher.match(allowedOrigin.toLowerCase(), lowcaseRequestOrigin)); if (pathMatcher.match(allowedOrigin.toLowerCase(), lowcaseRequestOrigin)) { return requestOrigin; } } return null; } }相应的可配置CorsFilter如下：@EnableConfigurationProperties @Configuration public class CorsConfig { /** * 不可与 @CrossOrigin 联用 */ @Configuration @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "corsFilterRegistration") public static class CorsFilterRegistrationConfig { @Bean public FilterRegistrationBean corsFilterRegistration(CorsPropertiesList corsPropertiesList) { System.out.println("create bean FilterRegistrationBean with " + corsPropertiesList); FilterRegistrationBean bean = new FilterRegistrationBean(createCorsFilter(corsPropertiesList)); bean.setOrder(0); return bean; } } /** * 不可与 @CrossOrigin 联用 */ @Configuration @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "corsFilter") public static class CorsFilterConfig { @Bean(name = "corsFilter") public CorsFilter corsFilter(CorsPropertiesList corsPropertiesList) { System.out.println("init bean CorsFilter with " + corsPropertiesList); return createCorsFilter(corsPropertiesList); } } private static CorsFilter createCorsFilter(CorsPropertiesList corsPropertiesList) { return new CorsFilter(createCorsConfigurationSource(corsPropertiesList)); } private static CorsConfigurationSource createCorsConfigurationSource(CorsPropertiesList corsPropertiesList) { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); for(CorsProperties corsProperties: corsPropertiesList.getList()) { // 路径也是 AntPathMarcher for(String pathPattern: corsProperties.getPathPatterns()) { source.registerCorsConfiguration(pathPattern, toCorsConfiguration(corsProperties)); } } return source; } private static CorsConfiguration toCorsConfiguration(CorsProperties corsProperties) { CorsConfiguration corsConfig = new AntPathMatcherCorsConfiguration(); corsConfig.setAllowedOrigins(Arrays.asList(corsProperties.getAllowedOrigins())); corsConfig.setAllowedMethods(Arrays.asList(corsProperties.getAllowedMethods())); corsConfig.setAllowedHeaders(Arrays.asList(corsProperties.getAllowedHeaders())); corsConfig.setAllowCredentials(corsProperties.getAllowedCredentials()); corsConfig.setMaxAge(corsProperties.getMaxAge()); corsConfig.setExposedHeaders(Arrays.asList(corsProperties.getExposedHeaders())); return corsConfig; } ... }通过@Configuration注解的配置类，添加的CorsFilter实例无法和@CrossOrigin一起使用，一旦CorsFilter校验不通过，请求就会被Rejected。直接使用 CorsConfigurationSourcepublic class CorsConfig { @Configuration @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "sourceConfig") public static class CorsConfigurationSourceConfig { @Bean public CorsConfigurationSource corsConfigurationSource(CorsPropertiesList corsPropertiesList) { System.out.println("init bean CorsConfigurationSource with " + corsPropertiesList); return createCorsConfigurationSource(corsPropertiesList); } } ... }自定义 Filter当然，你也可以自定义一个Filter来处理CORS，但既然有CorsFilter了，除非有什么特别的情况，否则无需自己实现一个Filter来处理CORS问题。如下给出一个大概的思路，可自行完善拓展。package io.gitlab.donespeak.tutorial.cors.filter; import io.gitlab.donespeak.tutorial.cors.config.properties.CorsProperties; import io.gitlab.donespeak.tutorial.cors.config.properties.CorsPropertiesList; ... @Slf4j public class CustomCorsFilter implements Filter { private CorsPropertiesList corsPropertiesList; private AntPathMatcher antPathMatcher = new AntPathMatcher(); private static final String ALL = "*"; private Map<String, CorsProperties> corsPropertiesMap = new LinkedHashMap<>(); public CustomCorsFilter(CorsPropertiesList corsPropertiesList) { this.corsPropertiesList = corsPropertiesList; for(CorsProperties corsProperties: corsPropertiesList.getList()) { for(String pathPattern: corsProperties.getPathPatterns()) { corsPropertiesMap.put(pathPattern, corsProperties); } } } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest servletRequest = (HttpServletRequest)request; HttpServletResponse servletResponse = (HttpServletResponse)response; String origin = servletRequest.getHeader("Origin"); List<CorsProperties> corsPropertiesList = getCorsPropertiesMatch(servletRequest.getServletPath()); if(log.isDebugEnabled()) { log.debug("Try to check origin: " + origin); } CorsProperties originPassCorsProperties = null; for(CorsProperties corsProperties: corsPropertiesList) { if (corsProperties != null && isOriginAllowed(origin, corsProperties.getAllowedOrigins())) { originPassCorsProperties = corsProperties; break; } } if (originPassCorsProperties != null) { servletResponse.setHeader("Access-Control-Allow-Origin", origin); servletResponse.setHeader("Access-Control-Allow-Methods", StringUtils.arrayToCommaDelimitedString(originPassCorsProperties.getAllowedMethods())); servletResponse.setHeader("Access-Control-Allow-Headers", StringUtils.arrayToCommaDelimitedString(originPassCorsProperties.getAllowedHeaders())); servletResponse.addHeader("Access-Control-Expose-Headers", StringUtils.arrayToCommaDelimitedString(originPassCorsProperties.getExposedHeaders())); servletResponse.addHeader("Access-Control-Allow-Credentials", String.valueOf(originPassCorsProperties.getAllowedCredentials())); servletResponse.setHeader("Access-Control-Max-Age", String.valueOf(originPassCorsProperties.getMaxAge())); } else { servletResponse.setHeader("Access-Control-Allow-Origin", null); } if ("OPTIONS".equals(servletRequest.getMethod())) { servletResponse.setStatus(HttpServletResponse.SC_OK); } else { chain.doFilter(servletRequest, servletResponse); } } private List<CorsProperties> getCorsPropertiesMatch(String path) { List<CorsProperties> corsPropertiesList = new ArrayList<>(); for(Map.Entry<String, CorsProperties> entry: corsPropertiesMap.entrySet()) { if(antPathMatcher.match(entry.getKey(), path)) { corsPropertiesList.add(entry.getValue()); } } return corsPropertiesList; } private boolean isOriginAllowed(String origin, String[] allowedOrigins) { if (StringUtils.isEmpty(origin) || (allowedOrigins == null || allowedOrigins.length == 0)) { return false; } for (String allowedOrigin : allowedOrigins) { if (ALL.equals(allowedOrigin) || isOriginMatch(origin, allowedOrigin)) { return true; } } return false; } private boolean isOriginMatch(String origin, String originPattern) { return antPathMatcher.match(originPattern, origin); } }相关的配置如下：@EnableConfigurationProperties @Configuration public class CorsConfig { /** * 可与 @CrossOrigin 联用 */ @Configuration @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "customFilter") public static class CustomCorsFilterConfig { @Bean public CustomCorsFilter customCorsFilter(CorsPropertiesList corsPropertiesList) { System.out.println("init bean CustomCorsFilter with " + corsPropertiesList); return new CustomCorsFilter(corsPropertiesList); } } }因为@CrossOrigin并非通过Filter进行的处理，这里的CustomCorsFilter仅仅做添加Header的操作，如果没有校验成功，不回结束FilterChain，因而可以和@CrossOrigin一起使用。拓展获取第三方平台数据如果想要获取第三方平台的数据，可以采用服务器代理的方式进行处理。因为直接干涉第三平台的服务器的配置，而且同源策略也只有在浏览器中有效。因而可以将自己的服务器访问第三方平台的数据再返回给自己的客户端。参考和其他浏览器的同源策略Cross-Origin Resource Sharing (CORS)浏览器同源政策及其规避方法 @ruanyifeng.com跨域资源共享 CORS 详解 @ruanyifeng.com跨域资源共享 CORS5.1.8.RELEASE/spring-framework-referenceSpring 里那么多种 CORS 的配置方式，到底有什么区别源码见：tutorial/cors
实现uniapp点击按钮从相册选择图片上传，上传成功后返回图片的路径及URL参数前端使用uni自带的图片选择以及上传组件 <view> 上传图片 </view> 后端创建一个实体类，用来返回上传完成后的文件结果package cn.vwmwv.hgnuman.entity; import lombok.Data; @Data public class UpFile { String pathString; String fileUrl; String msg; } 封装一个上传文件的方法/* * Copyright (c) 2022. Kaygb * All rights reserved. * Site: https://www.vwmwv.cn/ * Github: https://github.com/kaygb/ */ package cn.vwmwv.hgnuman.utils; import cn.vwmwv.hgnuman.entity.UpFile; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.system.ApplicationHome; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Objects; import java.util.Random; @Slf4j public class UploadFile { public UpFile upFile( MultipartFile file){ String basePath = new ApplicationHome(this.getClass()).getSource().getParentFile().getPath() + "/upload-files/"; log.info(basePath); String pathString; String fileNameString; String file_url; UpFile upFile = new UpFile(); // 检查文件是否为null以及文件类型后缀是否符合要求 if (file != null && CheckFileType.CheckImgType(file.getContentType())) { // 图片后缀 String file_suffix = Objects.requireNonNull(file.getOriginalFilename()).substring(file.getOriginalFilename().lastIndexOf("."), file.getOriginalFilename().length()); Random rand = new Random(); // 图片名称 fileNameString = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "_" + (rand.nextInt(1000) + 1) + file_suffix; // 存储路径 pathString = basePath + new SimpleDateFormat("yyyy").format(new Date()) + "/" + new SimpleDateFormat("MMdd").format(new Date()) + "/" + fileNameString; // 图片url file_url = "/upload-files/" + new SimpleDateFormat("yyyy").format(new Date()) + "/" + new SimpleDateFormat("MMdd").format(new Date()) + "/" + fileNameString; try { File files = new File(pathString); // 创建新文件 boolean dir = files.getParentFile().exists(); //检查文件路径是否存在 if (!files.getParentFile().exists()) { // 如果不存在则创建文件夹 dir = files.getParentFile().mkdirs(); // 文件夹创建结果 } if (dir) { file.transferTo(files); // 把文件传输到对应的位置 upFile.setFileUrl(file_url); upFile.setPathString(pathString); upFile.setMsg(""); }else { upFile.setFileUrl("error"); upFile.setPathString("error"); upFile.setMsg("文件创建失败"); } } catch (Exception e) { e.printStackTrace(); } } else { upFile.setFileUrl("error"); upFile.setPathString("error"); upFile.setMsg("图片类型仅允许：jpg,jpeg,png,gif,hevc"); } return upFile; } } 处理路由并返回状态 @CrossOrigin // 允许跨域 @PostMapping("/fileUpload1") public Map<String, Object> fileUpload1(@RequestParam(value = "file") MultipartFile file) { Map<String, Object> map = new HashMap<>(); Map<String, String> upstatus = new HashMap<>(); UploadFile uploadFile = new UploadFile(); UpFile f = uploadFile.upFile(file); map.put("code", "0"); upstatus.put("path", f.getPathString()); upstatus.put("url", f.getFileUrl()); map.put("data", upstatus); map.put("msg", f.getMsg()); return map; }测试点击按钮上传图片，可以看到打印出来了文件在服务端存储的位置以及返回的url路径
在最近上线的小圈子Springboot项目中，使用了阿里巴巴开源fastjson对api中的数据进行解析。引入依赖<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>RELEASE</version> </dependency>后端代码创建Service创建HttpClient Service，用来处理对外部url的http请求@Service public class HttpClient { public String client(String url, HttpMethod method, MultiValueMap<String,String> param) throws Exception{ RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String > responseEntity = restTemplate.getForEntity(url,String.class); return responseEntity.getBody(); } }处理url的json数据并转换成字符串String url = "https://circleapi.url.cn/xxx/xxx/"+page; HttpMethod httpMethod = HttpMethod.GET; MultiValueMap<String,String> param = new LinkedMultiValueMap<>(); String jsonData = httpClient.client(url,httpMethod,param);提取数据节点注：代码块中的三个点...是忽略未展示的代码通过观察可以发现，我们只需要获取data节点下的数据即可，所以我们将data节点下的数据转换为数组JSONObject jsonObject = JSONObject.parseObject(jsonData); JSONArray jsonArray = jsonObject.getJSONArray("data");对JsonArray数组进行遍历，分割出单条数据for (int i = 0;i<jsonArray.size();i++){ String itemData = jsonArray.getString(i); JSONObject itemJsonObject = JSONObject.parseObject(itemData); String title = itemJsonObject.getString("title"); String text = itemJsonObject.getString("text"); String link = itemJsonObject.getString("link"); ··· }我们发现，在单条数据下的Author节点下还存在数据，所已我们也要对节点下的数据进行处理，将此节点转换为一个JSONObject对象，通过这个对象我们可以获取数据String itemAuthorItem = itemJsonObject.getString("author"); JSONObject itemAuthor = JSONObject.parseObject(itemAuthorItem); String name = itemAuthor.getString("name"); String hash = itemAuthor.getString("hash"); String site = itemAuthor.getString("site");创建一个时间转换工具类，顺便格式化一下时间戳public class DateTimeUtils { public static String convertTimestamp2Date(Long timestamp, String pattern) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern); return simpleDateFormat.format(new Date(timestamp)); } }对时间进行格式化Long timestamp = Long.parseLong(itemJsonObject.getString("created")) *1000; String pattern = "yyyy-MM-dd HH:mm:ss"; String created = DateTimeUtils.convertTimestamp2Date(timestamp, pattern);// 调用写好的转换类创建对象进行数据处理创建一个DataItem对象来对我们所需要的数据进行处理，并存入一个List中便于前台使用。package cn.vwmwv.circle.entity; import lombok.Data; import java.util.Date; /** * @ClassName: item * @Description: * @Author: kaygb * @Date: 2021/08/2021/8/20 17:12 */ @Data public class DataItem { String title; String text; String link; String create; // 作者信息 String authorHash; String name; String site; } 获取到了信息就可以在for循环中把数据存入List了List<DataItem> dataItemList = new ArrayList<>(); ... for(...){ DataItem dataItem = new DataItem(); dataItem.setTitle(title); dataItem.setText(text); dataItem.setLink(link); dataItem.setCreate(created); dataItem.setAuthorHash(hash); dataItem.setName(name); dataItem.setSite(site); log.info(title); log.info(site); dataItemList.add(dataItem); } 接下来就是对dataItemList的处理了，可以通过Model交给thymleaf或者其他模板引擎处理。
如果我们需要把数据库中的数据导出，那么大家可能会使用POI，POI是非常强大的，但是却有它的缺点，比如比较消耗内存，所以阿里巴巴团队便开源了JAVA解析Excel工具EasyExcel。easyexcel重写了poi对07版Excel的解析，能够原本一个3M的excel用POI sax依然需要100M左右内存降低到几M，并且再大的excel不会出现内存溢出，03版依赖POI的sax模式。在上层做了模型转换的封装，让使用者更加简单方便。什么是EasyExcel?EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。 Github地址:https://github.com/alibaba/easyexcel使用引入POM依赖由于是对POI进行的优化，所以我们可以在引入EasyExcel的时候先将之前的POI依赖删除或注释掉，然后填入以下依赖： <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.10</version> </dependency>然后Reload maven等待引入EasyExcel。创建实体类/* * Copyright (c) 2021. Kaygb * All rights reserved. * Site: https://www.vwmwv.cn/ * Github: https://github.com/kaygb/ */ package cn.vwmwv.hgnuman.entity; import com.alibaba.excel.annotation.ExcelIgnore; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.write.style.ColumnWidth; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import org.springframework.data.annotation.CreatedDate; import javax.persistence.*; import java.util.Date; // 报修任务 @Data @Entity @Table(name = "mana_task") @ColumnWidth(30) public class Task { @ExcelIgnore @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; @ExcelProperty(value = "创建时间", index = 0) @CreatedDate @JsonFormat(pattern="yyyy-MM-dd hh:mm:ss") @Column Date createtime;// 创建时间 @ExcelIgnore @Column Date updatetime;// 接单时间 @ExcelIgnore @Column Date verfytime;// 审核时间 @ExcelProperty(value = "报修内容描述", index = 1) @Column String description;// 报修内容描述 @ExcelProperty(value = "图片链接", index = 2) @Column String imgsrc; // 图片地址 @ExcelIgnore @Column Integer categoryid; // 所属分类id @ExcelProperty(value = "分类", index = 3) @Column String categoryname; // 此处省略部分代码 }在实体类上使用注解，表示该字段是否是需要导出的字段，并通过index设设置列顺序@ExcelProperty(value = "表头字段" index = 1)@ExcelIgnore // 忽略此字段，即导出的时候不输出@ColumnWidth(30) // 指定列宽为30取出数据写入Excel项目中使用的Spring-data-jpa，所以我们通过方法对数据库进行时间区间查询并返回一个List（数据库和数据记得配置好）List<Task> taskList = taskRepository.findAllByCreatetimeGreaterThanEqualAndCreatetimeLessThanEqual(startTimeData,endTimeData);创建导出数据的路径为项目同级根目录String PATH = new ApplicationHome(this.getClass()).getSource().getParentFile().getPath() + "/export/"; File file=new File(PATH); if(!file.exists()){//如果文件夹不存在 file.mkdir();//创建文件夹 }写入数据// 将List数据写入Excel表格 try{ String fileName = System.currentTimeMillis() + ".xlsx"; String writeNamePath = PATH + fileName; // 创建Excel表单名称 log.info("writeNamePath: " + writeNamePath); if (taskList.size()!=0){ EasyExcel.write(writeNamePath, Task.class).sheet("分类报修任务单").doWrite(taskList); // 保存生成的数据日期、路径、到数据库中 }else{ return 2;// 无数据 } log.info(PATH); }catch(Exception e){ e.printStackTrace(); }性能测试博主是先从云数据库中取出数据之后，处理一遍再使用Excel导出，实测七万多条数据用了不到五秒的时间，可见性能很强大了。文档可查阅 EasyExcel · 语雀 (yuque.com)
什么是XSS？XSS攻击全称跨站脚本攻击，是一种在web应用中的计算机安全漏洞，它允许恶意web用户将代码植入到提供给其它用户使用的页面中。也就是说，在日常所常用的表单中，我们内嵌一段js代码提交，用户通过访问再从数据库中取出这段代码，就会跟随页面进行执行，甚至可以发送cookie，跳转网站等等。我们可以使用以下代码填入表单中验证系统是否存在XSS问题。 使用Antisamy进行XSS过滤AntiSamy是OWASP的一个开源项目，通过对用户输入的 HTML / CSS / JavaScript 等内容进行检验和清理，确保输入符合应用规范。AntiSamy被广泛应用于Web服务对存储型和反射型XSS的防御中。如何使用？引入依赖和配置文件首先我们引入maven依赖。<dependency> <groupId>org.owasp.antisamy</groupId> <artifactId>antisamy</artifactId> <version>RELEASE</version> </dependency>引入之后，我们可以通过查看jar包可以发现，AntiSamy自带了多个配置文件：其中，这些常用的配置文件介绍如下：antisamy-anythinggoes.xml允许所有有效的HTML和CSS元素输入（但能拒绝JavaScript或跟CSS相关的网络钓鱼攻击），因为它包含了对于每个元素的基本规则，所以你在裁剪其它策略文件的时候可以把它作为一个知识库，一般不建议使用。antisamy-ebay.xmleBay 是当下最流行的在线拍卖网站之一。它是一个面向公众的站点，因此它允许任何人发布一系列富HTML的内容，允许输入的内容列表包含了比 Slashdot 更多的富文本内容，所以它的受攻击面也要大得多。该策略相对安全，适用于电子商务网站。antisamy-myspace.xmlMySpace 是最流行的一个社交网站之一。用户允许提交除了JavaScript之外的几乎所有他们想用的HTML和CSS。MySpace现在用一个黑名单来验证用户输入的HTML，相对较危险，不建议使用。antisamy-slashdot.xmlSlashdot 是一个提供技术新闻的网站，其安全策略非常严格。用户只能提交下列的HTML标签：< b>、< u>、< i>、< a>、< blockquote>，并且还不支持CSS。该策略文件来实现了类似的功能，允许所有文本格式的标签来直接修饰字体、颜色或者强调作用，适用于新闻网站的评论过滤。antisamy-tinymce.xml只允许文本格式通过，相对较安全antisamy.xml默认规则，允许大部分HTML通过这里，我们使用slashdot配置文件，将此文件复制到resource目录中即可开始使用。创建XSSFilter过滤器通过doFilter方法来对传入的request过滤package cn.vwmwv.hgnuman.congfig; import cn.vwmwv.hgnuman.filter.XssFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AntiSamyConfiguration { // 过滤器配置文件 @Bean public FilterRegistrationBean filterRegistrationBean(){ // XSS过滤器 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new XssFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.setOrder(1); return filterRegistrationBean; } }创建XssRequestWrapper package cn.vwmwv.hgnuman.filter; import org.owasp.validator.html.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; public class XssRequestWrapper extends HttpServletRequestWrapper { /** * Constructs a request object wrapping the given request. * * @param request the {@link HttpServletRequest} to be wrapped. * @throws IllegalArgumentException if the request is null */ public XssRequestWrapper(HttpServletRequest request) { super(request); } @Override public String[] getParameterValues( String name){ String[] values = super.getParameterValues( name); if ( values == null){ return null; } int len = values.length; String[] newArray = new String[len]; for (int j = 0; j < len; j++){ // 过滤 newArray[j] = xssClean( values[j]); } return newArray; } /** * 策略文件 * 注意，需要将要使用的策略文件放到项目资源文件路径下 * */ private static String antiSamyPath = XssRequestWrapper.class.getClassLoader() .getResource( "antisamy-slashdot.xml").getFile(); // 这里选择配置文件 /** * AntiSamy过滤数据 * @param taintedHTML 需要进行过滤的数据 * @return 返回过滤后的数据 * */ private String xssClean( String taintedHTML){ try{ // 指定策略文件 Policy policy = Policy.getInstance(antiSamyPath); // 使用AntiSamy进行过滤 AntiSamy antiSamy = new AntiSamy(); CleanResults cr = antiSamy.scan( taintedHTML, policy); taintedHTML = cr.getCleanHTML(); }catch( ScanException e) { e.printStackTrace(); }catch( PolicyException e) { e.printStackTrace(); } return taintedHTML; } }创建AntiSamy路由过滤配置/* * Copyright (c) 2021. Lorem ipsum dolor sit amet, consectetur adipiscing elit. * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. * Vestibulum commodo. Ut rhoncus gravida arcu. */ package cn.vwmwv.hgnuman.congfig; import cn.vwmwv.hgnuman.filter.XssFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AntiSamyConfiguration { // 过滤器配置文件 @Bean public FilterRegistrationBean filterRegistrationBean(){ // XSS过滤器 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new XssFilter()); filterRegistrationBean.addUrlPatterns("/*"); // 通过“ /* ”来匹配所以的提交路由，也就是对所有提交生效 filterRegistrationBean.setOrder(1); // 设置优先级 return filterRegistrationBean; } }参考资料https://blog.csdn.net/qq_35946990/article/details/74982760
现在，我们需要使用springboot和layui来上传图片或者文件，springboot已经集成了文件上传功能，我们可以直接开始使用。本次的需求是：在表单中，点击按钮上传图片，上传成功则在按钮旁边把上传的图片显示出来。视图层上传文件之后，需要将上传后获取的文件url路径放到一个隐藏的input中，在提交的时候提交给数据库存起来，所以，我们先在表单中放一个按钮和一个隐藏的img标签 问题描述 上传照片 上传图片 立即提交 其中的重点在下方，可以看到，我们放入了一个按钮，一个img标签，并且为img标签设置了默认不显示。 上传照片 上传图片 接下来，使用js来请求接口并且上传文件，上传成功后res为服务器返回的值，包括了url和实际路径，然后接着把上面的img标签填充src并且让他显示即可。//执行实例 var uploadInst = upload.render({ elem: '#uploadimg' //绑定元素 ,url: '/fileUpload' //上传接口 ,accept: 'file' ,method: 'POST' ,done: function(res){ //上传完毕回调 $("#show_up_img").attr("src","/upload-files/"+res.data.url).css("display",""); layer.alert(res.data.url + " " + res.data.path); } ,error: function(){ layer.alert("上传失败！图片限制10MB内"); //请求异常回调 } });视图的显示以及提交逻辑已经写好了，接下来，我们需要在后端处理上传的文件。控制层我们先设置一个上传的目录，也就是一个上传到服务器的路径，这里我先设置为D盘的一个文件夹。（当然，服务器大都是运行linux的，而linux和win的文件系统稍有不同，这部分后面再说）D://upload-files//在处理文件之前，我们要先配置一下路径映射，将请求的/upload-files映射到我们刚刚设置的文件夹@Configuration public class AdminWebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 映射资源url与本地资源路径 registry.addResourceHandler("/upload-files/**").addResourceLocations("file:D:/upload-files/"); } }接着，我们写一个post方法，来处理上传的文件，其中，为了避免中文文件名称的编码问题，将上传的文件名称截取到最后一个小数点，然后通过时间戳加上随机数的方式来避免文件名重复。String file_suffix = Objects.requireNonNull(file.getOriginalFilename()).substring(file.getOriginalFilename().lastIndexOf("."),file.getOriginalFilename().length()); Random rand = new Random(); fileNameString = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "_"+ (rand.nextInt(1000)+1) + file_suffix; pathString = "D://upload-files//" + fileNameString;然后使用一个Map来返回layui所需要的json的数据Map<String,Object> map = new HashMap<>(); Map<String,String> datas = new HashMap<>(); datas.put("path", pathString); datas.put("url",fileNameString); //状态码如果成功返回0 map.put("code", "0"); //错误代码 map.put("msg", ""); //返回路径 map.put("data", datas);完整代码如下：@PostMapping("/fileUpload") public Map<String,Object> fileUpload(@RequestParam(value = "file") MultipartFile file, Model model, HttpServletRequest request) { System.out.println(file); String pathString = null; String fileNameString = null; if(file!=null) { String file_suffix = Objects.requireNonNull(file.getOriginalFilename()).substring(file.getOriginalFilename().lastIndexOf("."),file.getOriginalFilename().length()); Random rand = new Random(); fileNameString = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "_"+ (rand.nextInt(1000)+1) + file_suffix; pathString = "D://upload-files//" + fileNameString; } try { assert pathString != null; File files=new File(pathString); //打印查看上传路径 System.out.println(pathString); if(!files.getParentFile().exists()){ files.getParentFile().mkdirs(); } file.transferTo(files); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } Map<String,Object> map = new HashMap<>(); Map<String,String> datas = new HashMap<>(); datas.put("path", pathString); datas.put("url",fileNameString); //状态码如果成功返回0 map.put("code", "0"); //错误代码 map.put("msg", ""); //返回实体对象集合 map.put("data", datas); return map; }优化在图片上传成功之前，我们可以添加一个正常上传的状态，在网络不好的情况下就会显示加载状态写一个函数，然后调用它function showloading(stat) { if (stat) {//如果是true则显示loading // console.log(t); loading = layer.load(1, { shade: [0.3, '#000'], //0.1透明度的白色背景 content:' 图片上传中,请耐心等待... ', success: function (layerContentStyle) { layerContentStyle.find('.layui-layer-content').css({ 'padding-top': '50px', 'text-align': 'left', 'line-height':'30px', 'color':'#fff', 'width': '300px' }) } }); }else{//如果是false则关闭loading // console.log("关闭loading层:" + t); layer.closeAll('loading'); } }接着修改上传逻辑的代码，改成下面这个样子即可var uploadInst = upload.render({ elem: '#uploadimg' //绑定元素 ,url: '/fileUpload' //上传接口 ,accept: 'file' ,method: 'POST' ,before: function(input) {// 上传完成之前 //返回的参数item，即为当前的input DOM对象 showloading(true) } ,done: function(res){ //上传完毕回调 showloading(false); $("#show_up_img").attr("src","/upload-files/"+res.data.url).css("display",""); layer.alert(res.data.url + " " + res.data.path); } ,error: function(){ showloading(false); layer.alert("上传失败！图片限制10MB内"); //请求异常回调 } });补充中途遇到过一次上传失败的情况，排查后发现是因为Springboot对上传的文件做了一个默认的限制，我们在yml配置文件里面修改一下即可上传文件到jar包相对目录使用下面的方式可以获取到当前jar包所运行的目录String basePath = new ApplicationHome(this.getClass()).getSource().getParentFile().getPath()参考layui 上传图片文件时加载状态封装 | 易学教程 (e-learn.cn)