背景:老大做了一个 SDK,包含字符加解密、文件加解密,要求能从前端访问,并且能演示的 Demo。
思路:html 写页面,js 发送请求,freemarker 做简单的参数交互,JAVA 后端处理。
一、项目依赖
● java17
● springboot 3.1.2
● 模板技术:spring-boot-starter-freemarker 2.3.32
● 工具包:commons-io 2.16.0
● pom 文件:
xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.xxx.xx.x</groupId>
<artifactId>xx-x</artifactId>
<version>3.0-SNAPSHOT</version>
</parent>
<artifactId>x-sdk-service</artifactId>
<modelVersion>4.0.0</modelVersion>
<description>sdk 项目服务</description>
<packaging>jar</packaging>
<name>sdk-service</name>
<properties>
<jdk.version>17</jdk.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.1.2</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
<!-- SpringBoot 拦截器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Spring框架基本的核心工具 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.8</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 前端渲染视图技术 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.16.1</version>
</dependency>
</dependencies>
</project>
二、前端代码
使用的模板技术,JS + Freemark。"${baseUrl}"指向的是后端配置的IP地址,可改为静态的,但是那样html页面就写死了。文件放在项目:resource/templates下,无则需要创建。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>演示页</title>
</head>
<script>
// 访问地址
const baseUrl = "${baseUrl}"
/**
* 加密
*/
function getEnc() {
// 处理传入数据
let data = document.getElementById("encText").value;
data = btoa(data);
// 组装请求
const oReq = new XMLHttpRequest();
const url = baseUrl + "/enc?data=" + data;
console.log("request " + url)
oReq.open("GET", url, true);
// 响应类型
oReq.responseType = "text";
oReq.onprogress = function (result) {
console.log(result)
// 失败处理
if (fail(result)) {
return;
}
// 处理响应
const data = result.currentTarget.response;
let parse = JSON.parse(data);
const text = parse.data;
document.getElementById("decText").value = String(text);
};
oReq.send();
}
/**
* 解密
*/
function getDec() {
let data = document.getElementById("decText").value;
const oReq = new XMLHttpRequest();
data = btoa(data);
const url = baseUrl + "/dec?data=" + data;
console.log("request " + url)
oReq.open("GET", url, true);
// 响应类型
oReq.responseType = "text";
oReq.onprogress = function (result) {
console.log(result)
if (fail(result)) {
return;
}
const data = result.currentTarget.response;
let parse = JSON.parse(data);
const text = parse.data;
document.getElementById("encText").value = String(atob(text));
};
oReq.send();
}
/**
* 文件加密
*/
function getEncFile() {
const fileInput = document.getElementById('encFile');
const file = fileInput.files[0];
let formData = new FormData();
formData.append("file", file)
const oReq = new XMLHttpRequest();
const url = baseUrl + "/encFile";
console.log("request " + url)
oReq.open("POST", url, true);
// 响应类型
oReq.onprogress = function (result) {
console.log(result)
if (fail(result)) {
return;
}
const data = result.currentTarget.response;
let parse = JSON.parse(data);
const fileName = parse.data;
// 下载文件
downloadFile(baseUrl + "/downLoadFile/" + fileName, fileName)
// 原组件置空
fileInput.value = '';
};
oReq.send(formData);
}
/**
* 文件解密
*/
function getDecFile() {
const fileInput = document.getElementById('decFile');
const file = fileInput.files[0];
let formData = new FormData();
formData.append("file", file)
const oReq = new XMLHttpRequest();
const url = baseUrl + "/decFile";
console.log("request " + url)
oReq.open("POST", url, true);
// 响应类型
oReq.onload = function (result) {
console.log(result)
if (fail(result)) {
console.log("encFile:" + result)
return;
}
const data = result.currentTarget.response;
let parse = JSON.parse(data);
const fileName = parse.data;
downloadFile(baseUrl + "/downLoadFile/" + fileName, fileName)
fileInput.value = '';
};
oReq.send(formData);
}
/**
* 下载文件
* @param url
* @param fileName
*/
function downloadFile(url, fileName) {
console.log("downloadFile:" + url)
console.log("downloadFile fileName:" + fileName)
fetch(url)
.then(response => response.blob())
.then(blob => {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.target = "_blank"; // 可选,如果希望在新窗口中下载文件,请取消注释此行
link.click();
});
}
/**
* 请求是否失败
* @param result 请求
* @returns {boolean} true 失败
*/
function fail(result) {
const data = result.currentTarget.response;
let parse = JSON.parse(data);
if (200 !== parse.code) {
alert("请求失败:" + parse.msg)
return true;
}
return false;
}
</script>
<body style="text-align: center;margin-top: auto">
<h1>加密工具演示页</h1>
<!-- 文本加密 -->
<div style="margin-top: 48px">
<h3>文本加密</h3>
<div>
<input id="encText" type="text" style="width: 200px;"/>
<button onclick="getEnc();" style="margin-left: 12px">加密</button>
</div>
<div style="margin-top: 12px">
<input id="decText" type="text" style="width: 200px;"/>
<button onclick="getDec();" style="margin-left: 12px">解密</button>
</div>
</div>
<!-- 文件加密 -->
<div style="margin-top: 48px;">
<h3>文件加解密</h3>
<div style="margin-top: 24px">
文件加密:
<input id="encFile" type="file" onchange="getEncFile()"/>
</div>
<div style="margin-top: 24px;">
文件解密:
<input id="decFile" type="file" onchange="getDecFile()"/>
</div>
</div>
</body>
</html>
三、后端配置
application配置文件
yaml
server:
port: 12321
tmv:
baseUrl: http://localhost:12321
baseDir: ./config/
# application.yml
spring:
servlet:
multipart:
max-file-size: 10000MB
max-request-size: 10000MB
#配置freemarker
freemarker:
#指定HttpServletRequest的属性是否可以覆盖controller的model的同名项
allow-request-override: false
#req访问request
request-context-attribute: req
#后缀名freemarker默认后缀为.ftl,当然你也可以改成自己习惯的.html
suffix: .ftl
#设置响应的内容类型
content-type: text/html;charset=utf-8
#是否允许mvc使用freemarker
enabled: true
#是否开启template caching
cache: false
#设定模板的加载路径,多个以逗号分隔,默认: ["classpath:/templates/"]
template-loader-path: classpath:/templates/
#设定Template的编码
charset: UTF-8
# 设置静态文件路径,js,css等
mvc:
static-path-pattern: /static/**
四、后端代码
java
@RestController
@RequestMapping
@Slf4j
public class EncryptController {
@Value("${tmv.baseUrl:http://localhost:12321}")
String baseUrl;
@Value("${tmv.baseDir:D:\\temp\\temp\\}")
String baseDir;
private static KmService kmService;
/**
* 初始化 SDK
*/
@PostConstruct
public void init() {
try {
log.info("load kmService start, baseDir: {}", baseDir);
kmService = KmService.getInstance(baseDir);
log.info("load kmService success");
} catch (Exception e) {
log.error("SDK加载失败,请检查");
log.error("load kmService error", e);
}
}
@RequestMapping("/")
public ModelAndView index() {
// 跳转到index.ftl 模板
ModelAndView mv = new ModelAndView("index");
// 添加属性
mv.addObject("baseUrl", baseUrl);
return mv;
}
@GetMapping("enc")
public TResult enc(@RequestParam String data) {
log.info("enc request: {}", data);
String result;
try {
result = kmService.enc(data, null);
} catch (Exception e) {
log.error("enc error", e);
result = e.getMessage();
}
log.info("enc resp: {}", result);
return TResult.success(result);
}
@GetMapping("dec")
public TResult dec(@RequestParam String data) {
log.info("dec : {}", data);
// 需要转码
byte[] bytes = Base64.decodeBase64(data);
log.info("dec : {}", data);
String result;
try {
result = kmService.dec(new String(bytes), null);
} catch (Exception e) {
log.error("dec error", e);
result = e.getMessage();
}
log.info("dec resp: {}", result);
return TResult.success(result);
}
@PostMapping("encFile")
public TResult encFile(@RequestParam MultipartFile file) {
log.info("fileName request: {}", file.getOriginalFilename());
long size = file.getSize();
log.info("file size: {} bit {} kb", size, size / 1000);
String result;
try {
// 创建临时文件
File tempFile = File.createTempFile(System.currentTimeMillis() + "", "");
byte[] bytes = file.getBytes();
FileUtils.copyInputStreamToFile(new ByteArrayInputStream(bytes), tempFile);
File destFile = new File(baseDir + UUIDGenerator.getUUID() + ".enc.txt", "");
// 加密
boolean flag = kmService.fileEncrypt(tempFile.getAbsolutePath(), destFile.getAbsolutePath());
if (flag) {
result = destFile.getName();
} else {
throw new RuntimeException("处理失败,请检查输入信息是否有误");
}
} catch (Exception e) {
log.error("encFile error", e);
return TResult.fail(e.getMessage());
}
log.info("enc resp: {}", result);
return TResult.success(result);
}
@PostMapping("decFile")
public TResult decFile(@RequestParam MultipartFile file) {
log.info("decFile fileName request: {}", file.getOriginalFilename());
long size = file.getSize();
log.info("file size: {} bit {} kb", size, size / 1000);
String result;
try {
// 创建临时文件
File tempFile = File.createTempFile(System.currentTimeMillis() + "", "");
byte[] bytes = file.getBytes();
FileUtils.copyInputStreamToFile(new ByteArrayInputStream(bytes), tempFile);
File destFile = new File(baseDir + UUIDGenerator.getUUID() + ".dec.txt", "");
// 加密
boolean flag = kmService.fileDecrypt(tempFile.getAbsolutePath(), destFile.getAbsolutePath());
if (flag) {
result = destFile.getName();
} else {
throw new RuntimeException("处理失败,请检查输入信息是否有误");
}
} catch (Exception e) {
log.error("decFile error", e);
return TResult.fail(e.getMessage());
}
log.info("enc resp: {}", result);
return TResult.success(result);
}
@GetMapping("downLoadFile/{fileName}")
public void downLoadFile(HttpServletResponse response, @PathVariable("fileName") String fileName) throws IOException {
log.info("downLoadFile fileName request: {}", fileName);
String filePath = baseDir + fileName;
log.info("filePath :{}", filePath);
File file = new File(filePath);
try {
copy2respond(response, file);
} catch (Exception e) {
log.error("decFile error", e);
}
}
/**
* 文件转下载流
*
* @param response 响应
* @param file 文件
* @throws IOException 异常
*/
private static void copy2respond(HttpServletResponse response, File file) throws IOException {
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
FileInputStream fileInputStream = new FileInputStream(file);
OutputStream out = response.getOutputStream();
byte[] buffer = new byte[4096];
int length;
while ((length = fileInputStream.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
out.flush();
fileInputStream.close();
}
}
响应对象
java
@Data
public class TResult implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "响应码")
@Getter
private int code;
@Schema(description = "响应消息")
@Getter
private String msg;
@Schema(description = "响应数据")
@Getter
private Object data;
public static TResult success(Object data){
TResult tResult = new TResult();
tResult.setCode(200);
tResult.setMsg("OK");
tResult.setData(data);
return tResult;
}
public static TResult fail(String messag){
TResult tResult = new TResult();
tResult.setCode(500);
tResult.setMsg(messag);
tResult.setData(null);
return tResult;
}
}
五、页面效果
最后,作为一个Javaer,会点前端是OK的。但,技不外露,最好不要随便接前端需求。还好最终老大放了一条生路,否则笔者大概率要学习深入理解Html + css + JavaScript了。
(因为笔者觉得只是两个输入框所以自告奋勇的接下来,谁知道后面又要加远程访问、文件加解密、UI优化、全局异常捕获、人性化提示...差点翻车)