├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── example │ │ └── demo │ │ ├── DemoApplication.java │ │ ├── controller │ │ ├── CaptchaController.java │ │ └── CaptchaRestController.java │ │ ├── model │ │ ├── BaseBo.java │ │ └── ResponseMessage.java │ │ ├── service │ │ ├── AutzQueryService.java │ │ └── impl │ │ │ └── AutzQueryServiceImpl.java │ │ ├── support │ │ ├── CaptchaConfig.java │ │ ├── CaptchaConst.java │ │ └── cache │ │ │ ├── CacheConfig.java │ │ │ ├── CacheManagerHolder.java │ │ │ └── CaffeineAutoConfiguration.java │ │ └── util │ │ ├── CaptchaUtil.java │ │ ├── ClassLoaderWrapper.java │ │ ├── ExceptionHelper.java │ │ ├── NamedThreadFactory.java │ │ ├── Resources.java │ │ ├── UtilFile.java │ │ ├── UtilNet.java │ │ ├── UtilString.java │ │ └── UtilWeb.java ├── resources │ ├── application.yml │ └── static │ │ ├── css │ │ └── captcha.css │ │ ├── img │ │ ├── erricon.png │ │ ├── loading.png │ │ ├── refresh.png │ │ └── source │ │ │ ├── 0.png │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ │ └── js │ │ ├── drag.js │ │ └── jquery.1.12.4.min.js └── webapp │ └── jsp │ └── login.jsp └── test └── java └── com └── example └── demo └── DemoApplicationTests.java /README.md: -------------------------------------------------------------------------------- 1 | # java-captcha 2 | 关于java-captcha项目,他是一个基于Java平台,滑动拼图解锁的一种验证,或者说一种解决方案。 3 | 受限于自身研究。具体解锁时的因素可以有更多。目前只是简单的判断是否在范围正负内。有能力的,可以将滑动轨迹等传入后台进行判断。 4 | 5 | ## 何为验证码 6 | 验证码,通常是为了识别操作的发起者,是人还是机器。目前现有的验证码有图片验证,即让你输入图片中的字符,中文等。 7 | 也有12306那样的点击对应图片,以及选择图中对应汉字等。实现逻辑,各有千秋。但都是为了简单方便准确的区别开,判断是人还是机器。 8 | 9 | ## 滑动拼图验证码 10 | 滑动拼图验证码是这两年流行起来的一种方式,验证码只需要,在滚动条上将拼图拖到对应位置,符合即可验证成功。不需要输入,不需要计算等。 11 | 目前流行的有极验平台:https://www.geetest.com/Sensebot 有兴趣的可以关注下。 12 | 关于拼图组成:我采用原始图片,拼图图片,水印图片组成。拼图图片为原始图片上一个区域内的一块,水印图片是指在原图上新增一块拼图图片对应的黑块,或者别的颜色的区域。 13 | 14 | ## 验证思路 15 | 用户进入验证页面---->后台选择原始图片---->后台生成水印图片与拼图图片---->放置到缓存中并记录偏移量---->返回给前台 16 | ---->前台进行加载三张图片---->滑动验证码---->后台校验偏移量---->返回一个随机码给前台---->前台操作时带上返回的验证码---->完成校验 17 | 18 | ## 验证设计 19 | 大体分为 20 | RestController: 21 | captcha: 选择并生成原始图片,拼图图片与水印图片,并存入缓存,在将缓存对应的key返回给前台。 22 | image: 前台根据后台返回的key,调用此接口从缓存中加载对应的图片 23 | check: 验证偏移量,同时返回成功与否,成功则带上一个随机码,并放入缓存中 24 | Service: 25 | captcha: 这里改成了Utils,作为一个工具类,方便别的地方直接引用,主要作用完成图片的生成 26 | auth: 缓存相关,以及验证随机码。 27 | 28 | ### 为什么采用缓存存储图片 29 | 验证码生成的图片多小而多,且复用性低。缓存效率会高很多,同时易于清理。 30 | ### 关于caffeine缓存 31 | caffeine缓存是一个GitHub上基于Java8编写的高性能高速缓存,设计机制等参照guava。在spring boot2中,默认推荐使用caffeine替代guava。所以这里我也做出了改动。 32 | ### 为什么读取图片采用自己写的Resources去读取 33 | 读取选择图片有很多种方式,这里可以自己去抽取封装下,方便改造成自己使用的。前后分离中,可以去直接调某些接口,甚至直接访问某些别的接口获取图片。 34 | 同时在springboot的jar包部署中。只能精准去读取对应的图片。不能模糊读取对应目录。不然无法读取到内部路径。 35 | ### 为什么采用验证偏移量 36 | 因个人实力和需求限定。只是简单的判断最终拼图所在的偏移量。这里其实可以扩展开,比如前台记录并传入后台,拼图在滑动过程中的轨迹,同时可以用上 37 | 机器学习或人工智能来判断滑动的轨迹,是人为,还是机器所为。(人移到过超中理论上会上下有所偏移) 38 | ### 注意极限问题 39 | 1.注意缓存的过期时间,图片的过期时间应该是很短的,例如30S.因为缓存验证码生成后,页面会立即调用加载。加载后可以主动删除,也可以偷懒,通过过期时间去控制。 40 | 我这里设置的图片30S,验证码和偏移量可以规定为5分钟,或者10分钟。同时如果图片时间设计缓存时间过长,高并发时,可能会出现内存溢出等情况。 41 | 2.原图可以先从缓存中查找是否存在。理论在高并发过程中,能减少100-200ms单次请求。减轻IO读取压力。 42 | ### 小扩展思路 43 | 其实后台还可以在上一个RSA等非对称加密。每次请求给出具体图片地址时,返回一个公钥给前台,前台根据公钥加密关键参数, 44 | 后台根据私钥解密获取参数,私钥和公钥一起生成,每个不一样,私钥存储与缓存中。这样更安全和增加爆破成本。 45 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | demo 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | demo 12 | Demo project for Spring Boot 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-parent 18 | 2.1.1.RELEASE 19 | 20 | 21 | 22 | 23 | UTF-8 24 | UTF-8 25 | 1.8 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-test 37 | test 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.springframework 48 | spring-context-support 49 | 5.1.3.RELEASE 50 | 51 | 52 | 53 | 54 | com.google.guava 55 | guava 56 | 25.1-jre 57 | 58 | 59 | 60 | com.github.ben-manes.caffeine 61 | caffeine 62 | 2.6.2 63 | 64 | 65 | 66 | org.projectlombok 67 | lombok 68 | 1.16.16 69 | 70 | 71 | 72 | com.alibaba 73 | fastjson 74 | 1.2.47 75 | 76 | 77 | commons-codec 78 | commons-codec 79 | 1.10 80 | 81 | 82 | commons-lang 83 | commons-lang 84 | 2.4 85 | 86 | 87 | org.apache.commons 88 | commons-lang3 89 | 3.3.2 90 | 91 | 92 | 93 | commons-io 94 | commons-io 95 | 1.3.2 96 | 97 | 98 | 99 | org.apache.tomcat.embed 100 | tomcat-embed-jasper 101 | 102 | 103 | 104 | javax.servlet 105 | javax.servlet-api 106 | 107 | 108 | 109 | 110 | javax.servlet.jsp 111 | javax.servlet.jsp-api 112 | 2.3.1 113 | 114 | 115 | 116 | 117 | javax.servlet 118 | jstl 119 | 120 | 121 | 122 | 123 | 124 | ${project.artifactId} 125 | 126 | 127 | 128 | 129 | src/main/webapp 130 | 131 | META-INF/resources 132 | 133 | **/** 134 | 135 | 136 | 137 | src/main/resources 138 | 139 | **/** 140 | 141 | false 142 | 143 | 144 | src/main/java 145 | 146 | **/** 147 | 148 | false 149 | 150 | 151 | 152 | 153 | org.apache.maven.plugins 154 | maven-compiler-plugin 155 | 3.1 156 | 157 | ${java.version} 158 | ${java.version} 159 | ${project.build.sourceEncoding} 160 | 161 | 162 | 163 | 164 | org.apache.maven.plugins 165 | maven-surefire-plugin 166 | 167 | true 168 | 169 | 170 | 171 | 172 | org.springframework.boot 173 | spring-boot-maven-plugin 174 | 1.3.7.RELEASE 175 | 176 | com.example.demo.DemoApplication 177 | true 178 | 179 | 180 | 181 | 182 | repackage 183 | 184 | 185 | 186 | 187 | 188 | 189 | org.apache.maven.plugins 190 | maven-war-plugin 191 | 2.6 192 | 193 | false 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import com.example.demo.support.CaptchaConfig; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 7 | import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | 10 | @SpringBootApplication(exclude={DataSourceAutoConfiguration.class,HibernateJpaAutoConfiguration.class}) 11 | @EnableConfigurationProperties({CaptchaConfig.class}) 12 | public class DemoApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(DemoApplication.class, args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/controller/CaptchaController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | /** 7 | * @author wuchen 8 | * @version 0.1 9 | * @date 2018/9/1 15:50 10 | * @use 访问滑动验证码相关页面 11 | */ 12 | @Controller 13 | public class CaptchaController { 14 | 15 | @GetMapping("/captcha") 16 | public String login(){ 17 | return "login"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/controller/CaptchaRestController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.controller; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.example.demo.model.ResponseMessage; 5 | import com.example.demo.service.AutzQueryService; 6 | import com.example.demo.support.CaptchaConfig; 7 | import com.example.demo.support.CaptchaConst; 8 | import com.example.demo.util.*; 9 | import lombok.NonNull; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import javax.imageio.ImageIO; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.awt.image.BufferedImage; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.util.Map; 22 | import java.util.Objects; 23 | import java.util.Random; 24 | 25 | /** 26 | * @author wuchen 27 | * @version 0.1 28 | * @date 2018/9/1 15:54 29 | * @use 滑动验证码rest接口 30 | */ 31 | @RestController 32 | @RequestMapping("captcha") 33 | public class CaptchaRestController { 34 | 35 | /** 36 | * 偏移量区间 37 | */ 38 | private static final int OFFSET = 4; 39 | /** 40 | * 日志提供器 41 | * Modifiers should be declared in the correct order 42 | */ 43 | private static final Logger log = LoggerFactory.getLogger(CaptchaRestController.class); 44 | 45 | @Autowired 46 | private AutzQueryService autzQueryService; 47 | 48 | @Autowired(required = false) 49 | private CaptchaConfig captchaConfig; 50 | 51 | /** 52 | * 判断是否验证成功 53 | * 54 | * @param request 请求来源 55 | * @return 成功则返回验证码,失败则返回失败信息 56 | */ 57 | @PostMapping("/checkCaptcha") 58 | public ResponseMessage checkCaptcha(HttpServletRequest request, @NonNull String point) { 59 | if (!UtilString.isNumber(point)) { 60 | log.warn("传入的偏移量为:{}", point); 61 | return ResponseMessage.error("非法参数!"); 62 | } 63 | String host = UtilWeb.getIpAddr(request); 64 | Integer veriCode = autzQueryService.getCurrentIdCaptcha(host); 65 | if ((Integer.valueOf(point) < veriCode + OFFSET) && (Integer.valueOf(point) > veriCode - OFFSET)) { 66 | // 验证通过后,生成一个验证码放入缓存并返回给前台 67 | String code = autzQueryService.putCurrentIpCode(host); 68 | //说明验证通过 69 | return ResponseMessage.ok(code); 70 | } else { 71 | return ResponseMessage.error("error"); 72 | 73 | } 74 | 75 | } 76 | 77 | /** 78 | * 生成图片 79 | * 80 | * @param request 请求 81 | * @return 返回图片及其对应请求地址 82 | * @throws IOException 丢出异常 83 | */ 84 | @PostMapping("/captchaImage") 85 | @ResponseBody 86 | public String captchaImage(HttpServletRequest request) throws IOException { 87 | CaptchaUtil resUtil = new CaptchaUtil(); 88 | String hostIp = UtilWeb.getIpAddr(request); 89 | byte[] imageData ; 90 | 91 | // 获取验证码原图 92 | String sourceImageName = getSourceImageName(); 93 | String pngName = sourceImageName.substring(sourceImageName.lastIndexOf("/") + 1); 94 | String pngBaseStr = autzQueryService.getCaptchaImageBase64Str(pngName); 95 | InputStream sourceImageInputStream; 96 | if (UtilString.isNotEmpty(pngBaseStr)) { 97 | log.info("从缓存加载了原文件:{}", pngName); 98 | sourceImageInputStream = CaptchaUtil.getInputStreamFromBase64Str(pngBaseStr); 99 | } else { 100 | // 获取对应的流 101 | sourceImageInputStream = getSourceImageInputStream(sourceImageName); 102 | } 103 | if (Objects.nonNull(sourceImageInputStream)) { 104 | imageData = UtilFile.input2byte(sourceImageInputStream); 105 | }else { 106 | log.error("读取原文件异常!"); 107 | return null; 108 | } 109 | // 读取文件 110 | Map result = resUtil.createCaptchaImage(hostIp, sourceImageName, imageData); 111 | if (result.size() > 0) { 112 | return JSON.toJSONString(result); 113 | } else { 114 | return null; 115 | } 116 | 117 | } 118 | 119 | private String getSourceImageName() { 120 | Random random = new Random(); 121 | // 获取原始图片的完整路径,随机采用一张 122 | int sourceSize = random.nextInt(captchaConfig.getSize()); 123 | return UtilString.join(captchaConfig.getPath(), sourceSize, CaptchaConst.PIC_SUFFIX); 124 | } 125 | 126 | /** 127 | * 根据原图文件路径去获取对应的文件流 128 | * 129 | * @param sourceImageName 原图名 130 | * @return 文件流 131 | * @throws IOException 异常 132 | */ 133 | private InputStream getSourceImageInputStream(String sourceImageName) throws IOException { 134 | return Resources.getResourceAsStream(sourceImageName); 135 | } 136 | 137 | /** 138 | * 从缓存中去加载某些图片 139 | * 140 | * @param imageName 图片名 141 | * @param response 从缓存中获取的图片 142 | */ 143 | @GetMapping("/image/{imageName:.+}") 144 | public void getImage(@PathVariable String imageName, HttpServletResponse response) { 145 | if (UtilString.isNotEmpty(imageName)) { 146 | try { 147 | // 先从缓存中获取是否有对应图片名的base64字符串 148 | String base64Str = autzQueryService.getCaptchaImageBase64Str(imageName); 149 | if (UtilString.isNotEmpty(base64Str)) { 150 | // 有的话则转为图片的输入流,并写出去 151 | InputStream inputStreamFromBase64Str = CaptchaUtil.getInputStreamFromBase64Str(base64Str); 152 | if (Objects.nonNull(inputStreamFromBase64Str)) { 153 | BufferedImage bufferedImage = ImageIO.read(inputStreamFromBase64Str); 154 | ImageIO.write(bufferedImage, "png", response.getOutputStream()); 155 | } 156 | } 157 | } catch (IOException e) { 158 | log.error("获取图片失败!"); 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/model/BaseBo.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.model; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * @author itw_wangjb03 7 | * @date 2018/6/12 8 | * sprint by itw_wangjb03:用于 9 | */ 10 | public interface BaseBo extends Serializable { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/model/ResponseMessage.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.model; 2 | 3 | import com.example.demo.util.ExceptionHelper; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.builder.ReflectionToStringBuilder; 6 | import org.apache.commons.lang3.builder.ToStringStyle; 7 | 8 | /** 9 | * @author itw_wangjb03 10 | * @date 2018/6/12 11 | * sprint by itw_wangjb03:用于 12 | */ 13 | @Getter 14 | public class ResponseMessage implements BaseBo { 15 | /** 16 | * 时间戳 17 | */ 18 | private Long timestamp; 19 | /** 20 | * 成功状态 21 | */ 22 | private Boolean success; 23 | /** 24 | * 状态码 25 | */ 26 | private Integer code; 27 | /** 28 | * 消息内容 29 | */ 30 | private String message; 31 | /** 32 | * 数据存放字段 33 | */ 34 | private Object data; 35 | 36 | //成功构造 37 | public static ResponseMessage ok() { 38 | return ok(null); 39 | } 40 | 41 | 42 | public static ResponseMessage ok(Object data) { 43 | ResponseMessage msg = new ResponseMessage(); 44 | msg.timeStamp().code(200).data(data).success(Boolean.TRUE); 45 | return msg; 46 | } 47 | 48 | 49 | //失败构造 50 | public static ResponseMessage error(Exception ex) { 51 | return error(500, ExceptionHelper.getBootMessage(ex)); 52 | } 53 | 54 | public static ResponseMessage error(String message) { 55 | return error(500, message); 56 | } 57 | 58 | 59 | public static ResponseMessage error(int code, String message) { 60 | ResponseMessage msg = new ResponseMessage(); 61 | msg.code(code).message(message).timeStamp().success(Boolean.FALSE); 62 | return msg; 63 | } 64 | 65 | 66 | //设置报文头信息 67 | private ResponseMessage timeStamp() { 68 | this.timeStamp(System.currentTimeMillis()); 69 | return this; 70 | } 71 | 72 | private ResponseMessage timeStamp(Long timeStamp) { 73 | this.timestamp = timeStamp; 74 | return this; 75 | } 76 | 77 | public ResponseMessage success(Boolean success){ 78 | this.success=success; 79 | return this; 80 | } 81 | 82 | public ResponseMessage code(int code) { 83 | this.code = code; 84 | return this; 85 | } 86 | 87 | public ResponseMessage message(String message) { 88 | this.message = message; 89 | return this; 90 | } 91 | 92 | 93 | //设置数据信息 94 | public ResponseMessage data(T data) { 95 | //处理分页信息 96 | this.data = data; 97 | return this; 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | return ReflectionToStringBuilder.toString(this , ToStringStyle.SHORT_PREFIX_STYLE ); 103 | } 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/service/AutzQueryService.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.service; 2 | 3 | /** 4 | * @author itw_wangjb03 5 | * @date 2018/5/4 6 | * sprint13 by itw_wangjb03:用于处理验证码 7 | */ 8 | public interface AutzQueryService { 9 | /** 10 | * 获取当前IP验证码 11 | * @param host 当前IP 12 | * @return 验证码 13 | */ 14 | String getCurrentIPCode(String host); 15 | 16 | /** 17 | * 获取对应IP地址的滑动验证码的距离 18 | * @param host IP 19 | * @return 距离 20 | */ 21 | Integer getCurrentIdCaptcha(String host); 22 | 23 | /** 24 | * 给当前IP放置一个验证码 25 | * @param host IP 26 | */ 27 | String putCurrentIpCode(String host); 28 | 29 | /** 30 | * 获取存放在redis中的某个图片的base64编码 31 | * @param imageName 32 | * @return 33 | */ 34 | String getCaptchaImageBase64Str(String imageName); 35 | 36 | /** 37 | * 移除当前IP的验证码 38 | * @param host 当前IP 39 | */ 40 | void removeCurrentIPCode(String host); 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/service/impl/AutzQueryServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.service.impl; 2 | 3 | import com.example.demo.service.AutzQueryService; 4 | import com.example.demo.support.CaptchaConst; 5 | import com.example.demo.support.cache.CacheManagerHolder; 6 | import com.example.demo.util.UtilString; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.cache.Cache; 10 | import org.springframework.cache.CacheManager; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.Objects; 14 | import java.util.UUID; 15 | 16 | /** 17 | * @author itw_wangjb03 18 | * @date 2018/5/4 19 | * sprint13 by itw_wangjb03:用于处理当前IP的验证码 20 | */ 21 | @Service 22 | public class AutzQueryServiceImpl implements AutzQueryService { 23 | 24 | private static final Logger log = LoggerFactory.getLogger(AutzQueryServiceImpl.class); 25 | 26 | 27 | /** 28 | * 获取当前IP验证码 29 | * sprint by itw_wangjb03:获取当前IP的验证码 30 | * 31 | * @param host 当前IP 32 | * @return 验证码 33 | */ 34 | @Override 35 | public String getCurrentIPCode(String host) { 36 | String verificationCode = null; 37 | // 先去取cache块 38 | Cache cache = CacheManagerHolder.getManager().getCache(CaptchaConst.VERIFICATION_CODE); 39 | if (cache != null) { 40 | // 再取当前IP 41 | Cache.ValueWrapper wrapper = cache.get(host); 42 | if (wrapper != null) { 43 | // 有值就返回出去 44 | verificationCode = wrapper.get().toString(); 45 | } 46 | } 47 | return verificationCode; 48 | } 49 | 50 | @Override 51 | public Integer getCurrentIdCaptcha(String host) { 52 | String verificationCode = null; 53 | // 先去取cache块 54 | Cache cache = CacheManagerHolder.getManager().getCache(CaptchaConst.VERIFICATION_CODE); 55 | if (cache != null) { 56 | // 再取当前IP 57 | Cache.ValueWrapper wrapper = cache.get(host); 58 | if (wrapper != null) { 59 | // 有值就返回出去 60 | verificationCode = wrapper.get().toString(); 61 | } 62 | } 63 | if (UtilString.isNotEmpty(verificationCode)) { 64 | return Integer.parseInt(verificationCode); 65 | } 66 | return 0; 67 | } 68 | 69 | /** 70 | * 给当前IP放置一个验证码 71 | * 72 | * @param host IP 73 | */ 74 | @Override 75 | public String putCurrentIpCode(String host) { 76 | String veriCode = ""; 77 | if (UtilString.isNotEmpty(host)) { 78 | String code = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 6); 79 | CacheManager manager = CacheManagerHolder.getManager(); 80 | if (Objects.nonNull(manager)) { 81 | Cache cache = manager.getCache(CaptchaConst.VERIFICATION_CODE); 82 | if (Objects.nonNull(cache)) { 83 | cache.put(host, code); 84 | } 85 | } 86 | veriCode = code; 87 | } 88 | return veriCode; 89 | } 90 | 91 | /** 92 | * 获取存放在redis中的某个图片的base64编码 93 | * 94 | * @param imageName 95 | * @return 96 | */ 97 | @Override 98 | public String getCaptchaImageBase64Str(String imageName) { 99 | String base64Str = null; 100 | // 先去取cache块 101 | Cache cache = CacheManagerHolder.getManager().getCache(CaptchaConst.CACHE_CAPTCHA_IMG); 102 | if (cache != null) { 103 | // 再取当前IP 104 | Cache.ValueWrapper wrapper = cache.get(imageName); 105 | if (wrapper != null) { 106 | log.info("验证码图片从缓存加载成功:{}", imageName); 107 | // 有值就返回出去 108 | base64Str = wrapper.get().toString(); 109 | } 110 | } 111 | return base64Str; 112 | } 113 | 114 | /** 115 | * 移除当前IP的验证码 116 | * sprint13 by itw_wangjb03:用于清除当前IP的验证码 117 | * 118 | * @param host 当前IP 119 | */ 120 | @Override 121 | public void removeCurrentIPCode(String host) { 122 | // 先去取cache块 123 | Cache cache = CacheManagerHolder.getManager().getCache(CaptchaConst.VERIFICATION_CODE); 124 | if (cache != null) { 125 | // 再取当前IP 126 | Cache.ValueWrapper wrapper = cache.get(host); 127 | if (wrapper != null) { 128 | // 判断是否有验证码 129 | String verificationCode = wrapper.get().toString(); 130 | // 有的话就移除该验证码 131 | if (UtilString.isNotEmpty(verificationCode)) { 132 | cache.evict(host); 133 | } 134 | } 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/support/CaptchaConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.support; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | /** 6 | * @author wuchenl 7 | * @date 2018/12/8. 8 | * 读取滑动验证码原始图片所在路径和数量 9 | */ 10 | @ConfigurationProperties(prefix = "com.letters7.wuchen.captcha.source") 11 | public class CaptchaConfig { 12 | private String path; 13 | private Integer size; 14 | 15 | public String getPath() { 16 | return path; 17 | } 18 | 19 | public void setPath(String path) { 20 | this.path = path; 21 | } 22 | 23 | public Integer getSize() { 24 | return size; 25 | } 26 | 27 | public void setSize(Integer size) { 28 | this.size = size; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/support/CaptchaConst.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.support; 2 | 3 | /** 4 | * @author itw_wangjb03 5 | * @date 2018/9/21 6 | * sprint by itw_wangjb03:用于 7 | */ 8 | public class CaptchaConst { 9 | 10 | /** 11 | * 中划线 12 | */ 13 | public static final String MIDDLE_LINE="-"; 14 | 15 | /** 16 | * 验证码在缓存中的key 17 | */ 18 | public static final String CAPTCHA="captcha"; 19 | 20 | /** 21 | * 缓存中的集合名 22 | */ 23 | public static final String VERIFICATION_CODE="verificationCode"; 24 | 25 | /** 26 | * 图片的缓存集合名 27 | */ 28 | public static final String CACHE_CAPTCHA_IMG="captchaImage"; 29 | 30 | /** 31 | * 生成图片后缀 32 | */ 33 | public static final String PIC_SUFFIX = ".png"; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/support/cache/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.support.cache; 2 | 3 | import com.google.common.collect.Maps; 4 | import org.springframework.boot.autoconfigure.cache.CacheType; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | import java.util.Map; 8 | 9 | /** 10 | * @author wuchen 11 | * @version 0.1 12 | * @date 2018/12/13 14:36 13 | * @use 14 | */ 15 | @ConfigurationProperties(prefix = "spring.cache") 16 | public class CacheConfig { 17 | 18 | /** 19 | * 缓存类型 20 | */ 21 | private CacheType type; 22 | /** 23 | * 扩展了springboot默认的List cacheNames方式,支持过期时间的设置 24 | */ 25 | private Map cacheNames= Maps.newConcurrentMap(); 26 | 27 | public CacheType getType() { 28 | return type; 29 | } 30 | 31 | public CacheConfig setType(CacheType type) { 32 | this.type = type; 33 | return this; 34 | } 35 | 36 | public Map getCacheNames() { 37 | return cacheNames; 38 | } 39 | 40 | public CacheConfig setCacheNames(Map cacheNames) { 41 | this.cacheNames = cacheNames; 42 | return this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/support/cache/CacheManagerHolder.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.support.cache; 2 | 3 | 4 | import com.example.demo.util.NamedThreadFactory; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.cache.CacheManager; 9 | import org.springframework.cache.concurrent.ConcurrentMapCacheManager; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.annotation.PostConstruct; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.ScheduledFuture; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * @author zoubin02 20 | */ 21 | @Component 22 | public class CacheManagerHolder { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(CacheManagerHolder.class); 25 | 26 | // 定时任务执行器 27 | private final ScheduledExecutorService scheduledExecutorService 28 | = Executors.newScheduledThreadPool(1, new NamedThreadFactory("PRINT_CACHE", true)); 29 | 30 | private ScheduledFuture sendFuture = null; 31 | 32 | private int monitorInterval = 60*60; 33 | 34 | 35 | //单例模式 36 | private CacheManagerHolder(){ 37 | 38 | } 39 | private static CacheManagerHolder instance; 40 | public static CacheManagerHolder getInstance(){ 41 | if(instance==null){ 42 | instance = new CacheManagerHolder(); 43 | } 44 | return instance; 45 | } 46 | 47 | 48 | @Autowired(required = false) 49 | private CacheManager cacheManager; 50 | 51 | public static CacheManager target; 52 | 53 | public static final CacheManager getManager() { 54 | return target; 55 | } 56 | 57 | @PostConstruct 58 | public void init() { 59 | //如果系统没有配置cacheManager,则使用ConcurrentMapCacheManager 60 | if (cacheManager == null) { 61 | cacheManager = new ConcurrentMapCacheManager(); 62 | } 63 | 64 | if (target == null){ 65 | target = cacheManager; 66 | logger.info("系统选择了缓存-{}",cacheManager.getClass()); 67 | } 68 | 69 | // 70 | sendFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { 71 | // 收集统计信息 72 | try { 73 | send(); 74 | } catch (Throwable t) { // 防御性容错 75 | logger.error("Unexpected error occur at send statistic, cause: " + t.getMessage(), t); 76 | } 77 | }, monitorInterval, monitorInterval, TimeUnit.MINUTES); 78 | } 79 | 80 | 81 | private void send() { 82 | logger.info(""); 83 | cacheManager.getCacheNames().forEach( 84 | cacheName -> { 85 | logger.info("["+cacheName + "]-------->{}",cacheManager.getCache(cacheName).getClass()); 86 | } 87 | ); 88 | logger.info(""); 89 | } 90 | 91 | 92 | 93 | public void destroy(){ 94 | if(sendFuture!=null){ 95 | sendFuture.cancel(true); 96 | } 97 | } 98 | 99 | 100 | 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/support/cache/CaffeineAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.support.cache; 2 | 3 | import com.github.benmanes.caffeine.cache.Caffeine; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.cache.CacheManager; 8 | import org.springframework.cache.annotation.CachingConfigurerSupport; 9 | import org.springframework.cache.annotation.EnableCaching; 10 | import org.springframework.cache.caffeine.CaffeineCache; 11 | import org.springframework.cache.support.SimpleCacheManager; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.ComponentScan; 14 | import org.springframework.context.annotation.Configuration; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Iterator; 18 | import java.util.Map; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | /** 22 | * @author wuchen 23 | * @version 0.1 24 | * @date 2018/12/13 14:41 25 | * @use 26 | */ 27 | @EnableCaching 28 | @Configuration 29 | @EnableConfigurationProperties(CacheConfig.class) 30 | @ComponentScan("com.example.demo.support.cache") 31 | public class CaffeineAutoConfiguration extends CachingConfigurerSupport { 32 | @Bean 33 | @ConditionalOnClass(CaffeineCache.class) 34 | @ConditionalOnProperty(prefix = "spring.cache", name = "caffeine", matchIfMissing = true) 35 | public CacheManager caffeineCacheManager(CacheConfig cacheConfig) { 36 | 37 | SimpleCacheManager simpleCacheManager = new SimpleCacheManager(); 38 | ArrayList caches = new ArrayList<>(); 39 | Map cacheNames = cacheConfig.getCacheNames(); 40 | Iterator cacheNameIter = cacheNames.keySet().iterator(); 41 | while (cacheNameIter.hasNext()) { 42 | String cacheName = cacheNameIter.next(); 43 | Long outTime = cacheNames.get(cacheName); 44 | caches.add(new CaffeineCache(cacheName,Caffeine.newBuilder().recordStats().expireAfterWrite(outTime,TimeUnit.SECONDS).build())); 45 | } 46 | simpleCacheManager.setCaches(caches); 47 | return simpleCacheManager; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/util/CaptchaUtil.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.util; 2 | 3 | import com.example.demo.support.CaptchaConst; 4 | import com.example.demo.support.cache.CacheManagerHolder; 5 | import com.google.common.collect.Maps; 6 | import org.apache.commons.codec.binary.Base64; 7 | import org.apache.commons.codec.digest.DigestUtils; 8 | import org.apache.commons.io.IOUtils; 9 | import org.apache.commons.lang.math.RandomUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.cache.Cache; 13 | import org.springframework.cache.CacheManager; 14 | 15 | import javax.imageio.ImageIO; 16 | import javax.imageio.ImageReadParam; 17 | import javax.imageio.ImageReader; 18 | import javax.imageio.stream.ImageInputStream; 19 | import java.awt.*; 20 | import java.awt.geom.Arc2D; 21 | import java.awt.geom.Area; 22 | import java.awt.geom.Rectangle2D; 23 | import java.awt.image.BufferedImage; 24 | import java.io.*; 25 | import java.util.*; 26 | import java.util.List; 27 | 28 | /** 29 | * @author wuchenl 30 | * @date 2018/12/8. 31 | */ 32 | public class CaptchaUtil { 33 | private static Logger log = LoggerFactory.getLogger(CaptchaUtil.class); 34 | 35 | /** 36 | * 将我们配置的图片原始路径和数量配置注入进来使用 37 | */ 38 | 39 | /** 40 | * 阴影宽度 41 | */ 42 | private static final int SHADOW_WIDTH = 2; 43 | /** 44 | * 图片边缘亮色(黄色)宽度。 45 | */ 46 | private static final int LIGHT_HEIGHT_WIDTH = 2; 47 | /** 48 | * 圆弧直径 49 | */ 50 | private static final int ARC = 10; 51 | /** 52 | * 生成图片后缀 53 | */ 54 | private static final String PIC_SUFFIX = ".png"; 55 | /** 56 | * 生成图片后缀 57 | */ 58 | private static final String PNG_SUFFIX = "png"; 59 | 60 | private static final Color clrGlowInnerHi = new Color(253, 239, 175, 148); 61 | private static final Color clrGlowInnerLo = new Color(255, 209, 0); 62 | private static final Color clrGlowOuterHi = new Color(253, 239, 175, 124); 63 | private static final Color clrGlowOuterLo = new Color(255, 179, 0); 64 | 65 | 66 | //----------------临时全局变量区域---------------------- 67 | 68 | /** 69 | * 小图的宽---剪裁的图的宽度 70 | */ 71 | private int tailoringWidth = 50; 72 | /** 73 | * 小图的高---剪裁的图的高度 74 | */ 75 | private int tailoringHeight = 50; 76 | /** 77 | * 随机X位置---位于原图的X位置 78 | */ 79 | private int locationX = 0; 80 | /** 81 | * 随机Y位置----位于原图的Y位置 82 | */ 83 | private int locationY = 0; 84 | 85 | /** 86 | * 根据传入的文件流以及文件名,生成对应的验证码模块以及对应的缓存获取key 87 | * 88 | * @param host 请求ip地址 89 | * @param sourceImageName 请求对应的原图 90 | * @param imageData 原文件图片对应的数组 91 | * @return 对应的相关存取key 92 | * @throws IOException 93 | */ 94 | public Map createCaptchaImage(String host, String sourceImageName, byte[] imageData) throws IOException { 95 | String sourceName = sourceImageName.substring(sourceImageName.lastIndexOf("/")+1); 96 | Map resultMap = Maps.newConcurrentMap(); 97 | log.info("开始创建滑动验证码相关图片----请求地址为:{}", host); 98 | // 获取原始图片的完整路径,随机采用一张 99 | InputStream sourceImageInputStream = UtilFile.byte2Input(imageData); 100 | if (Objects.isNull(sourceImageInputStream)) { 101 | log.warn("读取原始图片异常:{}", sourceImageName); 102 | return resultMap; 103 | } 104 | 105 | // 读取原始图片大小。并判断是否符合预设值 106 | BufferedImage bufferedImage = ImageIO.read(sourceImageInputStream); 107 | int width = bufferedImage.getWidth(); 108 | int height = bufferedImage.getHeight(); 109 | if (width <= tailoringWidth * 2 || height <= tailoringHeight) { 110 | log.warn("原始图片不符合默认尺寸:{}*{}", width, height); 111 | return resultMap; 112 | } 113 | // 这里是控制剪裁图片生成的区域。尽量位于中间。同时。图片大于100*50 114 | Random random = new Random(); 115 | this.locationX = random.nextInt(width - tailoringWidth * 2) + tailoringWidth; 116 | this.locationY = random.nextInt(height - tailoringHeight); 117 | // 获取裁剪小图 118 | BufferedImage tailoringImageBuffer = tailoringImage(imageData); 119 | 120 | //创建shape区域 121 | List shapes = createSmallShape(); 122 | if (shapes.isEmpty()) { 123 | log.error("生成剪裁小图随机形状异常!"); 124 | return resultMap; 125 | } 126 | 127 | Shape area = shapes.get(0); 128 | Shape bigarea = shapes.get(1); 129 | //创建图层用于处理小图的阴影 130 | BufferedImage bfm1 = new BufferedImage(tailoringWidth, tailoringHeight, BufferedImage.TYPE_INT_ARGB); 131 | //创建图层用于处理大图的凹槽 132 | BufferedImage bfm2 = new BufferedImage(tailoringWidth, tailoringHeight, BufferedImage.TYPE_INT_ARGB); 133 | for (int i = 0; i < tailoringWidth; i++) { 134 | for (int j = 0; j < tailoringHeight; j++) { 135 | if (area.contains(i, j)) { 136 | bfm1.setRGB(i, j, tailoringImageBuffer.getRGB(i, j)); 137 | } 138 | if (bigarea.contains(i, j)) { 139 | bfm2.setRGB(i, j, Color.black.getRGB()); 140 | } 141 | } 142 | } 143 | //处理图片的边缘高亮及其阴影效果 144 | BufferedImage resultImgBuff = dealLightAndShadow(bfm1, area); 145 | //生成大小图随机名称 146 | String smallFileName = createSmallImg(resultImgBuff); 147 | //将灰色图当做水印印到原图上 148 | String bigImgName = createBigImg(bfm2, sourceImageName); 149 | if (smallFileName == null) { 150 | return null; 151 | } 152 | resultMap.put("smallImgName", smallFileName); 153 | resultMap.put("bigImgName", bigImgName); 154 | resultMap.put("location_y", String.valueOf(locationY)); 155 | resultMap.put("sourceImgName", sourceName); 156 | 157 | // 拼接放入redis的key 158 | // host = UtilString.join(host, CaptchaConst.MIDDLE_LINE, CaptchaConst.CAPTCHA); 159 | 160 | InputStream sourceInput = UtilFile.byte2Input(imageData); 161 | String sourcePngBase64 = getBase64FromInputStream(sourceInput); 162 | boolean cacheFlag; 163 | cacheFlag = putDataToCache(CaptchaConst.CACHE_CAPTCHA_IMG, sourceName, sourcePngBase64); 164 | if (!cacheFlag) { 165 | log.error("加载原始图片进缓存异常!"); 166 | return null; 167 | } 168 | String point=String.valueOf(locationX); 169 | //将x 轴位置作为验证码 放入到redis中,key为IP-captcha 170 | cacheFlag = putDataToCache(CaptchaConst.VERIFICATION_CODE, host, point); 171 | if (!cacheFlag) { 172 | log.error("加载验证图片偏移量进缓存异常!"); 173 | return null; 174 | } 175 | return resultMap; 176 | } 177 | 178 | 179 | /** 180 | * 创建小图 181 | * 182 | * @param resultImgBuff 183 | * @return 184 | */ 185 | private String createSmallImg(BufferedImage resultImgBuff) { 186 | String smallFileName = randomImgName("small_source_"); 187 | 188 | // 图片流先转输入流 189 | InputStream inputStream = getInputStreamFromBufferedImage(resultImgBuff, PNG_SUFFIX); 190 | if (Objects.isNull(inputStream)) { 191 | log.warn("生成小图失败:转inputStream流失败!"); 192 | return null; 193 | } 194 | // 然后转为base64编码 195 | String smallPngBase64 = getBase64FromInputStream(inputStream); 196 | if (UtilString.isEmpty(smallPngBase64)) { 197 | log.warn("生成小图失败:转base64编码失败!"); 198 | return null; 199 | } 200 | // 最后放入cache 201 | boolean cacheFlag = putDataToCache(CaptchaConst.CACHE_CAPTCHA_IMG, smallFileName, smallPngBase64); 202 | if (!cacheFlag) { 203 | log.error("加载小图进缓存异常!"); 204 | return null; 205 | } 206 | return smallFileName; 207 | } 208 | 209 | 210 | /** 211 | * 创建大图,即带小图水印缺口的图片 212 | * 213 | * @param sourceImageBuffer 大图,拼图的buffer 214 | * @param sourceName 源文件 215 | * @return 大图的buffer 216 | * @throws IOException 217 | */ 218 | private String createBigImg(BufferedImage sourceImageBuffer, String sourceName) throws IOException { 219 | //创建一个灰度化图层, 将生成的小图,覆盖到该图层,使其灰度化,用于作为一个水印图 220 | String bigImgName = randomImgName("big_source_"); 221 | //将灰度化之后的图片,整合到原有图片上 222 | BufferedImage bigImg = addWatermark(sourceName, sourceImageBuffer, 0.6F); 223 | // 转is流 224 | InputStream inputStream = getInputStreamFromBufferedImage(bigImg, "png"); 225 | if (Objects.isNull(inputStream)) { 226 | log.warn("生成大图失败:转inputStream流失败!"); 227 | return null; 228 | } 229 | //转base64编码 230 | String bigPngBase64 = getBase64FromInputStream(inputStream); 231 | if (UtilString.isEmpty(bigPngBase64)) { 232 | log.warn("生成大图失败:转base64编码失败!"); 233 | return null; 234 | } 235 | //存入redis 236 | boolean cacheFlag = putDataToCache(CaptchaConst.CACHE_CAPTCHA_IMG, bigImgName, bigPngBase64); 237 | if (!cacheFlag) { 238 | log.error("加载大图进缓存异常!"); 239 | return null; 240 | } 241 | return bigImgName; 242 | } 243 | 244 | /** 245 | * 添加水印 246 | * 247 | * @param sourceName 248 | * @param smallImage 249 | * @param alpha 250 | * @return 251 | * @throws IOException 252 | */ 253 | private BufferedImage addWatermark(String sourceName, BufferedImage smallImage, float alpha) throws IOException { 254 | InputStream inputStream = Resources.getResourceAsStream(sourceName); 255 | BufferedImage source = ImageIO.read(inputStream); 256 | Graphics2D graphics2D = source.createGraphics(); 257 | graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); 258 | graphics2D.drawImage(smallImage, locationX, locationY, null); 259 | graphics2D.dispose(); //释放 260 | return source; 261 | } 262 | 263 | /** 264 | * 生成大小图片文件名 265 | * 266 | * @param suf 文件名称 267 | * @return 根据位置生成后的 268 | */ 269 | private String randomImgName(String suf) { 270 | //按照坐标位生成图片 271 | return suf + locationY + "_" + DigestUtils.md5Hex(String.valueOf(locationX)).substring(0, 16) + PIC_SUFFIX; 272 | } 273 | 274 | /** 275 | * 生成 点随机形状 276 | * 277 | * @return 278 | */ 279 | private List createSmallShape() { 280 | //处理小图,在4个方向上 随机找到2个方向添加凸出 281 | //凸出1 282 | int face1 = RandomUtils.nextInt(3); 283 | //凸出2 284 | int face2; 285 | //使凸出1 与 凸出2不在同一个方向 286 | while (true) { 287 | face2 = RandomUtils.nextInt(3); 288 | if (face1 != face2) { 289 | break; 290 | } 291 | } 292 | //生成随机区域值, (10-20)之间 293 | int position1 = RandomUtils.nextInt((tailoringHeight - ARC * 2) / 2) + (tailoringHeight - ARC * 2) / 2; 294 | Shape shape1 = createShape(face1, 0, position1); 295 | Shape bigshape1 = createShape(face1, 2, position1); 296 | 297 | //生成中间正方体Shape, (具体边界+弧半径 = x坐标位) 298 | Shape centre = new Rectangle2D.Float(ARC, ARC, tailoringWidth - 2 * 10, tailoringHeight - 2 * 10); 299 | int position2 = RandomUtils.nextInt((tailoringHeight - ARC * 2) / 2) + (tailoringHeight - ARC * 2) / 2; 300 | Shape shape2 = createShape(face2, 0, position2); 301 | 302 | //因为后边图形需要生成阴影, 所以生成的小图shape + 阴影宽度 = 灰度化的背景小图shape(即大图上的凹槽) 303 | Shape bigshape2 = createShape(face2, SHADOW_WIDTH / 2, position2); 304 | Shape bigcentre = new Rectangle2D.Float(10 - SHADOW_WIDTH / 2, 10 - SHADOW_WIDTH / 2, 30 + SHADOW_WIDTH, 30 + SHADOW_WIDTH); 305 | 306 | //合并Shape 307 | Area area = new Area(centre); 308 | area.add(new Area(shape1)); 309 | area.add(new Area(shape2)); 310 | //合并大Shape 311 | Area bigarea = new Area(bigcentre); 312 | bigarea.add(new Area(bigshape1)); 313 | bigarea.add(new Area(bigshape2)); 314 | List list = new ArrayList<>(); 315 | list.add(area); 316 | list.add(bigarea); 317 | return list; 318 | } 319 | 320 | /** 321 | * 对图片进行裁剪 322 | * 323 | * @param imageData 剪裁前原始图片流 324 | * @return 裁剪之后的图片Buffered 325 | * @throws IOException 326 | */ 327 | private BufferedImage tailoringImage(byte[] imageData) throws IOException { 328 | Iterator iterator = ImageIO.getImageReadersByFormatName(PNG_SUFFIX); 329 | ImageReader render = (ImageReader) iterator.next(); 330 | InputStream sourceInputStream = UtilFile.byte2Input(imageData); 331 | if (Objects.isNull(sourceInputStream)) { 332 | log.info("剪裁图片时获取原图文件流异常!"); 333 | throw new IOException("剪裁图片时获取原图文件流异常"); 334 | } 335 | InputStream inputStream=UtilFile.byte2Input(imageData); 336 | ImageInputStream in = ImageIO.createImageInputStream(inputStream); 337 | render.setInput(in, true); 338 | BufferedImage tailoringImageBuffer; 339 | try { 340 | ImageReadParam param = render.getDefaultReadParam(); 341 | Rectangle rect = new Rectangle(locationX, locationY, tailoringWidth, tailoringHeight); 342 | param.setSourceRegion(rect); 343 | tailoringImageBuffer = render.read(0, param); 344 | } finally { 345 | 346 | try { 347 | in.close(); 348 | inputStream.close(); 349 | sourceInputStream.close(); 350 | } catch (Exception e) { 351 | log.error("关闭流出现异常{}",e); 352 | e.printStackTrace(); 353 | } 354 | } 355 | return tailoringImageBuffer; 356 | } 357 | 358 | 359 | /** 360 | * 创建圆形区域, 半径为5 type , 0:上方,1:右方 2:下方,3:左方 361 | * 362 | * @param type 363 | * @param size 364 | * @param position 365 | * @return 366 | */ 367 | private Shape createShape(int type, int size, int position) { 368 | Arc2D.Float d; 369 | if (type == 0) { 370 | //上 371 | d = new Arc2D.Float(position, 5, 10 + size, 10 + size, 0, 190, Arc2D.CHORD); 372 | } else if (type == 1) { 373 | //右 374 | d = new Arc2D.Float(35, position, 10 + size, 10 + size, 270, 190, Arc2D.CHORD); 375 | } else if (type == 2) { 376 | //下 377 | d = new Arc2D.Float(position, 35, 10 + size, 10 + size, 180, 190, Arc2D.CHORD); 378 | } else if (type == 3) { 379 | //左 380 | d = new Arc2D.Float(5, position, 10 + size, 10 + size, 90, 190, Arc2D.CHORD); 381 | } else { 382 | d = new Arc2D.Float(5, position, 10 + size, 10 + size, 90, 190, Arc2D.CHORD); 383 | } 384 | return d; 385 | } 386 | 387 | 388 | /** 389 | * 处理小图的边缘灯光及其阴影效果 390 | * 391 | * @param bfm 392 | * @param shape 393 | * @return 394 | */ 395 | private BufferedImage dealLightAndShadow(BufferedImage bfm, Shape shape) { 396 | //创建新的透明图层,该图层用于边缘化阴影, 将生成的小图合并到该图上 397 | BufferedImage buffimg = ((Graphics2D) bfm.getGraphics()).getDeviceConfiguration().createCompatibleImage(50, 50, Transparency.TRANSLUCENT); 398 | Graphics2D graphics2D = buffimg.createGraphics(); 399 | Graphics2D g2 = (Graphics2D) bfm.getGraphics(); 400 | //原有小图,边缘亮色处理 401 | paintBorderGlow(g2, LIGHT_HEIGHT_WIDTH, shape); 402 | //新图层添加阴影 403 | paintBorderShadow(graphics2D, SHADOW_WIDTH, shape); 404 | graphics2D.drawImage(bfm, 0, 0, null); 405 | return buffimg; 406 | } 407 | 408 | /** 409 | * 处理边缘亮色 410 | * 411 | * @param g2 412 | * @param glowWidth 413 | * @param clipShape 414 | */ 415 | private void paintBorderGlow(Graphics2D g2, int glowWidth, Shape clipShape) { 416 | int gw = glowWidth * 2; 417 | for (int i = gw; i >= 2; i -= 2) { 418 | float pct = (float) (gw - i) / (gw - 1); 419 | Color mixHi = getMixedColor(clrGlowInnerHi, pct, clrGlowOuterHi, 1.0f - pct); 420 | Color mixLo = getMixedColor(clrGlowInnerLo, pct, clrGlowOuterLo, 1.0f - pct); 421 | g2.setPaint(new GradientPaint(0.0f, 35 * 0.25f, mixHi, 0.0f, 35, mixLo)); 422 | g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, pct)); 423 | g2.setStroke(new BasicStroke(i)); 424 | g2.draw(clipShape); 425 | } 426 | } 427 | 428 | /** 429 | * 处理阴影 430 | * 431 | * @param g2 432 | * @param shadowWidth 433 | * @param clipShape 434 | */ 435 | private void paintBorderShadow(Graphics2D g2, int shadowWidth, Shape clipShape) { 436 | g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 437 | int sw = shadowWidth * 2; 438 | for (int i = sw; i >= 2; i -= 2) { 439 | float pct = (float) (sw - i) / (sw - 1); 440 | //pct<03. 用于去掉阴影边缘白边, pct>0.8用于去掉过深的色彩, 如果使用Color.lightGray. 可去掉pct>0.8 441 | if (pct < 0.3 || pct > 0.8) { 442 | continue; 443 | } 444 | g2.setColor(getMixedColor(new Color(54, 54, 54), pct, Color.WHITE, 1.0f - pct)); 445 | g2.setStroke(new BasicStroke(i)); 446 | g2.draw(clipShape); 447 | } 448 | } 449 | 450 | /** 451 | * 加点颜色更明显 452 | * 453 | * @param c1 454 | * @param pct1 455 | * @param c2 456 | * @param pct2 457 | * @return 458 | */ 459 | private static Color getMixedColor(Color c1, float pct1, Color c2, float pct2) { 460 | float[] clr1 = c1.getComponents(null); 461 | float[] clr2 = c2.getComponents(null); 462 | for (int i = 0; i < clr1.length; i++) { 463 | clr1[i] = (clr1[i] * pct1) + (clr2[i] * pct2); 464 | } 465 | return new Color(clr1[0], clr1[1], clr1[2], clr1[3]); 466 | } 467 | 468 | /** 469 | * 图片转base64 470 | * 471 | * @param inputStream 图片的输入流 472 | * @return 字符串 473 | */ 474 | public static String getBase64FromInputStream(InputStream inputStream) { 475 | if (Objects.isNull(inputStream)) { 476 | return null; 477 | } 478 | byte[] data; 479 | try { 480 | ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream(); 481 | byte[] buffer = new byte[100]; 482 | int rc = 0; 483 | while ((rc = inputStream.read(buffer, 0, 100)) > 0) { 484 | arrayOutputStream.write(buffer, 0, rc); 485 | } 486 | data = arrayOutputStream.toByteArray(); 487 | } catch (IOException e) { 488 | log.error("图片流转base64编码异常:{}", e); 489 | return null; 490 | } finally { 491 | IOUtils.closeQuietly(inputStream); 492 | } 493 | return new String(org.apache.commons.codec.binary.Base64.encodeBase64(data)); 494 | } 495 | 496 | /** 497 | * base64转字节流 498 | * 499 | * @param base64Str base64字符串 500 | * @return 流 501 | */ 502 | public static InputStream getInputStreamFromBase64Str(String base64Str) { 503 | if (UtilString.isEmpty(base64Str)) { 504 | return null; 505 | } 506 | byte[] bytes = Base64.decodeBase64(base64Str); 507 | if (Objects.isNull(bytes) || bytes.length == 0) { 508 | return null; 509 | } 510 | return new ByteArrayInputStream(bytes); 511 | } 512 | 513 | /** 514 | * bufferedImage 转为普通的InputStream 515 | * 516 | * @param bufferedImage bufferedImage流 517 | * @param fileType 图片类型 518 | * @return InputStream流 519 | */ 520 | private static InputStream getInputStreamFromBufferedImage(BufferedImage bufferedImage, String fileType) { 521 | if (Objects.isNull(bufferedImage)) { 522 | log.warn("BufferImage转InputStream异常:传入bufferedImage为空!"); 523 | return null; 524 | } 525 | //默认为png 526 | if (UtilString.isEmpty(fileType)) { 527 | fileType = "png"; 528 | } 529 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 530 | try { 531 | ImageIO.write(bufferedImage, fileType, outputStream); 532 | return new ByteArrayInputStream(outputStream.toByteArray()); 533 | } catch (IOException e) { 534 | log.error("BufferImage转InputStream异常:{}", e); 535 | return null; 536 | } 537 | } 538 | 539 | 540 | /** 541 | * 放置数据进缓存 542 | * 543 | * @param cacheName 缓存块名称 544 | * @param key 缓存的key 545 | * @param value 值域 546 | * @return 是否放置成功 547 | */ 548 | private boolean putDataToCache(String cacheName, String key, Object value) { 549 | boolean cacheFlag = false; 550 | CacheManager manager = CacheManagerHolder.getManager(); 551 | if (Objects.nonNull(manager)) { 552 | Cache cache = manager.getCache(cacheName); 553 | if (Objects.nonNull(cache)) { 554 | log.info("即将放入缓存:{}",key); 555 | cache.put(key, value); 556 | cacheFlag = true; 557 | } 558 | } 559 | return cacheFlag; 560 | } 561 | } 562 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/util/ClassLoaderWrapper.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.util; 2 | 3 | import java.io.InputStream; 4 | import java.net.URL; 5 | 6 | /** 7 | * A class to wrap access to multiple class loaders making them work as one 8 | */ 9 | public class ClassLoaderWrapper { 10 | 11 | private ClassLoader defaultClassLoader; 12 | 13 | public ClassLoaderWrapper() { 14 | } 15 | 16 | public void setDefaultClassLoader(ClassLoader defaultClassLoader){ 17 | this.defaultClassLoader = defaultClassLoader; 18 | } 19 | 20 | public ClassLoader getDefaultClassLoader(){ 21 | return this.defaultClassLoader; 22 | } 23 | 24 | /** 25 | * Get a resource as a URL using the current class path 26 | * 27 | * @param resource - the resource to locate 28 | * @return the resource or null 29 | */ 30 | public URL getResourceAsURL(String resource) { 31 | return getResourceAsURL(resource, new ClassLoader[]{ 32 | defaultClassLoader, 33 | Thread.currentThread().getContextClassLoader(), 34 | getClass().getClassLoader(), 35 | ClassLoader.getSystemClassLoader() 36 | }); 37 | } 38 | 39 | /** 40 | * Get a resource from the classpath, starting with a specific class loader 41 | * 42 | * @param resource - the resource to find 43 | * @param classLoader - the first classloader to try 44 | * @return the stream or null 45 | */ 46 | public URL getResourceAsURL(String resource, ClassLoader classLoader) { 47 | return getResourceAsURL(resource, new ClassLoader[]{ 48 | classLoader, 49 | defaultClassLoader, 50 | Thread.currentThread().getContextClassLoader(), 51 | getClass().getClassLoader(), 52 | ClassLoader.getSystemClassLoader() 53 | }); 54 | } 55 | 56 | 57 | 58 | 59 | 60 | /** 61 | * Get a resource from the classpath 62 | * 63 | * @param resource - the resource to find 64 | * @return the stream or null 65 | */ 66 | public InputStream getResourceAsStream(String resource) { 67 | return getResourceAsStream(resource, new ClassLoader[]{ 68 | defaultClassLoader, 69 | Thread.currentThread().getContextClassLoader(), 70 | getClass().getClassLoader(), 71 | ClassLoader.getSystemClassLoader() 72 | }); 73 | } 74 | 75 | /** 76 | * Get a resource from the classpath, starting with a specific class loader 77 | * 78 | * @param resource - the resource to find 79 | * @param classLoader - the first class loader to try 80 | * @return the stream or null 81 | */ 82 | public InputStream getResourceAsStream(String resource, ClassLoader classLoader) { 83 | return getResourceAsStream(resource, new ClassLoader[]{ 84 | classLoader, 85 | defaultClassLoader, 86 | Thread.currentThread().getContextClassLoader(), 87 | getClass().getClassLoader(), 88 | ClassLoader.getSystemClassLoader() 89 | }); 90 | } 91 | 92 | 93 | 94 | 95 | 96 | /** 97 | * Find a class on the classpath (or die trying) 98 | * 99 | * @param name - the class to look for 100 | * @return - the class 101 | * @throws ClassNotFoundException Duh. 102 | */ 103 | public Class classForName(String name) throws ClassNotFoundException { 104 | return classForName(name, new ClassLoader[]{ 105 | defaultClassLoader, 106 | Thread.currentThread().getContextClassLoader(), 107 | getClass().getClassLoader(), 108 | ClassLoader.getSystemClassLoader() 109 | }); 110 | } 111 | 112 | /** 113 | * Find a class on the classpath, starting with a specific classloader (or die trying) 114 | * 115 | * @param name - the class to look for 116 | * @param classLoader - the first classloader to try 117 | * @return - the class 118 | * @throws ClassNotFoundException Duh. 119 | */ 120 | public Class classForName(String name, ClassLoader classLoader) throws ClassNotFoundException { 121 | return classForName(name, new ClassLoader[]{ 122 | classLoader, 123 | defaultClassLoader, 124 | Thread.currentThread().getContextClassLoader(), 125 | getClass().getClassLoader(), 126 | ClassLoader.getSystemClassLoader() 127 | }); 128 | } 129 | 130 | /** 131 | * Try to getByUserId a resource from a group of classloaders 132 | * 133 | * @param resource - the resource to getByUserId 134 | * @param classLoader - the classloaders to examine 135 | * @return the resource or null 136 | */ 137 | InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) { 138 | for (ClassLoader cl : classLoader) { 139 | if (null != cl) { 140 | 141 | // try to find the resource as passed 142 | InputStream returnValue = cl.getResourceAsStream(resource); 143 | 144 | // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource 145 | if (null == returnValue) { 146 | returnValue = cl.getResourceAsStream("/" + resource); 147 | } 148 | 149 | if (null != returnValue) { 150 | return returnValue; 151 | } 152 | } 153 | } 154 | return null; 155 | } 156 | 157 | /** 158 | * Get a resource as a URL using the current class path 159 | * 160 | * @param resource - the resource to locate 161 | * @param classLoader - the class loaders to examine 162 | * @return the resource or null 163 | */ 164 | URL getResourceAsURL(String resource, ClassLoader[] classLoader) { 165 | URL url; 166 | for (ClassLoader cl : classLoader) { 167 | 168 | if (null != cl) { 169 | 170 | // look for the resource as passed in... 171 | url = cl.getResource(resource); 172 | 173 | // ...but some class loaders want this leading "/", so we'll add it 174 | // and try again if we didn't find the resource 175 | if (null == url) url = cl.getResource("/" + resource); 176 | 177 | // "It's always in the last place I look for it!" 178 | // ... because only an idiot would keep looking for it after finding it, so stop looking already. 179 | if (null != url) return url; 180 | 181 | } 182 | 183 | } 184 | // didn't find it anywhere. 185 | return null; 186 | 187 | } 188 | 189 | /** 190 | * Attempt to load a class from a group of classloaders 191 | * 192 | * @param name - the class to load 193 | * @param classLoader - the group of classloaders to examine 194 | * @return the class 195 | * @throws ClassNotFoundException - Remember the wisdom of Judge Smails: Well, the world needs ditch diggers, too. 196 | */ 197 | Class classForName(String name, ClassLoader[] classLoader) throws ClassNotFoundException { 198 | for (ClassLoader cl : classLoader) { 199 | if (null != cl) { 200 | try { 201 | Class c = cl.loadClass(name); 202 | 203 | if (null != c) return c; 204 | 205 | } catch (ClassNotFoundException e) { 206 | // we'll ignore this until all classloaders fail to locate the class 207 | } 208 | 209 | } 210 | 211 | } 212 | throw new ClassNotFoundException("Cannot find class: " + name); 213 | 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/util/ExceptionHelper.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.util; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import java.io.PrintWriter; 5 | import java.io.StringWriter; 6 | import java.lang.reflect.InvocationTargetException; 7 | 8 | public class ExceptionHelper { 9 | 10 | /** 11 | * 在request中获取异常类 12 | * @param request 13 | * @return 14 | */ 15 | public static Throwable getThrowable(HttpServletRequest request){ 16 | Throwable ex = null; 17 | if (request.getAttribute("exception") != null) { 18 | ex = (Throwable) request.getAttribute("exception"); 19 | } else if (request.getAttribute("javax.servlet.error.exception") != null) { 20 | ex = (Throwable) request.getAttribute("javax.servlet.error.exception"); 21 | } 22 | return ex; 23 | } 24 | 25 | /** 26 | * 将ErrorStack转化为String. 27 | */ 28 | public static String getStackTraceAsString(Throwable e) { 29 | if (e == null){ 30 | return ""; 31 | } 32 | StringWriter stringWriter = new StringWriter(); 33 | e.printStackTrace(new PrintWriter(stringWriter)); 34 | return stringWriter.toString(); 35 | } 36 | 37 | 38 | /** 39 | * 找出根异常消息 40 | */ 41 | public static String getBootMessage(Throwable ex) { 42 | if(ex == null){ 43 | return ""; 44 | } 45 | 46 | if( ex instanceof NullPointerException ){ 47 | String message = "NullPointerException["+ex.getStackTrace()[0]+"]"; 48 | return message; 49 | } 50 | 51 | if( ex.getCause()!=null ){ 52 | return ex.getCause().getMessage(); 53 | } 54 | return ex.getMessage(); 55 | } 56 | 57 | 58 | /** 59 | * 将反射时的checked exception转换为unchecked exception. 60 | */ 61 | public static RuntimeException convertReflectExceptionToUnchecked(Exception e) { 62 | if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException 63 | || e instanceof NoSuchMethodException) { 64 | return new IllegalArgumentException(e); 65 | } else if (e instanceof InvocationTargetException) { 66 | return new RuntimeException(((InvocationTargetException) e).getTargetException()); 67 | } else if (e instanceof RuntimeException) { 68 | return (RuntimeException) e; 69 | } 70 | return new RuntimeException("Unexpected Checked Exception.", e); 71 | } 72 | 73 | /** 74 | * 将CheckedException转换为UncheckedException. 75 | */ 76 | public static RuntimeException unchecked(Exception e) { 77 | if (e instanceof RuntimeException) { 78 | return (RuntimeException) e; 79 | } else { 80 | return new RuntimeException(e); 81 | } 82 | } 83 | 84 | 85 | } -------------------------------------------------------------------------------- /src/main/java/com/example/demo/util/NamedThreadFactory.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.util; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | /** 7 | * 带有名称的 线程 工厂类 8 | * @author zoubin02 on 5/5/14. 9 | */ 10 | public class NamedThreadFactory implements ThreadFactory { 11 | private static final AtomicInteger POOL_SEQ = new AtomicInteger(1); 12 | 13 | private final AtomicInteger threadNum = new AtomicInteger(1); 14 | 15 | private final String prefix; 16 | 17 | private final boolean daemon; 18 | 19 | private final ThreadGroup group; 20 | 21 | public NamedThreadFactory() { 22 | this("pool-" + POOL_SEQ.getAndIncrement(), false); 23 | } 24 | 25 | public NamedThreadFactory(String prefix) { 26 | this(prefix, false); 27 | } 28 | 29 | public NamedThreadFactory(String prefix, boolean daemon) { 30 | this.prefix = prefix + "-thread-"; 31 | this.daemon = daemon; 32 | SecurityManager s = System.getSecurityManager(); 33 | group = (s == null) ? Thread.currentThread().getThreadGroup() : s.getThreadGroup(); 34 | } 35 | 36 | @Override 37 | public Thread newThread(Runnable runnable) { 38 | String name = prefix + threadNum.getAndIncrement(); 39 | Thread ret = new Thread(group, runnable, name, 0); 40 | ret.setDaemon(daemon); 41 | return ret; 42 | } 43 | 44 | public ThreadGroup getThreadGroup() { 45 | return group; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/util/Resources.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.util; 2 | 3 | 4 | 5 | import java.io.*; 6 | import java.net.URL; 7 | import java.net.URLConnection; 8 | import java.nio.charset.Charset; 9 | import java.util.Properties; 10 | 11 | /** 12 | * A class to simplify access to resources through the classloader. 13 | */ 14 | public class Resources { 15 | 16 | private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper(); 17 | 18 | /** 19 | * Charset to use when calling getResourceAsReader. 20 | * null means use the system dsl. 21 | */ 22 | private static Charset charset; 23 | 24 | Resources() { 25 | } 26 | 27 | /** 28 | * Returns the dsl classloader (may be null). 29 | * 30 | * @return The dsl classloader 31 | */ 32 | public static ClassLoader getDefaultClassLoader() { 33 | return classLoaderWrapper.getDefaultClassLoader(); 34 | } 35 | 36 | /** 37 | * Sets the dsl classloader 38 | * 39 | * @param defaultClassLoader - the new dsl ClassLoader 40 | */ 41 | public static void setDefaultClassLoader(ClassLoader defaultClassLoader) { 42 | classLoaderWrapper.setDefaultClassLoader(defaultClassLoader); 43 | } 44 | 45 | /** 46 | * Returns the URL of the resource on the classpath 47 | * 48 | * @param resource The resource to find 49 | * @return The resource 50 | * @throws IOException If the resource cannot be found or read 51 | */ 52 | public static URL getResourceURL(String resource){ 53 | return classLoaderWrapper.getResourceAsURL(resource); 54 | } 55 | 56 | /** 57 | * Returns the URL of the resource on the classpath 58 | * 59 | * @param loader The classloader used to fetch the resource 60 | * @param resource The resource to find 61 | * @return The resource 62 | * @throws IOException If the resource cannot be found or read 63 | */ 64 | public static URL getResourceURL(ClassLoader loader, String resource) throws IOException { 65 | URL url = classLoaderWrapper.getResourceAsURL(resource, loader); 66 | if (url == null) throw new IOException("Could not find resource " + resource); 67 | return url; 68 | } 69 | 70 | /** 71 | * Returns a resource on the classpath as a Stream object 72 | * 73 | * @param resource The resource to find 74 | * @return The resource 75 | * @throws IOException If the resource cannot be found or read 76 | */ 77 | public static InputStream getResourceAsStream(String resource) throws IOException { 78 | return getResourceAsStream(null, resource); 79 | } 80 | 81 | /** 82 | * Returns a resource on the classpath as a Stream object 83 | * 84 | * @param loader The classloader used to fetch the resource 85 | * @param resource The resource to find 86 | * @return The resource 87 | * @throws IOException If the resource cannot be found or read 88 | */ 89 | public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException { 90 | InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader); 91 | if (in == null) { 92 | throw new IOException("Could not find resource " + resource); 93 | } 94 | return in; 95 | } 96 | 97 | /** 98 | * Returns a resource on the classpath as a Properties object 99 | * 100 | * @param resource The resource to find 101 | * @return The resource 102 | * @throws IOException If the resource cannot be found or read 103 | */ 104 | public static Properties getResourceAsProperties(String resource) throws IOException { 105 | Properties props = new Properties(); 106 | InputStream in = getResourceAsStream(resource); 107 | props.load(in); 108 | in.close(); 109 | return props; 110 | } 111 | 112 | /** 113 | * Returns a resource on the classpath as a Properties object 114 | * 115 | * @param loader The classloader used to fetch the resource 116 | * @param resource The resource to find 117 | * @return The resource 118 | * @throws IOException If the resource cannot be found or read 119 | */ 120 | public static Properties getResourceAsProperties(ClassLoader loader, String resource) throws IOException { 121 | Properties props = new Properties(); 122 | InputStream in = getResourceAsStream(loader, resource); 123 | props.load(in); 124 | in.close(); 125 | return props; 126 | } 127 | 128 | /** 129 | * Returns a resource on the classpath as a Reader object 130 | * 131 | * @param resource The resource to find 132 | * @return The resource 133 | * @throws IOException If the resource cannot be found or read 134 | */ 135 | public static Reader getResourceAsReader(String resource) throws IOException { 136 | Reader reader; 137 | if (charset == null) { 138 | reader = new InputStreamReader(getResourceAsStream(resource)); 139 | } else { 140 | reader = new InputStreamReader(getResourceAsStream(resource), charset); 141 | } 142 | return reader; 143 | } 144 | 145 | /** 146 | * Returns a resource on the classpath as a Reader object 147 | * 148 | * @param loader The classloader used to fetch the resource 149 | * @param resource The resource to find 150 | * @return The resource 151 | * @throws IOException If the resource cannot be found or read 152 | */ 153 | public static Reader getResourceAsReader(ClassLoader loader, String resource) throws IOException { 154 | Reader reader; 155 | if (charset == null) { 156 | reader = new InputStreamReader(getResourceAsStream(loader, resource)); 157 | } else { 158 | reader = new InputStreamReader(getResourceAsStream(loader, resource), charset); 159 | } 160 | return reader; 161 | } 162 | 163 | /** 164 | * Returns a resource on the classpath as a File object 165 | * 166 | * @param resource The resource to find 167 | * @return The resource 168 | * @throws IOException If the resource cannot be found or read 169 | */ 170 | public static File getResourceAsFile(String resource) throws IOException { 171 | return new File(getResourceURL(resource).getFile()); 172 | } 173 | 174 | /** 175 | * Returns a resource on the classpath as a File object 176 | * 177 | * @param loader - the classloader used to fetch the resource 178 | * @param resource - the resource to find 179 | * @return The resource 180 | * @throws IOException If the resource cannot be found or read 181 | */ 182 | public static File getResourceAsFile(ClassLoader loader, String resource) throws IOException { 183 | return new File(getResourceURL(loader, resource).getFile()); 184 | } 185 | 186 | /** 187 | * Gets a URL as an input stream 188 | * 189 | * @param urlString - the URL to getByUserId 190 | * @return An input stream with the data from the URL 191 | * @throws IOException If the resource cannot be found or read 192 | */ 193 | public static InputStream getUrlAsStream(String urlString) throws IOException { 194 | URL url = new URL(urlString); 195 | URLConnection conn = url.openConnection(); 196 | return conn.getInputStream(); 197 | } 198 | 199 | /** 200 | * Gets a URL as a Reader 201 | * 202 | * @param urlString - the URL to getByUserId 203 | * @return A Reader with the data from the URL 204 | * @throws IOException If the resource cannot be found or read 205 | */ 206 | public static Reader getUrlAsReader(String urlString) throws IOException { 207 | return new InputStreamReader(getUrlAsStream(urlString)); 208 | } 209 | 210 | /** 211 | * Gets a URL as a Properties object 212 | * 213 | * @param urlString - the URL to getByUserId 214 | * @return A Properties object with the data from the URL 215 | * @throws IOException If the resource cannot be found or read 216 | */ 217 | public static Properties getUrlAsProperties(String urlString) throws IOException { 218 | Properties props = new Properties(); 219 | InputStream in = getUrlAsStream(urlString); 220 | props.load(in); 221 | in.close(); 222 | return props; 223 | } 224 | 225 | /** 226 | * Loads a class 227 | * 228 | * @param className - the class to fetch 229 | * @return The loaded class 230 | * @throws ClassNotFoundException If the class cannot be found (duh!) 231 | */ 232 | public static Class classForName(String className) throws ClassNotFoundException { 233 | return classLoaderWrapper.classForName(className); 234 | } 235 | 236 | public static Charset getCharset() { 237 | return charset; 238 | } 239 | 240 | public static void setCharset(Charset charset) { 241 | Resources.charset = charset; 242 | } 243 | 244 | } 245 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/util/UtilFile.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.util; 2 | 3 | import com.google.common.collect.Lists; 4 | import org.apache.commons.io.IOUtils; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.*; 11 | import java.util.List; 12 | import java.util.Objects; 13 | 14 | 15 | /** 16 | * 文件流辅助类 17 | * 18 | * @author zoubin 19 | * @since 2013-09-13 20 | */ 21 | public class UtilFile extends Resources { 22 | 23 | private static Logger logger = LoggerFactory.getLogger(UtilFile.class); 24 | 25 | private static String LINE_SEPARATOR = System.getProperty("line.separator"); 26 | 27 | 28 | /** 29 | * 得到文件名中的父路径部分。 对两种路径分隔符都有效。 不存在时返回""。 如果文件名是以路径分隔符结尾的则不考虑该分隔符,例如"/path/"返回""。 30 | */ 31 | public static String getPathPart(String fileName) { 32 | int point = getPathLsatIndex(fileName); 33 | int length = fileName.length(); 34 | if (point == -1) { 35 | return ""; 36 | } else if (point == length - 1) { 37 | int secondPoint = getPathLsatIndex(fileName, point - 1); 38 | if (secondPoint == -1) { 39 | return ""; 40 | } else { 41 | return fileName.substring(0, secondPoint); 42 | } 43 | } else { 44 | return fileName.substring(0, point); 45 | } 46 | } 47 | 48 | /** 49 | * 得到文件的名字部分。 实际上就是路径中的最后一个路径分隔符后的部分。 50 | */ 51 | public static String getNamePart(String fileName) { 52 | int point = getPathLsatIndex(fileName); 53 | int length = fileName.length(); 54 | if (point == -1) { 55 | return fileName; 56 | } else if (point == length - 1) { 57 | int secondPoint = getPathLsatIndex(fileName, point - 1); 58 | if (secondPoint == -1) { 59 | if (length == 1) { 60 | return fileName; 61 | } else { 62 | return fileName.substring(0, point); 63 | } 64 | } else { 65 | return fileName.substring(secondPoint, point); 66 | } 67 | } else { 68 | return fileName.substring(point + 1, length); 69 | } 70 | } 71 | 72 | /** 73 | * 得到文件的类型。 实际上就是得到文件名中最后一个“.”后面的部分。 74 | */ 75 | public static String getTypePart(String fileName) { 76 | int point = fileName.lastIndexOf('.'); 77 | int length = fileName.length(); 78 | if (point == -1 || point == length - 1) { 79 | return ""; 80 | } else { 81 | return fileName.substring(point, length); 82 | } 83 | } 84 | 85 | //得到路径分隔符在文件路径中最后出现的位置。 对于DOS或者UNIX风格的分隔符都可以。 86 | private static int getPathLsatIndex(String fileName) { 87 | int point = fileName.lastIndexOf('/'); 88 | if (point == -1) { 89 | point = fileName.lastIndexOf('\\'); 90 | } 91 | return point; 92 | } 93 | 94 | 95 | //得到路径分隔符在文件路径中指定位置前最后出现的位置。 对于DOS或者UNIX风格的分隔符都可以。 96 | private static int getPathLsatIndex(String fileName, int fromIndex) { 97 | int point = fileName.lastIndexOf('/', fromIndex); 98 | if (point == -1) { 99 | point = fileName.lastIndexOf('\\', fromIndex); 100 | } 101 | return point; 102 | } 103 | 104 | 105 | public static boolean exist(String path) { 106 | return new File(path).exists(); 107 | } 108 | 109 | /** 110 | * *****************************读相关***************************** 111 | */ 112 | public static String read(InputStream is) throws IOException { 113 | return read(is, "utf-8"); 114 | } 115 | 116 | public static String read(InputStream is, String encoding) throws IOException { 117 | BufferedReader br = new BufferedReader(new InputStreamReader(is, encoding)); 118 | StringBuilder content = new StringBuilder(); 119 | String data = null; 120 | while ((data = br.readLine()) != null) { 121 | content.append(data); 122 | content.append(LINE_SEPARATOR); 123 | } 124 | return content.toString(); 125 | } 126 | 127 | //得到文件或者文件夹的大小(包含所有子文件) 128 | public static long getSize(File file) { 129 | if (file.exists()) { 130 | if (!file.isFile()) { 131 | long size = 0; 132 | File[] files = file.listFiles(); 133 | if (files != null && files.length > 0) { 134 | for (File f : files) { 135 | size += getSize(f); 136 | } 137 | } 138 | return size; 139 | } else { 140 | return file.length(); 141 | } 142 | } 143 | return 0; 144 | } 145 | 146 | //文件变为流 147 | public static byte[] readFile2Byte(String filePath) { 148 | byte[] buffer = null; 149 | FileInputStream fis = null; 150 | try { 151 | File file = new File(filePath); 152 | fis = new FileInputStream(file); 153 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 154 | byte[] b = new byte[1024]; 155 | int n; 156 | while ((n = fis.read(b)) != -1) { 157 | bos.write(b, 0, n); 158 | } 159 | fis.close(); 160 | bos.close(); 161 | buffer = bos.toByteArray(); 162 | } catch (FileNotFoundException e) { 163 | e.printStackTrace(); 164 | } catch (IOException e) { 165 | e.printStackTrace(); 166 | } finally { 167 | if (fis != null) { 168 | IOUtils.closeQuietly(fis); 169 | } 170 | } 171 | return buffer; 172 | } 173 | 174 | 175 | /** 176 | * 根据“文件名的后缀”获取文件内容类型(而非根据File.getContentType()读取的文件类型) 177 | * 178 | * @param returnFileName 带验证的文件名 179 | * @return 返回文件类型 180 | */ 181 | public static String getContentType(String returnFileName) { 182 | String contentType = "application/octet-stream"; 183 | if (returnFileName.lastIndexOf(".") < 0) 184 | return contentType; 185 | returnFileName = returnFileName.toLowerCase(); 186 | returnFileName = returnFileName.substring(returnFileName.lastIndexOf(".") + 1); 187 | if (returnFileName.equals("html") || returnFileName.equals("htm") || returnFileName.equals("shtml")) { 188 | contentType = "text/html"; 189 | } else if (returnFileName.equals("apk")) { 190 | contentType = "application/vnd.android.package-archive"; 191 | } else if (returnFileName.equals("sis")) { 192 | contentType = "application/vnd.symbian.install"; 193 | } else if (returnFileName.equals("sisx")) { 194 | contentType = "application/vnd.symbian.install"; 195 | } else if (returnFileName.equals("exe")) { 196 | contentType = "application/x-msdownload"; 197 | } else if (returnFileName.equals("msi")) { 198 | contentType = "application/x-msdownload"; 199 | } else if (returnFileName.equals("css")) { 200 | contentType = "text/css"; 201 | } else if (returnFileName.equals("xml")) { 202 | contentType = "text/xml"; 203 | } else if (returnFileName.equals("gif")) { 204 | contentType = "image/gif"; 205 | } else if (returnFileName.equals("jpeg") || returnFileName.equals("jpg")) { 206 | contentType = "image/jpeg"; 207 | } else if (returnFileName.equals("js")) { 208 | contentType = "application/x-javascript"; 209 | } else if (returnFileName.equals("atom")) { 210 | contentType = "application/atom+xml"; 211 | } else if (returnFileName.equals("rss")) { 212 | contentType = "application/rss+xml"; 213 | } else if (returnFileName.equals("mml")) { 214 | contentType = "text/mathml"; 215 | } else if (returnFileName.equals("txt")) { 216 | contentType = "text/plain"; 217 | } else if (returnFileName.equals("jad")) { 218 | contentType = "text/vnd.sun.j2me.app-descriptor"; 219 | } else if (returnFileName.equals("wml")) { 220 | contentType = "text/vnd.wap.wml"; 221 | } else if (returnFileName.equals("htc")) { 222 | contentType = "text/x-component"; 223 | } else if (returnFileName.equals("png")) { 224 | contentType = "image/png"; 225 | } else if (returnFileName.equals("tif") || returnFileName.equals("tiff")) { 226 | contentType = "image/tiff"; 227 | } else if (returnFileName.equals("wbmp")) { 228 | contentType = "image/vnd.wap.wbmp"; 229 | } else if (returnFileName.equals("ico")) { 230 | contentType = "image/x-icon"; 231 | } else if (returnFileName.equals("jng")) { 232 | contentType = "image/x-jng"; 233 | } else if (returnFileName.equals("bmp")) { 234 | contentType = "image/x-ms-bmp"; 235 | } else if (returnFileName.equals("svg")) { 236 | contentType = "image/svg+xml"; 237 | } else if (returnFileName.equals("jar") || returnFileName.equals("var") 238 | || returnFileName.equals("ear")) { 239 | contentType = "application/java-archive"; 240 | } else if (returnFileName.equals("doc")) { 241 | contentType = "application/msword"; 242 | } else if (returnFileName.equals("pdf")) { 243 | contentType = "application/pdf"; 244 | } else if (returnFileName.equals("rtf")) { 245 | contentType = "application/rtf"; 246 | } else if (returnFileName.equals("xls")) { 247 | contentType = "application/vnd.ms-excel"; 248 | } else if (returnFileName.equals("ppt")) { 249 | contentType = "application/vnd.ms-powerpoint"; 250 | } else if (returnFileName.equals("7z")) { 251 | contentType = "application/x-7z-compressed"; 252 | } else if (returnFileName.equals("rar")) { 253 | contentType = "application/x-rar-compressed"; 254 | } else if (returnFileName.equals("swf")) { 255 | contentType = "application/x-shockwave-flash"; 256 | } else if (returnFileName.equals("rpm")) { 257 | contentType = "application/x-redhat-package-manager"; 258 | } else if (returnFileName.equals("der") || returnFileName.equals("pem") 259 | || returnFileName.equals("crt")) { 260 | contentType = "application/x-x509-ca-cert"; 261 | } else if (returnFileName.equals("xhtml")) { 262 | contentType = "application/xhtml+xml"; 263 | } else if (returnFileName.equals("zip")) { 264 | contentType = "application/zip"; 265 | } else if (returnFileName.equals("mid") || returnFileName.equals("midi") 266 | || returnFileName.equals("kar")) { 267 | contentType = "audio/midi"; 268 | } else if (returnFileName.equals("mp3")) { 269 | contentType = "audio/mpeg"; 270 | } else if (returnFileName.equals("ogg")) { 271 | contentType = "audio/ogg"; 272 | } else if (returnFileName.equals("m4a")) { 273 | contentType = "audio/x-m4a"; 274 | } else if (returnFileName.equals("ra")) { 275 | contentType = "audio/x-realaudio"; 276 | } else if (returnFileName.equals("3gpp") 277 | || returnFileName.equals("3gp")) { 278 | contentType = "video/3gpp"; 279 | } else if (returnFileName.equals("mp4")) { 280 | contentType = "video/mp4"; 281 | } else if (returnFileName.equals("mpeg") 282 | || returnFileName.equals("mpg")) { 283 | contentType = "video/mpeg"; 284 | } else if (returnFileName.equals("mov")) { 285 | contentType = "video/quicktime"; 286 | } else if (returnFileName.equals("flv")) { 287 | contentType = "video/x-flv"; 288 | } else if (returnFileName.equals("m4v")) { 289 | contentType = "video/x-m4v"; 290 | } else if (returnFileName.equals("mng")) { 291 | contentType = "video/x-mng"; 292 | } else if (returnFileName.equals("asx") || returnFileName.equals("asf")) { 293 | contentType = "video/x-ms-asf"; 294 | } else if (returnFileName.equals("wmv")) { 295 | contentType = "video/x-ms-wmv"; 296 | } else if (returnFileName.equals("avi")) { 297 | contentType = "video/x-msvideo"; 298 | } 299 | return contentType; 300 | } 301 | 302 | 303 | /** 304 | * 修正路径,将 \\ 或 / 等替换为 File.separator 305 | * 306 | * @param path 待修正的路径 307 | * @return 修正后的路径 308 | */ 309 | public static String path(String path) { 310 | String p = UtilString.replace(path, "\\", "/"); 311 | p = UtilString.join(UtilString.split(p, "/"), "/"); 312 | if (!UtilString.startsWithAny(p, "/") && UtilString.startsWithAny(path, "\\", "/")) { 313 | p += "/"; 314 | } 315 | if (!UtilString.endsWithAny(p, "/") && UtilString.endsWithAny(path, "\\", "/")) { 316 | p = p + "/"; 317 | } 318 | if (path != null && path.startsWith("/")) { 319 | p = "/" + p; // linux下路径 320 | } 321 | return p; 322 | } 323 | 324 | /** 325 | * 获目录下的文件列表 326 | * 327 | * @param dir 搜索目录 328 | * @param searchDirs 是否是搜索目录 329 | * @return 文件列表 330 | */ 331 | public static List findChildrenList(File dir, boolean searchDirs) { 332 | List files = Lists.newArrayList(); 333 | for (String subFiles : dir.list()) { 334 | File file = new File(dir + "/" + subFiles); 335 | if (((searchDirs) && (file.isDirectory())) || ((!searchDirs) && (!file.isDirectory()))) { 336 | files.add(file.getName()); 337 | } 338 | } 339 | return files; 340 | } 341 | 342 | /** 343 | * 获取文件扩展名(返回小写) 344 | * 345 | * @param fileName 文件名 346 | * @return 例如:test.jpg 返回: jpg 347 | */ 348 | public static String getFileExtension(String fileName) { 349 | if ((fileName == null) || (fileName.lastIndexOf(".") == -1) || (fileName.lastIndexOf(".") == fileName.length() - 1)) { 350 | return null; 351 | } 352 | return UtilString.lowerCase(fileName.substring(fileName.lastIndexOf(".") + 1)); 353 | } 354 | 355 | /** 356 | * 获取文件名,不包含扩展名 357 | * 358 | * @param fileName 文件名 359 | * @return 例如:d:\files\test.jpg 返回:d:\files\test 360 | */ 361 | public static String getFileNameWithoutExtension(String fileName) { 362 | if ((fileName == null) || (fileName.lastIndexOf(".") == -1)) { 363 | return null; 364 | } 365 | return fileName.substring(0, fileName.lastIndexOf(".")); 366 | } 367 | 368 | 369 | /** 370 | ******************************复制相关***************************** 371 | */ 372 | /** 373 | * 复制单个文件,如果目标文件存在,则不覆盖 374 | * 375 | * @param srcFileName 待复制的文件名 376 | * @param descFileName 目标文件名 377 | * @return 如果复制成功,则返回true,否则返回false 378 | */ 379 | public static boolean copyFile(String srcFileName, String descFileName) { 380 | return UtilFile.copyFileCover(srcFileName, descFileName, false); 381 | } 382 | 383 | /** 384 | * 复制单个文件 385 | * 386 | * @param srcFileName 待复制的文件名 387 | * @param descFileName 目标文件名 388 | * @param coverlay 如果目标文件已存在,是否覆盖 389 | * @return 如果复制成功,则返回true,否则返回false 390 | */ 391 | public static boolean copyFileCover(String srcFileName, 392 | String descFileName, boolean coverlay) { 393 | File srcFile = new File(srcFileName); 394 | // 判断源文件是否存在 395 | if (!srcFile.exists()) { 396 | logger.debug("复制文件失败,源文件 " + srcFileName + " 不存在!"); 397 | return false; 398 | } 399 | // 判断源文件是否是合法的文件 400 | else if (!srcFile.isFile()) { 401 | logger.debug("复制文件失败," + srcFileName + " 不是一个文件!"); 402 | return false; 403 | } 404 | File descFile = new File(descFileName); 405 | // 判断目标文件是否存在 406 | if (descFile.exists()) { 407 | // 如果目标文件存在,并且允许覆盖 408 | if (coverlay) { 409 | logger.debug("目标文件已存在,准备删除!"); 410 | if (!UtilFile.delFile(descFileName)) { 411 | logger.debug("删除目标文件 " + descFileName + " 失败!"); 412 | return false; 413 | } 414 | } else { 415 | logger.debug("复制文件失败,目标文件 " + descFileName + " 已存在!"); 416 | return false; 417 | } 418 | } else { 419 | if (!descFile.getParentFile().exists()) { 420 | // 如果目标文件所在的目录不存在,则创建目录 421 | logger.debug("目标文件所在的目录不存在,创建目录!"); 422 | // 创建目标文件所在的目录 423 | if (!descFile.getParentFile().mkdirs()) { 424 | logger.debug("创建目标文件所在的目录失败!"); 425 | return false; 426 | } 427 | } 428 | } 429 | 430 | // 准备复制文件 431 | // 读取的位数 432 | int readByte = 0; 433 | InputStream ins = null; 434 | OutputStream outs = null; 435 | try { 436 | // 打开源文件 437 | ins = new FileInputStream(srcFile); 438 | // 打开目标文件的输出流 439 | outs = new FileOutputStream(descFile); 440 | byte[] buf = new byte[1024]; 441 | // 一次读取1024个字节,当readByte为-1时表示文件已经读取完毕 442 | while ((readByte = ins.read(buf)) != -1) { 443 | // 将读取的字节流写入到输出流 444 | outs.write(buf, 0, readByte); 445 | } 446 | logger.debug("复制单个文件 " + srcFileName + " 到" + descFileName 447 | + "成功!"); 448 | return true; 449 | } catch (Exception e) { 450 | logger.debug("复制文件失败:" + e.getMessage()); 451 | return false; 452 | } finally { 453 | // 关闭输入输出流,首先关闭输出流,然后再关闭输入流 454 | if (outs != null) { 455 | try { 456 | outs.close(); 457 | } catch (IOException oute) { 458 | oute.printStackTrace(); 459 | } 460 | } 461 | if (ins != null) { 462 | try { 463 | ins.close(); 464 | } catch (IOException ine) { 465 | ine.printStackTrace(); 466 | } 467 | } 468 | } 469 | } 470 | 471 | /** 472 | * 复制整个目录的内容,如果目标目录存在,则不覆盖 473 | * 474 | * @param srcDirName 源目录名 475 | * @param descDirName 目标目录名 476 | * @return 如果复制成功返回true,否则返回false 477 | */ 478 | public static boolean copyDirectory(String srcDirName, String descDirName) { 479 | return UtilFile.copyDirectoryCover(srcDirName, descDirName, 480 | false); 481 | } 482 | 483 | /** 484 | * 复制整个目录的内容 485 | * 486 | * @param srcDirName 源目录名 487 | * @param descDirName 目标目录名 488 | * @param coverlay 如果目标目录存在,是否覆盖 489 | * @return 如果复制成功返回true,否则返回false 490 | */ 491 | public static boolean copyDirectoryCover(String srcDirName, 492 | String descDirName, boolean coverlay) { 493 | File srcDir = new File(srcDirName); 494 | // 判断源目录是否存在 495 | if (!srcDir.exists()) { 496 | logger.debug("复制目录失败,源目录 " + srcDirName + " 不存在!"); 497 | return false; 498 | } 499 | // 判断源目录是否是目录 500 | else if (!srcDir.isDirectory()) { 501 | logger.debug("复制目录失败," + srcDirName + " 不是一个目录!"); 502 | return false; 503 | } 504 | // 如果目标文件夹名不以文件分隔符结尾,自动添加文件分隔符 505 | String descDirNames = descDirName; 506 | if (!descDirNames.endsWith(File.separator)) { 507 | descDirNames = descDirNames + File.separator; 508 | } 509 | File descDir = new File(descDirNames); 510 | // 如果目标文件夹存在 511 | if (descDir.exists()) { 512 | if (coverlay) { 513 | // 允许覆盖目标目录 514 | logger.debug("目标目录已存在,准备删除!"); 515 | if (!UtilFile.delFile(descDirNames)) { 516 | logger.debug("删除目录 " + descDirNames + " 失败!"); 517 | return false; 518 | } 519 | } else { 520 | logger.debug("目标目录复制失败,目标目录 " + descDirNames + " 已存在!"); 521 | return false; 522 | } 523 | } else { 524 | // 创建目标目录 525 | logger.debug("目标目录不存在,准备创建!"); 526 | if (!descDir.mkdirs()) { 527 | logger.debug("创建目标目录失败!"); 528 | return false; 529 | } 530 | 531 | } 532 | 533 | boolean flag = true; 534 | // 列出源目录下的所有文件名和子目录名 535 | File[] files = srcDir.listFiles(); 536 | for (int i = 0; i < files.length; i++) { 537 | // 如果是一个单个文件,则直接复制 538 | if (files[i].isFile()) { 539 | flag = UtilFile.copyFile(files[i].getAbsolutePath(), 540 | descDirName + files[i].getName()); 541 | // 如果拷贝文件失败,则退出循环 542 | if (!flag) { 543 | break; 544 | } 545 | } 546 | // 如果是子目录,则继续复制目录 547 | if (files[i].isDirectory()) { 548 | flag = UtilFile.copyDirectory(files[i] 549 | .getAbsolutePath(), descDirName + files[i].getName()); 550 | // 如果拷贝目录失败,则退出循环 551 | if (!flag) { 552 | break; 553 | } 554 | } 555 | } 556 | 557 | if (!flag) { 558 | logger.debug("复制目录 " + srcDirName + " 到 " + descDirName + " 失败!"); 559 | return false; 560 | } 561 | logger.debug("复制目录 " + srcDirName + " 到 " + descDirName + " 成功!"); 562 | return true; 563 | 564 | } 565 | 566 | 567 | /** 568 | ******************************删除相关***************************** 569 | */ 570 | /** 571 | * 删除文件,可以删除单个文件或文件夹 572 | * 573 | * @param fileName 被删除的文件名 574 | * @return 如果删除成功,则返回true,否是返回false 575 | */ 576 | public static boolean delFile(String fileName) { 577 | File file = new File(fileName); 578 | if (!file.exists()) { 579 | logger.debug(fileName + " 文件不存在!"); 580 | return true; 581 | } else { 582 | if (file.isFile()) { 583 | return UtilFile.deleteFile(fileName); 584 | } else { 585 | return UtilFile.deleteDirectory(fileName); 586 | } 587 | } 588 | } 589 | 590 | /** 591 | * 删除单个文件 592 | * 593 | * @param fileName 被删除的文件名 594 | * @return 如果删除成功,则返回true,否则返回false 595 | */ 596 | public static boolean deleteFile(String fileName) { 597 | File file = new File(fileName); 598 | if (file.exists() && file.isFile()) { 599 | if (file.delete()) { 600 | logger.debug("删除文件 " + fileName + " 成功!"); 601 | return true; 602 | } else { 603 | logger.debug("删除文件 " + fileName + " 失败!"); 604 | return false; 605 | } 606 | } else { 607 | logger.debug(fileName + " 文件不存在!"); 608 | return true; 609 | } 610 | } 611 | 612 | /** 613 | * 删除目录及目录下的文件 614 | * 615 | * @param dirName 被删除的目录所在的文件路径 616 | * @return 如果目录删除成功,则返回true,否则返回false 617 | */ 618 | public static boolean deleteDirectory(String dirName) { 619 | String dirNames = dirName; 620 | if (!dirNames.endsWith(File.separator)) { 621 | dirNames = dirNames + File.separator; 622 | } 623 | File dirFile = new File(dirNames); 624 | if (!dirFile.exists() || !dirFile.isDirectory()) { 625 | logger.debug(dirNames + " 目录不存在!"); 626 | return true; 627 | } 628 | boolean flag = true; 629 | // 列出全部文件及子目录 630 | File[] files = dirFile.listFiles(); 631 | for (int i = 0; i < files.length; i++) { 632 | // 删除子文件 633 | if (files[i].isFile()) { 634 | flag = UtilFile.deleteFile(files[i].getAbsolutePath()); 635 | // 如果删除文件失败,则退出循环 636 | if (!flag) { 637 | break; 638 | } 639 | } 640 | // 删除子目录 641 | else if (files[i].isDirectory()) { 642 | flag = UtilFile.deleteDirectory(files[i] 643 | .getAbsolutePath()); 644 | // 如果删除子目录失败,则退出循环 645 | if (!flag) { 646 | break; 647 | } 648 | } 649 | } 650 | 651 | if (!flag) { 652 | logger.debug("删除目录失败!"); 653 | return false; 654 | } 655 | // 删除当前目录 656 | if (dirFile.delete()) { 657 | logger.debug("删除目录 " + dirName + " 成功!"); 658 | return true; 659 | } else { 660 | logger.debug("删除目录 " + dirName + " 失败!"); 661 | return false; 662 | } 663 | 664 | } 665 | 666 | 667 | /** 668 | ******************************创建相关***************************** 669 | */ 670 | /** 671 | * 创建单个文件 672 | * 673 | * @param descFileName 文件名,包含路径 674 | * @return 如果创建成功,则返回true,否则返回false 675 | */ 676 | public static boolean createFile(String descFileName) { 677 | File file = new File(descFileName); 678 | if (file.exists()) { 679 | logger.debug("文件 " + descFileName + " 已存在!"); 680 | return false; 681 | } 682 | if (descFileName.endsWith(File.separator)) { 683 | logger.debug(descFileName + " 为目录,不能创建目录!"); 684 | return false; 685 | } 686 | if (!file.getParentFile().exists()) { 687 | // 如果文件所在的目录不存在,则创建目录 688 | if (!file.getParentFile().mkdirs()) { 689 | logger.debug("创建文件所在的目录失败!"); 690 | return false; 691 | } 692 | } 693 | 694 | // 创建文件 695 | try { 696 | if (file.createNewFile()) { 697 | logger.debug(descFileName + " 文件创建成功!"); 698 | return true; 699 | } else { 700 | logger.debug(descFileName + " 文件创建失败!"); 701 | return false; 702 | } 703 | } catch (Exception e) { 704 | e.printStackTrace(); 705 | logger.debug(descFileName + " 文件创建失败!"); 706 | return false; 707 | } 708 | } 709 | 710 | 711 | public static void writeFileByUTF8String(String filePathAndName, String buf) { 712 | try { 713 | writeFileByByte(filePathAndName, buf.getBytes("UTF-8")); 714 | } catch (UnsupportedEncodingException e) { 715 | e.printStackTrace(); 716 | throw ExceptionHelper.unchecked(e); 717 | } 718 | } 719 | 720 | public static void writeFileByByte(String filePathAndName, byte[] buf) { 721 | //创建文件 722 | createFile(filePathAndName); 723 | // 724 | BufferedOutputStream bos = null; 725 | FileOutputStream fos = null; 726 | File file = null; 727 | try { 728 | file = new File(filePathAndName); 729 | fos = new FileOutputStream(file); 730 | bos = new BufferedOutputStream(fos); 731 | bos.write(buf); 732 | } catch (Exception e) { 733 | e.printStackTrace(); 734 | } finally { 735 | if (bos != null) { 736 | IOUtils.closeQuietly(bos); 737 | } 738 | if (fos != null) { 739 | IOUtils.closeQuietly(fos); 740 | } 741 | } 742 | } 743 | 744 | public static void writeFileByUTF8String(String filePath, String fileName, String buf) { 745 | try { 746 | writeFileByByte(filePath, fileName, buf.getBytes("UTF-8")); 747 | } catch (UnsupportedEncodingException e) { 748 | e.printStackTrace(); 749 | throw ExceptionHelper.unchecked(e); 750 | } 751 | } 752 | 753 | public static void writeFileByByte(String filePath, String fileName, byte[] buf) { 754 | BufferedOutputStream bos = null; 755 | FileOutputStream fos = null; 756 | File file = null; 757 | try { 758 | File dir = new File(filePath); 759 | if (!dir.exists() && dir.isDirectory()) { 760 | dir.mkdirs(); 761 | } 762 | //创建文件 763 | createFile(filePath + File.separator + fileName); 764 | // 765 | file = new File(filePath + File.separator + fileName); 766 | fos = new FileOutputStream(file); 767 | bos = new BufferedOutputStream(fos); 768 | bos.write(buf); 769 | } catch (Exception e) { 770 | e.printStackTrace(); 771 | } finally { 772 | if (bos != null) { 773 | IOUtils.closeQuietly(bos); 774 | } 775 | if (fos != null) { 776 | IOUtils.closeQuietly(fos); 777 | } 778 | } 779 | } 780 | 781 | /** 782 | * 创建目录 783 | * 784 | * @param descDirName 目录名,包含路径 785 | * @return 如果创建成功,则返回true,否则返回false 786 | */ 787 | public static boolean createDirectory(String descDirName) { 788 | String descDirNames = descDirName; 789 | if (!descDirNames.endsWith(File.separator)) { 790 | descDirNames = descDirNames + File.separator; 791 | } 792 | File descDir = new File(descDirNames); 793 | if (descDir.exists()) { 794 | logger.debug("目录 " + descDirNames + " 已存在!"); 795 | return false; 796 | } 797 | // 创建目录 798 | if (descDir.mkdirs()) { 799 | logger.debug("目录 " + descDirNames + " 创建成功!"); 800 | return true; 801 | } else { 802 | logger.debug("目录 " + descDirNames + " 创建失败!"); 803 | return false; 804 | } 805 | 806 | } 807 | 808 | 809 | //=================================下载相关================================= 810 | 811 | /** 812 | * 向浏览器发送文件下载,支持断点续传 813 | * 814 | * @param file 要下载的文件 815 | * @param request 请求对象 816 | * @param response 响应对象 817 | * @return 返回错误信息,无错误信息返回null 818 | */ 819 | public static String downFile(File file, HttpServletRequest request, HttpServletResponse response) { 820 | return downFile(file, request, response, null); 821 | } 822 | 823 | /** 824 | * 向浏览器发送文件下载,支持断点续传 825 | * 826 | * @param file 要下载的文件 827 | * @param request 请求对象 828 | * @param response 响应对象 829 | * @param fileName 指定下载的文件名 830 | * @return 返回错误信息,无错误信息返回null 831 | */ 832 | public static String downFile(File file, HttpServletRequest request, HttpServletResponse response, String fileName) { 833 | String error = null; 834 | if (file != null && file.exists()) { 835 | if (file.isFile()) { 836 | if (file.length() <= 0) { 837 | error = "该文件是一个空文件。"; 838 | } 839 | if (!file.canRead()) { 840 | error = "该文件没有读取权限。"; 841 | } 842 | } else { 843 | error = "该文件是一个文件夹。"; 844 | } 845 | } else { 846 | error = "文件已丢失或不存在!"; 847 | } 848 | if (error != null) { 849 | logger.debug("---------------" + file + " " + error); 850 | return error; 851 | } 852 | 853 | long fileLength = file.length(); // 记录文件大小 854 | long pastLength = 0; // 记录已下载文件大小 855 | int rangeSwitch = 0; // 0:从头开始的全文下载;1:从某字节开始的下载(bytes=27000-);2:从某字节开始到某字节结束的下载(bytes=27000-39000) 856 | long toLength = 0; // 记录客户端需要下载的字节段的最后一个字节偏移量(比如bytes=27000-39000,则这个值是为39000) 857 | long contentLength = 0; // 客户端请求的字节总量 858 | String rangeBytes = ""; // 记录客户端传来的形如“bytes=27000-”或者“bytes=27000-39000”的内容 859 | RandomAccessFile raf = null; // 负责读取数据 860 | OutputStream os = null; // 写出数据 861 | OutputStream out = null; // 缓冲 862 | byte b[] = new byte[1024]; // 暂存容器 863 | 864 | if (request.getHeader("Range") != null) { // 客户端请求的下载的文件块的开始字节 865 | response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); 866 | logger.debug("request.getHeader(\"Range\") = " + request.getHeader("Range")); 867 | rangeBytes = request.getHeader("Range").replaceAll("bytes=", ""); 868 | if (rangeBytes.indexOf('-') == rangeBytes.length() - 1) {// bytes=969998336- 869 | rangeSwitch = 1; 870 | rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-')); 871 | pastLength = Long.parseLong(rangeBytes.trim()); 872 | contentLength = fileLength - pastLength; // 客户端请求的是 969998336 之后的字节 873 | } else { // bytes=1275856879-1275877358 874 | rangeSwitch = 2; 875 | String temp0 = rangeBytes.substring(0, rangeBytes.indexOf('-')); 876 | String temp2 = rangeBytes.substring(rangeBytes.indexOf('-') + 1, rangeBytes.length()); 877 | pastLength = Long.parseLong(temp0.trim()); // bytes=1275856879-1275877358,从第 1275856879 个字节开始下载 878 | toLength = Long.parseLong(temp2); // bytes=1275856879-1275877358,到第 1275877358 个字节结束 879 | contentLength = toLength - pastLength; // 客户端请求的是 1275856879-1275877358 之间的字节 880 | } 881 | } else { // 从开始进行下载 882 | contentLength = fileLength; // 客户端要求全文下载 883 | } 884 | 885 | // 如果设置了Content-Length,则客户端会自动进行多线程下载。如果不希望支持多线程,则不要设置这个参数。 响应的格式是: 886 | // Content-Length: [文件的总大小] - [客户端请求的下载的文件块的开始字节] 887 | // ServletActionContext.getResponse().setHeader("Content- Length", new Long(file.length() - p).toString()); 888 | response.reset(); // 告诉客户端允许断点续传多线程连接下载,响应的格式是:Accept-Ranges: bytes 889 | if (pastLength != 0) { 890 | response.setHeader("Accept-Ranges", "bytes");// 如果是第一次下,还没有断点续传,状态是默认的 200,无需显式设置;响应的格式是:HTTP/1.1 200 OK 891 | // 不是从最开始下载, 响应的格式是: Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小] 892 | logger.debug("---------------不是从开始进行下载!服务器即将开始断点续传..."); 893 | switch (rangeSwitch) { 894 | case 1: { // 针对 bytes=27000- 的请求 895 | String contentRange = new StringBuffer("bytes ").append(new Long(pastLength).toString()).append("-") 896 | .append(new Long(fileLength - 1).toString()).append("/").append(new Long(fileLength).toString()).toString(); 897 | response.setHeader("Content-Range", contentRange); 898 | break; 899 | } 900 | case 2: { // 针对 bytes=27000-39000 的请求 901 | String contentRange = rangeBytes + "/" + new Long(fileLength).toString(); 902 | response.setHeader("Content-Range", contentRange); 903 | break; 904 | } 905 | default: { 906 | break; 907 | } 908 | } 909 | } else { 910 | // 是从开始下载 911 | logger.debug("---------------是从开始进行下载!"); 912 | } 913 | 914 | try { 915 | response.addHeader("Content-Disposition", "attachment; filename=\"" + 916 | UtilString.urlEncode(UtilString.isBlank(fileName) ? file.getName() : fileName) + "\""); 917 | response.setContentType(getContentType(file.getName())); // set the MIME type. 918 | response.addHeader("Content-Length", String.valueOf(contentLength)); 919 | os = response.getOutputStream(); 920 | out = new BufferedOutputStream(os); 921 | raf = new RandomAccessFile(file, "r"); 922 | try { 923 | switch (rangeSwitch) { 924 | case 0: { // 普通下载,或者从头开始的下载 同1 925 | } 926 | case 1: { // 针对 bytes=27000- 的请求 927 | raf.seek(pastLength); // 形如 bytes=969998336- 的客户端请求,跳过 969998336 个字节 928 | int n = 0; 929 | while ((n = raf.read(b, 0, 1024)) != -1) { 930 | out.write(b, 0, n); 931 | } 932 | break; 933 | } 934 | case 2: { // 针对 bytes=27000-39000 的请求 935 | raf.seek(pastLength); // 形如 bytes=1275856879-1275877358 的客户端请求,找到第 1275856879 个字节 936 | int n = 0; 937 | long readLength = 0; // 记录已读字节数 938 | while (readLength <= contentLength - 1024) {// 大部分字节在这里读取 939 | n = raf.read(b, 0, 1024); 940 | readLength += 1024; 941 | out.write(b, 0, n); 942 | } 943 | if (readLength <= contentLength) { // 余下的不足 1024 个字节在这里读取 944 | n = raf.read(b, 0, (int) (contentLength - readLength)); 945 | out.write(b, 0, n); 946 | } 947 | break; 948 | } 949 | default: { 950 | break; 951 | } 952 | } 953 | out.flush(); 954 | logger.debug("---------------下载完成!"); 955 | } catch (IOException ie) { 956 | /** 957 | * 在写数据的时候, 对于 ClientAbortException 之类的异常, 958 | * 是因为客户端取消了下载,而服务器端继续向浏览器写入数据时, 抛出这个异常,这个是正常的。 959 | * 尤其是对于迅雷这种吸血的客户端软件, 明明已经有一个线程在读取 bytes=1275856879-1275877358, 960 | * 如果短时间内没有读取完毕,迅雷会再启第二个、第三个。。。线程来读取相同的字节段, 直到有一个线程读取完毕,迅雷会 KILL 961 | * 掉其他正在下载同一字节段的线程, 强行中止字节读出,造成服务器抛 ClientAbortException。 962 | * 所以,我们忽略这种异常 963 | */ 964 | logger.warn("提醒:向客户端传输时出现IO异常,但此异常是允许的,有可能客户端取消了下载,导致此异常,不用关心!"); 965 | } 966 | } catch (Exception e) { 967 | logger.error(e.getMessage(), e); 968 | } finally { 969 | if (out != null) { 970 | try { 971 | out.close(); 972 | } catch (IOException e) { 973 | logger.error(e.getMessage(), e); 974 | } 975 | } 976 | if (raf != null) { 977 | try { 978 | raf.close(); 979 | } catch (IOException e) { 980 | logger.error(e.getMessage(), e); 981 | } 982 | } 983 | } 984 | return null; 985 | } 986 | 987 | /** 988 | * byte[] 转 InputStream 数组转数据流 989 | * 990 | * @param byteData 数组 991 | * @return 992 | */ 993 | public static InputStream byte2Input(byte[] byteData) { 994 | if (byteData.length > 0) { 995 | return new ByteArrayInputStream(byteData); 996 | } 997 | logger.info("byte数组转InputStream 流异常,传入数组为空"); 998 | return null; 999 | } 1000 | 1001 | /** 1002 | * InputStream流转byte数组 1003 | * 1004 | * @param inStream 输入流 1005 | * @return 数组 1006 | * @throws IOException 1007 | */ 1008 | public static byte[] input2byte(InputStream inStream) 1009 | throws IOException { 1010 | if (Objects.isNull(inStream)) { 1011 | return new byte[0]; 1012 | } 1013 | ByteArrayOutputStream swapStream = new ByteArrayOutputStream(); 1014 | byte[] buff = new byte[100]; 1015 | int rc = 0; 1016 | while ((rc = inStream.read(buff, 0, 100)) > 0) { 1017 | swapStream.write(buff, 0, rc); 1018 | } 1019 | return swapStream.toByteArray(); 1020 | } 1021 | } -------------------------------------------------------------------------------- /src/main/java/com/example/demo/util/UtilNet.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.util; 2 | 3 | import java.io.IOException; 4 | import java.net.*; 5 | import java.util.Enumeration; 6 | import java.util.Random; 7 | import java.util.regex.Pattern; 8 | 9 | /** 10 | * @author zoubin02 on 7/23/14. 11 | */ 12 | public class UtilNet { 13 | 14 | public static final String LOCALHOST = "127.0.0.1"; 15 | public static final String ANYHOST = "0.0.0.0"; 16 | 17 | private static final int RND_PORT_START = 30000; 18 | private static final int RND_PORT_RANGE = 10000; 19 | 20 | private static final Random RANDOM = new Random(System.currentTimeMillis()); 21 | private static final int MIN_PORT = 0; 22 | private static final int MAX_PORT = 65535; 23 | private static final Pattern ADDRESS_PATTERN = Pattern.compile("^\\d{1,3}(\\.\\d{1,3}){3}\\:\\d{1,5}$"); 24 | private static final Pattern LOCAL_IP_PATTERN = Pattern.compile("127(\\.\\d{1,3}){3}$"); 25 | private static final Pattern IP_PATTERN = Pattern.compile("\\d{1,3}(\\.\\d{1,3}){3,5}$"); 26 | private static volatile InetAddress LOCAL_ADDRESS = null; 27 | 28 | private UtilNet() { 29 | } 30 | 31 | public static int getRandomPort() { 32 | return RND_PORT_START + RANDOM.nextInt(RND_PORT_RANGE); 33 | } 34 | 35 | public static int getAvailablePort() { 36 | ServerSocket ss = null; 37 | try { 38 | ss = new ServerSocket(); 39 | ss.bind(null); 40 | return ss.getLocalPort(); 41 | } catch (IOException e) { 42 | return getRandomPort(); 43 | } finally { 44 | if (ss != null) { 45 | try { 46 | ss.close(); 47 | } catch (IOException e) { 48 | } 49 | } 50 | } 51 | } 52 | 53 | public static int getAvailablePort(int port) { 54 | if (port <= 0) { 55 | return getAvailablePort(); 56 | } 57 | for (int i = port; i < MAX_PORT; i++) { 58 | ServerSocket ss = null; 59 | try { 60 | ss = new ServerSocket(i); 61 | return i; 62 | } catch (IOException e) { 63 | // continue 64 | } finally { 65 | if (ss != null) { 66 | try { 67 | ss.close(); 68 | } catch (IOException e) { 69 | } 70 | } 71 | } 72 | } 73 | return port; 74 | } 75 | 76 | public static boolean isInvalidPort(int port) { 77 | return port > MIN_PORT || port <= MAX_PORT; 78 | } 79 | 80 | public static boolean isValidAddress(String address) { 81 | return ADDRESS_PATTERN.matcher(address).matches(); 82 | } 83 | 84 | public static boolean isLocalHost(String host) { 85 | return host != null 86 | && (LOCAL_IP_PATTERN.matcher(host).matches() 87 | || host.equalsIgnoreCase("localhost")); 88 | } 89 | 90 | public static boolean isAnyHost(String host) { 91 | return "0.0.0.0".equals(host); 92 | } 93 | 94 | public static boolean isInvalidLocalHost(String host) { 95 | return host == null 96 | || host.length() == 0 97 | || host.equalsIgnoreCase("localhost") 98 | || host.equals("0.0.0.0") 99 | || (LOCAL_IP_PATTERN.matcher(host).matches()); 100 | } 101 | 102 | public static boolean isValidLocalHost(String host) { 103 | return !isInvalidLocalHost(host); 104 | } 105 | 106 | public static InetSocketAddress getLocalSocketAddress(String host, int port) { 107 | return isInvalidLocalHost(host) ? 108 | new InetSocketAddress(port) : new InetSocketAddress(host, port); 109 | } 110 | 111 | private static boolean isValidAddress(InetAddress address) { 112 | if (address == null || address.isLoopbackAddress()) 113 | return false; 114 | String name = address.getHostAddress(); 115 | return (name != null 116 | && !ANYHOST.equals(name) 117 | && !LOCALHOST.equals(name) 118 | && IP_PATTERN.matcher(name).matches()); 119 | } 120 | 121 | public static boolean isValidHost(String host){ 122 | return IP_PATTERN.matcher(host).matches(); 123 | } 124 | 125 | public static String getLocalHost() { 126 | InetAddress address = getLocalAddress(); 127 | String result = address == null ? LOCALHOST : address.getHostAddress(); 128 | if("0:0:0:0:0:0:0:1".equalsIgnoreCase(result)){ 129 | return LOCALHOST; 130 | } 131 | return result; 132 | } 133 | 134 | public static String getLocalHostName(){ 135 | InetAddress address = getLocalAddress(); 136 | if(address == null){ 137 | return "localhost"; 138 | } 139 | return address.getHostName(); 140 | } 141 | 142 | /** 143 | * 遍历本地网卡,返回第一个合理的IP。 144 | * 145 | * @return 本地网卡IP 146 | */ 147 | public static InetAddress getLocalAddress() { 148 | if (LOCAL_ADDRESS != null) 149 | return LOCAL_ADDRESS; 150 | InetAddress localAddress = getLocalAddress0(); 151 | LOCAL_ADDRESS = localAddress; 152 | return localAddress; 153 | } 154 | 155 | private static InetAddress getLocalAddress0() { 156 | InetAddress localAddress = null; 157 | try { 158 | localAddress = InetAddress.getLocalHost(); 159 | if (isValidAddress(localAddress)) { 160 | return localAddress; 161 | } 162 | } catch (Exception e) { 163 | // logger.warn("Failed to retriving ip address, " + e.getMessage(), e); 164 | } 165 | try { 166 | Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); 167 | if (interfaces != null) { 168 | while (interfaces.hasMoreElements()) { 169 | try { 170 | NetworkInterface network = interfaces.nextElement(); 171 | if (network.isLoopback() || network.isVirtual() || !network.isUp()) { 172 | continue; 173 | } 174 | Enumeration addresses = network.getInetAddresses(); 175 | while (addresses.hasMoreElements()) { 176 | try { 177 | InetAddress address = addresses.nextElement(); 178 | if (isValidAddress(address)) { 179 | return address; 180 | } 181 | } catch (Exception e) { 182 | // logger.warn("Failed to retriving ip address, " + e.getMessage(), e); 183 | } 184 | } 185 | } catch (Exception e) { 186 | // logger.warn("Failed to retriving ip address, " + e.getMessage(), e); 187 | } 188 | } 189 | } 190 | } catch (Exception e) { 191 | // logger.warn("Failed to retriving ip address, " + e.getMessage(), e); 192 | } 193 | // logger.error("Could not get local host ip address, will use 127.0.0.1 instead."); 194 | return localAddress; 195 | } 196 | 197 | 198 | /** 199 | * @param hostName 200 | * @return ip address or hostName if UnknownHostException 201 | */ 202 | public static String getIpByHost(String hostName) { 203 | try { 204 | return InetAddress.getByName(hostName).getHostAddress(); 205 | } catch (UnknownHostException e) { 206 | return hostName; 207 | } 208 | } 209 | 210 | public static String toAddressString(InetSocketAddress address) { 211 | return address.getAddress().getHostAddress() + ":" + address.getPort(); 212 | } 213 | 214 | public static InetSocketAddress toAddress(String address) { 215 | int i = address.indexOf(':'); 216 | String host; 217 | int port; 218 | if (i > -1) { 219 | host = address.substring(0, i); 220 | port = Integer.parseInt(address.substring(i + 1)); 221 | } else { 222 | host = address; 223 | port = 0; 224 | } 225 | return new InetSocketAddress(host, port); 226 | } 227 | 228 | public static String toURL(String protocol, String host, int port, String path) { 229 | StringBuilder sb = new StringBuilder(); 230 | sb.append(protocol).append("://"); 231 | sb.append(host).append(':').append(port); 232 | if (path.charAt(0) != '/') 233 | sb.append('/'); 234 | sb.append(path); 235 | return sb.toString(); 236 | } 237 | 238 | } 239 | -------------------------------------------------------------------------------- /src/main/java/com/example/demo/util/UtilString.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.util; 2 | 3 | import org.apache.commons.lang3.StringEscapeUtils; 4 | import org.slf4j.helpers.FormattingTuple; 5 | import org.slf4j.helpers.MessageFormatter; 6 | 7 | import java.io.UnsupportedEncodingException; 8 | import java.net.URLEncoder; 9 | import java.util.Collection; 10 | import java.util.Map; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | 15 | /** 16 | * UtilString 17 | * 18 | * @author wuchenl 19 | */ 20 | 21 | public class UtilString extends org.apache.commons.lang3.StringUtils { 22 | 23 | 24 | //编译后的正则表达式缓存 25 | private static final Map PATTERN_CACHE = new ConcurrentHashMap<>(); 26 | private static final char SEPARATOR = '_'; 27 | private static final String CHARSET_NAME = "UTF-8"; 28 | 29 | 30 | 31 | public static String defaultIfNull(String object, String defaultValue) { 32 | return object == null ? defaultValue : object; 33 | } 34 | 35 | 36 | /** 37 | * 编译一个正则表达式,并且进行缓存,如果换成已存在则使用缓存 38 | * 39 | * @param regex 表达式 40 | * @return 编译后的Pattern 41 | */ 42 | public static final Pattern compileRegex(String regex) { 43 | Pattern pattern = PATTERN_CACHE.get(regex); 44 | if (pattern == null) { 45 | pattern = Pattern.compile(regex); 46 | PATTERN_CACHE.put(regex, pattern); 47 | } 48 | return pattern; 49 | } 50 | 51 | /** 52 | * 对象是否为无效值 53 | * 54 | * @param obj 要判断的对象 55 | * @return 是否为有效值(不为null 和 "" 字符串) 56 | */ 57 | public static boolean isEmptyObject(Object obj) { 58 | return obj == null || "".equals(obj.toString()); 59 | } 60 | 61 | 62 | 63 | 64 | 65 | 66 | /** 67 | * 对象是否为true 68 | * 69 | * @param obj 70 | * @return 71 | */ 72 | public static boolean isTrue(Object obj) { 73 | return "true".equals(String.valueOf(obj)); 74 | } 75 | 76 | 77 | /** 78 | * 参数是否是有效数字 (整数或者小数) 79 | * 80 | * @param obj 参数(对象将被调用string()转为字符串类型) 81 | * @return 是否是数字 82 | */ 83 | public static boolean isNumber(Object obj) { 84 | if (obj instanceof Number) { 85 | return true; 86 | } 87 | return isInt(obj) || isDouble(obj); 88 | } 89 | 90 | /** 91 | * 参数是否是有效整数 92 | * 93 | * @param obj 参数(对象将被调用string()转为字符串类型) 94 | * @return 是否是整数 95 | */ 96 | public static boolean isInt(Object obj) { 97 | if (isEmptyObject(obj)) { 98 | return false; 99 | } 100 | if (obj instanceof Integer) { 101 | return true; 102 | } 103 | return obj.toString().matches("[-+]?\\d+"); 104 | } 105 | 106 | /** 107 | * 字符串参数是否是double 108 | * 109 | * @param obj 参数(对象将被调用string()转为字符串类型) 110 | * @return 是否是double 111 | */ 112 | public static boolean isDouble(Object obj) { 113 | if (isEmptyObject(obj)) { 114 | return false; 115 | } 116 | if (obj instanceof Double || obj instanceof Float) { 117 | return true; 118 | } 119 | return compileRegex("[-+]?\\d+\\.\\d+").matcher(obj.toString()).matches(); 120 | } 121 | 122 | /** 123 | * 判断一个对象是否为boolean类型,包括字符串中的true和false 124 | * 125 | * @param obj 要判断的对象 126 | * @return 是否是一个boolean类型 127 | */ 128 | public static boolean isBoolean(Object obj) { 129 | if (obj instanceof Boolean) { 130 | return true; 131 | } 132 | String strVal = String.valueOf(obj); 133 | return "true".equalsIgnoreCase(strVal) || "false".equalsIgnoreCase(strVal); 134 | } 135 | 136 | /** 137 | * 将对象转为int值,如果对象无法进行转换,则使用默认值 138 | * 139 | * @param object 要转换的对象 140 | * @param defaultValue 默认值 141 | * @return 转换后的值 142 | */ 143 | public static int toInt(Object object, int defaultValue) { 144 | if (object instanceof Number) { 145 | return ((Number) object).intValue(); 146 | } 147 | if (isInt(object)) { 148 | return Integer.parseInt(object.toString()); 149 | } 150 | if (isDouble(object)) { 151 | return (int) Double.parseDouble(object.toString()); 152 | } 153 | return defaultValue; 154 | } 155 | 156 | /** 157 | * 将对象转为int值,如果对象不能转为,将返回0 158 | * 159 | * @param object 要转换的对象 160 | * @return 转换后的值 161 | */ 162 | public static int toInt(Object object) { 163 | return toInt(object, 0); 164 | } 165 | 166 | /** 167 | * 将对象转为long类型,如果对象无法转换,将返回默认值 168 | * 169 | * @param object 要转换的对象 170 | * @param defaultValue 默认值 171 | * @return 转换后的值 172 | */ 173 | public static long toLong(Object object, long defaultValue) { 174 | if (object instanceof Number) { 175 | return ((Number) object).longValue(); 176 | } 177 | if (isInt(object)) { 178 | return Long.parseLong(object.toString()); 179 | } 180 | if (isDouble(object)) { 181 | return (long) Double.parseDouble(object.toString()); 182 | } 183 | return defaultValue; 184 | } 185 | 186 | /** 187 | * 将对象转为 long值,如果无法转换,则转为0 188 | * 189 | * @param object 要转换的对象 190 | * @return 转换后的值 191 | */ 192 | public static long toLong(Object object) { 193 | return toLong(object, 0); 194 | } 195 | 196 | /** 197 | * 将对象转为Double,如果对象无法转换,将使用默认值 198 | * 199 | * @param object 要转换的对象 200 | * @param defaultValue 默认值 201 | * @return 转换后的值 202 | */ 203 | public static double toDouble(Object object, double defaultValue) { 204 | if (object instanceof Number) { 205 | return ((Number) object).doubleValue(); 206 | } 207 | if (isNumber(object)) { 208 | return Double.parseDouble(object.toString()); 209 | } 210 | if (null == object) { 211 | return defaultValue; 212 | } 213 | return 0; 214 | } 215 | 216 | /** 217 | * 将对象转为Double,如果对象无法转换,将使用默认值0 218 | * 219 | * @param object 要转换的对象 220 | * @return 转换后的值 221 | */ 222 | public static double toDouble(Object object) { 223 | return toDouble(object, 0); 224 | } 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | /** 234 | * 字符串连接,将参数列表拼接为一个字符串 235 | * 236 | * @return 返回拼接后的字符串 237 | */ 238 | public static String concatWithMarkAndSplit(String mark , String split , Collection more) { 239 | StringBuilder buf = new StringBuilder(); 240 | if(more==null || more.isEmpty()){ 241 | return buf.toString(); 242 | } 243 | int i = 0; 244 | for (Object obj : more) { 245 | if (i != 0) { 246 | buf.append(mark).append(obj).append(mark).append(split); 247 | } 248 | buf.append(mark).append(obj).append(mark); 249 | i++; 250 | } 251 | return buf.toString(); 252 | } 253 | 254 | public static String concat(Collection more) { 255 | return concatWithSpilt("", more); 256 | } 257 | 258 | public static String concatWithSpilt(String split , Collection more) { 259 | StringBuilder buf = new StringBuilder(); 260 | int i = 0; 261 | for (Object obj : more) { 262 | if (i != 0) { 263 | buf.append(split); 264 | } 265 | buf.append(obj); 266 | i++; 267 | } 268 | return buf.toString(); 269 | } 270 | 271 | public static String concat(Object... more) { 272 | return concatWithSpilt("", more); 273 | } 274 | 275 | public static String concatWithSpilt(String split , Object... more) { 276 | StringBuilder buf = new StringBuilder(); 277 | for (int i = 0; i < more.length; i++) { 278 | if (i != 0) { 279 | buf.append(split); 280 | } 281 | buf.append(more[i]); 282 | } 283 | return buf.toString(); 284 | } 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | /** 293 | * 驼峰命名法工具 294 | * @return 295 | * toCamelCase("hello_world") == "helloWorld" 296 | * toCapitalizeCamelCase("hello_world") == "HelloWorld" 297 | * toUnderScoreCase("helloWorld") = "hello_world" 298 | */ 299 | public static String toCamelCase(String s) { 300 | if (s == null) { 301 | return null; 302 | } 303 | 304 | s = s.toLowerCase(); 305 | 306 | StringBuilder sb = new StringBuilder(s.length()); 307 | boolean upperCase = false; 308 | for (int i = 0; i < s.length(); i++) { 309 | char c = s.charAt(i); 310 | 311 | if (c == SEPARATOR) { 312 | upperCase = true; 313 | } else if (upperCase) { 314 | sb.append(Character.toUpperCase(c)); 315 | upperCase = false; 316 | } else { 317 | sb.append(c); 318 | } 319 | } 320 | 321 | return sb.toString(); 322 | } 323 | 324 | /** 325 | * 驼峰命名法工具 326 | * @return 327 | * toCamelCase("hello_world") == "helloWorld" 328 | * toCapitalizeCamelCase("hello_world") == "HelloWorld" 329 | * toUnderScoreCase("helloWorld") = "hello_world" 330 | */ 331 | public static String toCapitalizeCamelCase(String s) { 332 | if (s == null) { 333 | return null; 334 | } 335 | s = toCamelCase(s); 336 | return s.substring(0, 1).toUpperCase() + s.substring(1); 337 | } 338 | 339 | /** 340 | * 驼峰命名法工具 341 | * @return 342 | * toCamelCase("hello_world") == "helloWorld" 343 | * toCapitalizeCamelCase("hello_world") == "HelloWorld" 344 | * toUnderScoreCase("helloWorld") = "hello_world" 345 | */ 346 | public static String toUnderScoreCase(String s) { 347 | if (s == null) { 348 | return null; 349 | } 350 | 351 | StringBuilder sb = new StringBuilder(); 352 | boolean upperCase = false; 353 | for (int i = 0; i < s.length(); i++) { 354 | char c = s.charAt(i); 355 | 356 | boolean nextUpperCase = true; 357 | 358 | if (i < (s.length() - 1)) { 359 | nextUpperCase = Character.isUpperCase(s.charAt(i + 1)); 360 | } 361 | 362 | if ((i > 0) && Character.isUpperCase(c)) { 363 | if (!upperCase || !nextUpperCase) { 364 | sb.append(SEPARATOR); 365 | } 366 | upperCase = true; 367 | } else { 368 | upperCase = false; 369 | } 370 | 371 | sb.append(Character.toLowerCase(c)); 372 | } 373 | 374 | return sb.toString(); 375 | } 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | /** 385 | * 缩略字符串(不区分中英文字符) 386 | * @param str 目标字符串 387 | * @param length 截取长度 388 | * @return 389 | */ 390 | public static String abbr(String str, int length) { 391 | if (str == null) { 392 | return ""; 393 | } 394 | try { 395 | StringBuilder sb = new StringBuilder(); 396 | int currentLength = 0; 397 | for (char c : replaceHtml(StringEscapeUtils.unescapeHtml4(str)).toCharArray()) { 398 | currentLength += String.valueOf(c).getBytes("GBK").length; 399 | if (currentLength <= length - 3) { 400 | sb.append(c); 401 | } else { 402 | sb.append("..."); 403 | break; 404 | } 405 | } 406 | return sb.toString(); 407 | } catch (UnsupportedEncodingException e) { 408 | e.printStackTrace(); 409 | } 410 | return ""; 411 | } 412 | 413 | 414 | 415 | /** 416 | * 替换掉HTML标签方法 417 | */ 418 | public static String replaceHtml(String html) { 419 | if (isBlank(html)){ 420 | return ""; 421 | } 422 | String regEx = "<.+?>"; 423 | Pattern p = Pattern.compile(regEx); 424 | Matcher m = p.matcher(html); 425 | String s = m.replaceAll(""); 426 | return s; 427 | } 428 | 429 | 430 | 431 | /** 432 | * 转换为JS获取对象值,生成三目运算返回结果 433 | * @param objectString 对象串 434 | * 例如:row.user.id 435 | * 返回:!row?'':!row.user?'':!row.user.id?'':row.user.id 436 | */ 437 | public static String javascriptGetValueStyle(String objectString){ 438 | StringBuilder result = new StringBuilder(); 439 | StringBuilder val = new StringBuilder(); 440 | String[] vals = split(objectString, "."); 441 | for (int i=0; i getParameters(HttpServletRequest request) { 45 | Map parameters = new HashMap<>(); 46 | Enumeration enumeration = request.getParameterNames(); 47 | while (enumeration.hasMoreElements()) { 48 | String name = String.valueOf(enumeration.nextElement()); 49 | parameters.put(name, request.getParameter(name)); 50 | } 51 | return parameters; 52 | } 53 | 54 | public static Map getHeaders(HttpServletRequest request) { 55 | Map map = new LinkedHashMap<>(); 56 | Enumeration enumeration = request.getHeaderNames(); 57 | while (enumeration.hasMoreElements()) { 58 | String key = enumeration.nextElement(); 59 | String value = request.getHeader(key); 60 | map.put(key, value); 61 | } 62 | return map; 63 | } 64 | 65 | 66 | //获取请求客户端的真实ip地址 67 | public static String getIpAddr(HttpServletRequest request) { 68 | String ip = request.getHeader("x-forwarded-for"); 69 | 70 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 71 | ip = request.getHeader("x-forwarded-host"); 72 | } 73 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 74 | ip = request.getHeader("X-Real-IP"); 75 | } 76 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 77 | ip = request.getHeader("Proxy-Client-IP"); 78 | } 79 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 80 | ip = request.getHeader("WL-Proxy-Client-IP"); 81 | } 82 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 83 | ip = request.getRemoteAddr(); 84 | } 85 | if("0:0:0:0:0:0:0:1".equalsIgnoreCase(ip)){ 86 | return UtilNet.LOCALHOST; 87 | } 88 | return ip; 89 | } 90 | 91 | /** 92 | * 获取请求体内容 93 | */ 94 | public static String getRequestPayload(HttpServletRequest request) { 95 | StringBuilder sb = new StringBuilder(); 96 | BufferedReader reader = null; 97 | try { 98 | request.setCharacterEncoding("utf8"); 99 | reader = request.getReader(); 100 | //做标记为了reset 101 | reader.mark(0); 102 | char[] buff = new char[1024]; 103 | int len; 104 | while ((len = reader.read(buff)) != -1) { 105 | sb.append(buff, 0, len); 106 | } 107 | } catch (IOException e) { 108 | logger.error(e.getMessage()); 109 | }finally{ 110 | if (reader!=null){ 111 | try { 112 | reader.reset(); 113 | } catch (IOException e) { 114 | logger.error(e.getMessage()); 115 | } 116 | } 117 | } 118 | return sb.toString(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | servlet: 4 | context-path: /captcha 5 | spring: 6 | mvc: 7 | view: 8 | prefix: /jsp/ 9 | suffix: .jsp 10 | cache: 11 | type: caffeine 12 | cacheNames: 13 | verificationCode: 600 14 | # 生成的图片缓存时间 30S,图片生成完以后就会立刻会消费掉 30S周期已经够长。或者考虑改为使用后即移除。 15 | captchaImage: 30 16 | com: 17 | letters7: 18 | wuchen: 19 | captcha: 20 | source: 21 | path: static/img/source/ 22 | size: 5 -------------------------------------------------------------------------------- /src/main/resources/static/css/captcha.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0px; 3 | margin: 0px; 4 | } 5 | 6 | .yzm { 7 | width: 260px; 8 | height: 120px; 9 | position: relative; 10 | margin-left: 80px; 11 | } 12 | 13 | #drag { 14 | position: relative; 15 | background-color: #e8e8e8; 16 | width: 300px; 17 | height: 34px; 18 | line-height: 34px; 19 | margin-left: 80px; 20 | text-align: center; 21 | } 22 | 23 | #drag .handler { 24 | position: absolute; 25 | top: 0px; 26 | left: 0px; 27 | width: 40px; 28 | height: 32px; 29 | border: 1px solid #ccc; 30 | cursor: move; 31 | } 32 | 33 | .handler_bg { 34 | background: #fff url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0ZDhlNWY5My05NmI0LTRlNWQtOGFjYi03ZTY4OGYyMTU2ZTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NTEyNTVEMURGMkVFMTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTEyNTVEMUNGMkVFMTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2MTc5NzNmZS02OTQxLTQyOTYtYTIwNi02NDI2YTNkOWU5YmUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NGQ4ZTVmOTMtOTZiNC00ZTVkLThhY2ItN2U2ODhmMjE1NmU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+YiRG4AAAALFJREFUeNpi/P//PwMlgImBQkA9A+bOnfsIiBOxKcInh+yCaCDuByoswaIOpxwjciACFegBqZ1AvBSIS5OTk/8TkmNEjwWgQiUgtQuIjwAxUF3yX3xyGIEIFLwHpKyAWB+I1xGSwxULIGf9A7mQkBwTlhBXAFLHgPgqEAcTkmNCU6AL9d8WII4HOvk3ITkWJAXWUMlOoGQHmsE45ViQ2KuBuASoYC4Wf+OUYxz6mQkgwAAN9mIrUReCXgAAAABJRU5ErkJggg==") no-repeat center; 35 | } 36 | 37 | .handler_ok_bg { 38 | background: #fff url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0ZDhlNWY5My05NmI0LTRlNWQtOGFjYi03ZTY4OGYyMTU2ZTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDlBRDI3NjVGMkQ2MTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDlBRDI3NjRGMkQ2MTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDphNWEzMWNhMC1hYmViLTQxNWEtYTEwZS04Y2U5NzRlN2Q4YTEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NGQ4ZTVmOTMtOTZiNC00ZTVkLThhY2ItN2U2ODhmMjE1NmU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+k+sHwwAAASZJREFUeNpi/P//PwMyKD8uZw+kUoDYEYgloMIvgHg/EM/ptHx0EFk9I8wAoEZ+IDUPiIMY8IN1QJwENOgj3ACo5gNAbMBAHLgAxA4gQ5igAnNJ0MwAVTsX7IKyY7L2UNuJAf+AmAmJ78AEDTBiwGYg5gbifCSxFCZoaBMCy4A4GOjnH0D6DpK4IxNSVIHAfSDOAeLraJrjgJp/AwPbHMhejiQnwYRmUzNQ4VQgDQqXK0ia/0I17wJiPmQNTNBEAgMlQIWiQA2vgWw7QppBekGxsAjIiEUSBNnsBDWEAY9mEFgMMgBk00E0iZtA7AHEctDQ58MRuA6wlLgGFMoMpIG1QFeGwAIxGZo8GUhIysmwQGSAZgwHaEZhICIzOaBkJkqyM0CAAQDGx279Jf50AAAAAABJRU5ErkJggg==") no-repeat center; 39 | } 40 | 41 | .handler_err_bg { 42 | background: #fff url("../img/erricon.png"); 43 | } 44 | 45 | #drag .drag_bg { 46 | background-color: #7ac23c; 47 | height: 34px; 48 | width: 0px; 49 | } 50 | 51 | #drag .drag_err { 52 | background-color: darkred; 53 | height: 34px; 54 | width: 0px; 55 | } 56 | 57 | #drag .drag_text { 58 | position: absolute; 59 | top: 0px; 60 | width: 300px; 61 | -moz-user-select: none; 62 | -webkit-user-select: none; 63 | user-select: none; 64 | -o-user-select: none; 65 | -ms-user-select: none; 66 | } 67 | 68 | .yzm_image_source { 69 | float: left; 70 | width: 260px; 71 | height: 113px; 72 | margin: 0 !important; 73 | border: 0px; 74 | padding: 0 !important; 75 | background-image: url("../img/source/0.png"); 76 | } 77 | 78 | .yzm_image_source:before { 79 | content: ""; 80 | position: absolute; 81 | width: 100px; 82 | height: 100%; 83 | top: 0; 84 | left: -150px; 85 | overflow: hidden; 86 | background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, 0) 100%); 87 | background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), 88 | color-stop(50%, rgba(255, 255, 255, .2)), color-stop(100%, rgba(255, 255, 255, 0))); 89 | background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, 0) 100%); 90 | background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 0, 0) 100%); 91 | -webkit-transform: skewX(-25deg); 92 | -moz-transform: skewX(-25deg) 93 | } 94 | 95 | .run:before { 96 | left: 70%; 97 | transition: left 2s ease 0s; 98 | } 99 | 100 | .yzm_image_cut_big { 101 | float: left; 102 | width: 260px; 103 | height: 113px; 104 | margin: 0 !important; 105 | border: 0px; 106 | padding: 0 !important; 107 | } 108 | 109 | .yzm_image_cut_loading { 110 | float: left; 111 | width: 260px; 112 | height: 113px; 113 | margin: 0 !important; 114 | border: 0px; 115 | padding: 0 !important; 116 | background-image: url("../img/loading.png"); 117 | } 118 | 119 | #xy_img { 120 | z-index: 999; 121 | width: 60px; 122 | height: 60px; 123 | position: relative; 124 | } -------------------------------------------------------------------------------- /src/main/resources/static/img/erricon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuchenl/java-JigsawVerification/5e4305ad3069582c46c1666ae1f099497b5b8ad7/src/main/resources/static/img/erricon.png -------------------------------------------------------------------------------- /src/main/resources/static/img/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuchenl/java-JigsawVerification/5e4305ad3069582c46c1666ae1f099497b5b8ad7/src/main/resources/static/img/loading.png -------------------------------------------------------------------------------- /src/main/resources/static/img/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuchenl/java-JigsawVerification/5e4305ad3069582c46c1666ae1f099497b5b8ad7/src/main/resources/static/img/refresh.png -------------------------------------------------------------------------------- /src/main/resources/static/img/source/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuchenl/java-JigsawVerification/5e4305ad3069582c46c1666ae1f099497b5b8ad7/src/main/resources/static/img/source/0.png -------------------------------------------------------------------------------- /src/main/resources/static/img/source/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuchenl/java-JigsawVerification/5e4305ad3069582c46c1666ae1f099497b5b8ad7/src/main/resources/static/img/source/1.png -------------------------------------------------------------------------------- /src/main/resources/static/img/source/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuchenl/java-JigsawVerification/5e4305ad3069582c46c1666ae1f099497b5b8ad7/src/main/resources/static/img/source/2.png -------------------------------------------------------------------------------- /src/main/resources/static/img/source/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuchenl/java-JigsawVerification/5e4305ad3069582c46c1666ae1f099497b5b8ad7/src/main/resources/static/img/source/3.png -------------------------------------------------------------------------------- /src/main/resources/static/img/source/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuchenl/java-JigsawVerification/5e4305ad3069582c46c1666ae1f099497b5b8ad7/src/main/resources/static/img/source/4.png -------------------------------------------------------------------------------- /src/main/resources/static/js/drag.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | $.fn.drag = function (options, sucfun, errfun) { 3 | var isvalid = false; 4 | var x, drag = this, isMove = false, defaults = {}; 5 | var options = $.extend(defaults, options); 6 | //添加背景,文字,滑块 7 | var html = '
' + 8 | '
拖动图片验证登陆
' + 9 | '
'; 10 | this.append(html); 11 | 12 | var handler = drag.find('.handler'); 13 | var drag_bg = drag.find('.drag_bg'); 14 | var text = drag.find('.drag_text'); 15 | var maxWidth = drag.width() - handler.width(); //能滑动的最大间距 16 | 17 | //鼠标按下时候的x轴的位置 18 | handler.mousedown(function (e) { 19 | if (isvalid) { 20 | return false; 21 | } 22 | $gt_cut.css("display", "none"); 23 | $gt_cut_hidden.show(); 24 | $xy_img.show(); 25 | isMove = true; 26 | x = e.pageX - parseInt(handler.css('left'), 10); 27 | }); 28 | var $xy_img = $("#xy_img"); 29 | var $gt_cut = $("#yzm_image_source"); 30 | var $gt_cut_hidden = $("#yzm_image_cut_big"); 31 | //鼠标指针在上下文移动时,移动距离大于0小于最大间距,滑块x轴位置等于鼠标移动距离 32 | $("#drag").mousemove(function (e) { 33 | if (isvalid) { 34 | return false; 35 | } 36 | var _x = e.pageX - x; 37 | if (isMove) { 38 | if (_x > 0 && _x <= maxWidth) { 39 | $xy_img.css({'left': _x}); 40 | handler.css({'left': _x}); 41 | drag_bg.css({'width': _x}); 42 | } else if (_x > maxWidth) { //鼠标指针移动距离达到最大时清空事件 43 | // dragOk(); 44 | } 45 | } 46 | }).mouseup(function (e) { 47 | if (isvalid) { 48 | return false; 49 | } 50 | isMove = false; 51 | var _x = e.pageX - x; 52 | console.log(_x); 53 | $.ajax({ 54 | type: "POST", 55 | url: "captcha/checkCaptcha", 56 | dataType: "JSON", 57 | async: false, 58 | data: {point: _x}, 59 | success: function (result) { 60 | console.log(result); 61 | if (result.code == 200) { 62 | dragOk(_x); 63 | } else { 64 | dragErr(); 65 | } 66 | }, 67 | error: function (err) { 68 | console.log(err); 69 | $.ligerDialog.error('服务异常!'); 70 | } 71 | }); 72 | }); 73 | 74 | var errcount = 0; 75 | 76 | function dragErr() { 77 | errcount = errcount + 1; 78 | isvalid = false; 79 | $(".drag_bg").css("background-color", "#C22A0E"); 80 | text.text("验证失败"); 81 | handler.removeClass('handler_bg').addClass('handler_err_bg'); 82 | setTimeout(function () { 83 | //还原 84 | text.text("拖动图片验证登陆"); 85 | $(".drag_bg").css("background-color", "#7ac23c"); 86 | handler.removeClass("handler_err_bg").addClass("handler_bg"); 87 | // $xy_img.css("display", "none"); 88 | $xy_img.css({'left': 0}); 89 | handler.css({'left': 0}); 90 | drag_bg.css({'width': 0}); 91 | //验证失败, 拖动错误数大于3次,那么就重置 92 | if (errcount >= 3) { 93 | errfun(); 94 | errcount = 0; 95 | $xy_img.css("display", "none"); 96 | $gt_cut.show(); 97 | $gt_cut_hidden.css("display", "none"); 98 | } 99 | }, 1000); 100 | } 101 | 102 | //清空事件 103 | function dragOk(_x) { 104 | isvalid = true; 105 | handler.removeClass('handler_bg').addClass('handler_ok_bg'); 106 | text.text('验证通过'); 107 | drag.css({'color': '#fff'}); 108 | handler.unbind('mousedown'); 109 | $(document).unbind('mousemove'); 110 | $(document).unbind('mouseup'); 111 | //验证通过,隐藏刷新 112 | $("#refreshyzm").css("display", "none"); 113 | $("#passcheck").val("1"); 114 | $("#yzm").val(_x); 115 | 116 | //显示原始图片,并做光线闪过 117 | $xy_img.css("display", "none"); 118 | $gt_cut_hidden.css("display", "none"); 119 | $gt_cut.show(); 120 | $gt_cut.addClass("run"); 121 | } 122 | }; 123 | })(jQuery); 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/main/webapp/jsp/login.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 2 | 3 | 4 | 登1录 5 | 6 | 7 | 8 | 47 | 48 | 49 |
50 | 51 | 52 |
53 | 54 | 55 |
56 |
57 | 58 | 59 |
60 |
61 | 62 |
63 | 64 |
65 | 66 |
67 | 68 |
69 | 70 |
71 | 73 | 76 |
77 |
78 |
79 | 80 |
81 | 82 |
83 |
84 | 85 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/test/java/com/example/demo/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class DemoApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------