JS+Springboot做一个交互Demo

背景:老大做了一个 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优化、全局异常捕获、人性化提示...差点翻车)

相关推荐
橙露42 分钟前
JavaScript 异步编程:Promise、async/await 从原理到实战
开发语言·javascript·ecmascript
一线大码1 小时前
Java 使用国密算法实现数据加密传输
java·spring boot·后端
我命由我123451 小时前
React Router 6 - 嵌套路由、路由传递参数
前端·javascript·react.js·前端框架·html·ecmascript·js
十六年开源服务商2 小时前
2026年WordPress网站地图完整指南
java·前端·javascript
英俊潇洒美少年3 小时前
MessageChannel 如何实现时间切片
javascript·react.js·ecmascript
技术钱4 小时前
react数据大屏四种适配方案
javascript·react.js·ecmascript
李明卫杭州4 小时前
JavaScript 严格模式下 arguments 的区别
前端·javascript
一次旅行4 小时前
今日心理学知识分享(三)
开发语言·javascript·程序人生·ecmascript
牛十二5 小时前
openclaw安装mcporter搜索小红书
开发语言·javascript·ecmascript
小金鱼Y5 小时前
🔥 前端人必看:浏览器安全核心知识点全解析(XSS/CSRF/DDoS)
前端·javascript·安全