日志输出-第四章-接口级(单体应用)前后端数据加解密 Filter 实现

文章目录

日志输出-第四章-接口级(单体应用)前后端数据加解密 Filter 实现

前置内容

  1. 日志输出指南
  2. 日志输出-第二章-接口级出入参的实现
  3. 日志输出-第三章-接口级出入参输出完整数据的实现

一、概述

上一章内容为如何输出完整数据,但是一般情况下还是会采用第二章的实现方式(因为输出 body 会影响性能),上一章的处理方式实际上更多是用于处理前后端数据的加解密。

本章的内容实际上并不属于 日志输出的范围 而是对上一章的内容进行了衍生(因为日志输出是需要在流量的出入口做处理,前后端数据加解密也是在流量的出入口做处理,并且都是对 body 数据处理),是 SpringBoot 项目如何处理前后端数据加解密问题(SpringCloud 版本的加解密会在后续日志写到相应版本后再更新)。

一般情况下的做法分为两种:

  1. 通过 Filter 的方式,也就是和我们上一章的日志输出一样,只是将输出日志改为对数据进行加解密就可以了。
  2. 通过 SpringRequestBodyAdviceResponseBodyAdvice 来实现。

两种的实现难度都差不多,唯一的区别在于 通过 Spring 的这种方式好像是只能实现 POST 这类请求的拦截(我只是大概试了一下)。

二、通过 Filter 的方式实现

这一部分除了输出之外,最大的区别在于 Response 的包装类,因为上一章的内容在于如何输出日志,所以我们实际上只需要在响应中写数据的时候写两份就可以做到了。

java 复制代码
/**
     * @Param
     * @Return
     * @Description 内部类,对 ServletOutputStream 进行包装,方便日志记录
     * @Author lizelin
     * @Date 2023/11/3 16:43
     **/
    private class ResponseWrapperOutputStream extends ServletOutputStream {
        private ByteArrayOutputStream bos = null;
        private HttpServletResponse response = null;

        public ResponseWrapperOutputStream(ByteArrayOutputStream stream, HttpServletResponse response) {
            bos = stream;
            this.response = response;
        }

        /**
         * @Param b
         * @Return void
         * @Description 将写暂存一份方便日志
         * @Author lizelin
         * @Date 2023/11/3 17:31
         **/
        @Override
        public void write(int b) throws IOException {
            bos.write(b);
            response.getOutputStream().write(b);
        }

        /**
         * @Param b
         * @Return void
         * @Description 将写暂存一份方便日志
         * @Author lizelin
         * @Date 2023/11/3 17:32
         **/
        @Override
        public void write(byte[] b) throws IOException {
            bos.write(b, 0, b.length);
            response.getOutputStream().write(b, 0, b.length);
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setWriteListener(WriteListener writeListener) {
            //不需要重新
        }
    }

也就是上面代码中的 write 部分,ByteArrayOutputStream 的作用在于我们自己输出日志使用,但是向客户端响应的部分实际上还是走的 response.getOutputStream().write(b); 也就是说实际上我们上一章的内容只是在原有的响应逻辑上做了一个旁路逻辑用于输出日志。

2.1、加解密工具类

一共有两组密钥,后端响应加密、前端响应解密、后端请求解密、前端请求加密

java 复制代码
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.asymmetric.SM2;

/**
 * @ClassName ParametersSecureUtil
 * @Author lizelin
 * @Description 参数加密 util
 * @Date 2024/5/29 14:50
 * @Version 1.0
 */
public class ParametersSecureUtil {
    //后端持有私钥解密
    private static String REQ_REAR_PRIVATE_KEY = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALLu5fiTx0RbbCMVFaiAx9LSiddTQT58ab07I6LtlIem8J2Q8C+1P6m+8w81T3kPz8WfpQM+7npdv3FhQmrRggj37Lm5+8Q7OMMCXG3va6kpRr9xGd4TjyuJBi8AGX58MnojruiQCwhxjngyJggmHZYlAB61A3OMq1Bi2ExBbkGnAgMBAAECgYAGJKGMmTY8KI9b3PtzX4h8unG1DMyuooLW1lLw4ws4ZQjZwAIfATAAWefqW8AwvdQ6SrLVm7GATfumntoy5KJ8MaF86pfTcGsuqpYZXwcjAHJ4sikKPZYUPn+BaEDcMIBBx8QkVSd0okV2m0bwou6nbVoorjkLCzdQrlXSpmeeUQJBAN8BL8QFYed+BLqyYMiZicGjuRRKGe4QUeMjpLDys0WC4HXCQjozbk1t9LL63GzC7BkMrH/BIReqIv9S7i6ff/UCQQDNaGeXNwGWj2JfsrQMmBe3HReVuwNV7bBlD2EmKT8csZ3F05t+JMR/XaBP44ApZGiCjfPfDAUPBNR0TYEXVAWrAkEAx5SLSDa8+W36E5CjN8TZ2fiKIpNzA3GNp+f1c/ux38sS0bFKjkYLOLbooeoLrjcBECYcl7WjxUcaTUHOMuHCpQJAYocOCY6tCFdGzLiffNsHpSIjSgMmmnUlA5TY+MEYMN9R2q6iC2P/jUiPuUJbG3+6UcVdkUPmuUmLzy3OGi6HeQJBAJOZmEtgHMCfqirahDbrDIbRojw7qNDSX2bbIPEmiFs8iEN+JR8ULUwDBRvmhj6a2IddfPGLOEK5xepiuco2BGs=";
    //前端持有公钥加密 这个实际上是给前端用的,后端不需要使用
    private static String REQ_FRONT_PUBLICE_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCy7uX4k8dEW2wjFRWogMfS0onXU0E+fGm9OyOi7ZSHpvCdkPAvtT+pvvMPNU95D8/Fn6UDPu56Xb9xYUJq0YII9+y5ufvEOzjDAlxt72upKUa/cRneE48riQYvABl+fDJ6I67okAsIcY54MiYIJh2WJQAetQNzjKtQYthMQW5BpwIDAQAB";
    //后端持有公钥加密
    private static String RES_REAR_PUBLICE_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9AeIxSwTTLmtgCOMsCpytG+SdX5PjC0jOjuIbY4wd61rVNemjqJNldBrrJ6ldF+t+5GXB/O0IevAL47At5WltTcWrOGEpSJssHDaVmya5E/yyDDP+3PPlvH6KR1SdgH8fppipjWRFYU5/ke+EQLTmrNxFqvqniUlEPl/63TyuqQIDAQAB";
    //前端持有私钥解密
    private static String RES_FRONT_PRIVATE_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL0B4jFLBNMua2AI4ywKnK0b5J1fk+MLSM6O4htjjB3rWtU16aOok2V0GusnqV0X637kZcH87Qh68AvjsC3laW1Nxas4YSlImywcNpWbJrkT/LIMM/7c8+W8fopHVJ2Afx+mmKmNZEVhTn+R74RAtOas3EWq+qeJSUQ+X/rdPK6pAgMBAAECgYBr+qR/5szl3TIo1kr6gUGLQFE2e0Egx/SbVVPls9R7z1bAUiGdhxRWNKOgTrNaZOz8PH3J+rZsTtfO4xBm2BaHCTmCShOVl6RG/qeNC1A1S2nmpkckvS4XfH7DVs0IILaEVnHIYLSUd8oiP/nJ+Hppn6Sj7cUGSwb2itYx1YtpUQJBAN6dRipcvKHpQkvodFfkT1m6XkwgrMuiIvTmqWYvKTXUwZ7uvyYIjB42O7DziCKoBFUualED/g69ft5fhGDepFUCQQDZWlovo0RSjfUyzuh1VcI9mQu3gLSgfUbJKgeOD+435jHFhIXiKKIdaN1L1R1MLggCbaZGyq0rqS6D9GykRpUFAkEA1DJKXbsEO7ni7gRoUhdY5AjYNey3iWvFsnfkZXjy6VMiNOMS5agkF/BOOcAJti894gxaX1tU4qwSsNmPj97p+QJAC7vW9o9n1tUXEaEd54ezrsOeYE+wcKGSurVsJv0xLQ9eTH11BNqQtem9WKSuqjgp8oec3GGAq8S8YB9H5i5xSQJALd09O7Hv0fZRn8yI09qQ2KCB0CpIHrXHjGI1I/TR72k/DlTJOOIKe6LnkecXF21xiMOq0aqhs0Ol5U2FIXkkzw==";
    /**
     * 请求
     */
    private static RSA reqRsa = SecureUtil.rsa(REQ_REAR_PRIVATE_KEY, REQ_FRONT_PUBLICE_KEY);
    /**
     * 响应
     */
    private static RSA resRsa = SecureUtil.rsa(RES_FRONT_PRIVATE_KEY, RES_REAR_PUBLICE_KEY);

    /**
     * @Param encryptedData
     * @Return java.lang.String
     * @Description 请求解密
     * @Author lizelin
     * @Date 2024/5/29 15:43
     **/
    public static String requestDecrypt(String encryptedData) {
        return  reqRsa.decryptStr(encryptedData, KeyType.PrivateKey);
    }
    /**
     * @Param encryptedData
     * @Return java.lang.String
     * @Description 请求内容加密,todo 测试用
     * @Author lizelin
     * @Date 2024/5/29 15:52
     **/
    public static String requestEncrypt(String encryptedData) {
        return  reqRsa.encryptHex(encryptedData, KeyType.PublicKey);
    }
    /**
     * @Param encryptedData
     * @Return java.lang.String
     * @Description 响应加密
     * @Author lizelin
     * @Date 2024/5/29 15:45
     **/
    public static String responseEncrypt(String encryptedData) {
        return resRsa.encryptHex(encryptedData, KeyType.PublicKey);
    }

    /**
     * @Param encryptedData
     * @Return java.lang.String
     * @Description 响应内容解密 todo 测试用
     * @Author lizelin
     * @Date 2024/5/29 15:52
     **/
    public static String responseDecrypt(String encryptedData) {
        return resRsa.decryptStr(encryptedData, KeyType.PrivateKey);
    }

    public static void main(String[] args) {
//        String str = "{\n" +
//                "    \"createBy\":\"lzl\",\n" +
//                "    \"content\":\"大厦春,你要干什么\"\n" +
//                "}";
        String str = "95b06517952572ccd3cb645991658bcfee0cc71a465b454fa2db6cd814c2ff72e69130c334105d4303fc6378f2c0720a7e24f1c1d19f366840dc75bfa858833df7860373070b8586b42127cd489b419ac0093da7936d984c65a4b8d2b8dc1697eb3d239b7446258d4eaabaf5341e92ab2d4cb25f8da3c571c165c35e635fa1db";

        System.out.println(responseDecrypt(str));
    }

}

2.2、请求包装类

这个实际上与上一章的代码一致

java 复制代码
import cn.hutool.core.collection.CollUtil;
import org.apache.catalina.util.ParameterMap;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Map;

/**
 * @ClassName LogHttpServletRequestWrapper
 * @Author lizelin
 * @Description 日志 Http Servlet 请求包装器
 * @Date 2024/5/27 18:26
 * @Version 1.0
 */
public class LogHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 所有参数的 Map 集合
     */
    private Map<String, String[]> parameterMap;

    /**
     * 存储 body 数据的容器(这里存储为解析流后的JSON)
     */
    private String body;

    /**
     * @Param
     * @Return java.lang.String
     * @Description 获取Body
     * @Author lizelin
     * @Date 2023/11/3 16:09
     **/
    public String getBody() {
        return this.body;
    }

    /**
     * @Param body
     * @Return void
     * @Description 修改 body
     * @Author lizelin
     * @Date 2023/11/3 16:09
     **/
    public void setBody(String body) {
        this.body = body;
    }


    public LogHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
        // 给参数集合赋值
        parameterMap = request.getParameterMap();
        // 获取Body
        body = RequestResponseUtil.getRequestBody(request);

    }

    /**
     * @Param parameterMap
     * @Return void
     * @Description 替换整个参数 Map
     * @Author lizelin
     * @Date 2023/11/3 14:59
     **/
    public void setParameterMap(Map<String, String[]> parameterMap) {
        this.parameterMap = parameterMap;
    }

    /**
     * @Param key
     * @Param value
     * @Return void
     * @Description 向参数集合中添加参数
     * @Author lizelin
     * @Date 2023/11/3 14:59
     **/
    public void putParameterMap(String key, String[] value) {
        if (this.parameterMap instanceof ParameterMap) {
            ((ParameterMap<String, String[]>) this.parameterMap).setLocked(false);
        }
        this.parameterMap.put(key, value);
    }

    /**
     * @Param
     * @Return java.util.Enumeration<java.lang.String>
     * @Description 获取所有参数名
     * @Author lizelin
     * @Date 2023/11/3 14:59
     **/
    @Override
    public Enumeration<String> getParameterNames() {
        return CollUtil.asEnumeration(parameterMap.keySet().iterator());
    }

    /**
     * @Param name
     * @Return java.lang.String
     * @Description 获取指定参数名的值,如果有重复的参数名,则返回第一个的值 接收一般变量 ,如 text 类型
     * @Author lizelin
     * @Date 2023/11/3 14:59
     **/
    @Override
    public String getParameter(String name) {
        ArrayList<String> values = CollUtil.toList(parameterMap.get(name));
        if (CollUtil.isNotEmpty(values)) {
            return values.get(0);
        } else {
            return null;
        }
    }

    /**
     * @Param name
     * @Return java.lang.String[]
     * @Description 获取单个的某个 key 的 value
     * @Author lizelin
     * @Date 2023/11/3 14:58
     **/
    @Override
    public String[] getParameterValues(String name) {
        return parameterMap.get(name);
    }

    /**
     * @Param
     * @Return java.util.Map<java.lang.String, java.lang.String [ ]>
     * @Description 获取值列表
     * @Author lizelin
     * @Date 2023/11/3 14:58
     **/
    @Override
    public Map<String, String[]> getParameterMap() {
        return parameterMap;
    }


    /**
     * @Param
     * @Return java.lang.String
     * @Description 获取 queryString
     * @Author lizelin
     * @Date 2023/11/3 14:57
     **/
    @Override
    public String getQueryString() {
        return super.getQueryString();
    }


    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    /**
     * @Param
     * @Return javax.servlet.ServletInputStream
     * @Description 重写获取输入流,因为在输出日志的时候会读取输入流,而流只能读取一次,所以在向后传递的时候就需要做特殊处理
     * @Author lizelin
     * @Date 2023/11/3 14:55
     **/
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {
                //不需要重写
            }

            @Override
            public int read() {
                //重写读
                return byteArrayInputStream.read();
            }
        };
    }
}

2.3、响应包装类

这里主要的修改点在于去掉了 response.getOutputStream().write(b, 0, b.length) 这些内容。也就是响应包装类其实主要用途在于读取数据,写数据的部分在 Filter 中实现

java 复制代码
import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;

/**
 * @ClassName LogHttpServletResponseWrapper
 * @Author lizelin
 * @Description 日志 Http Servlet 响应 包装器
 * @Date 2024/5/27 18:25
 * @Version 1.0
 */
@Slf4j
public class LogHttpServletResponseWrapper extends HttpServletResponseWrapper {


    private ByteArrayOutputStream byteArrayOutputStream = null;
    private ResponseWrapperOutputStream servletOutputStream = null;
    private PrintWriter printWriter = null;

    public LogHttpServletResponseWrapper(HttpServletResponse response) throws IOException {
        super(response);
        byteArrayOutputStream = new ByteArrayOutputStream();
        servletOutputStream = new ResponseWrapperOutputStream(byteArrayOutputStream);
        printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, this.getCharacterEncoding()));
    }


    /**
     * @Param
     * @Return javax.servlet.ServletOutputStream
     * @Description 获取 OutputStream
     * @Author lizelin
     * @Date 2023/11/3 16:47
     **/
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return servletOutputStream;
    }

    /**
     * @Param
     * @Return java.io.PrintWriter
     * @Description 获取 Writer
     * @Author lizelin
     * @Date 2023/11/3 16:46
     **/
    @Override
    public PrintWriter getWriter() throws UnsupportedEncodingException {
        return printWriter;
    }

    /**
     * @Param
     * @Return void
     * @Description 获取 flushBuffer
     * @Author lizelin
     * @Date 2023/11/3 16:46
     **/
    @Override
    public void flushBuffer() throws IOException {
        if (servletOutputStream != null) {
            servletOutputStream.flush();
        }
        if (printWriter != null) {
            printWriter.flush();
        }
    }

    /**
     * @Param
     * @Return void
     * @Description 重置流
     * @Author lizelin
     * @Date 2023/11/3 16:46
     **/
    @Override
    public void reset() {
        byteArrayOutputStream.reset();
    }

    /**
     * @Param
     * @Return String
     * @Description 读取流中的数据
     * @Author lizelin
     * @Date 2023/11/3 16:43
     **/
    public String getBody() throws IOException {
        //刷新缓冲区
        flushBuffer();
        //读取流中的数据
        return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8);
    }

    public byte[] getBodyBytes() throws IOException {
        //刷新缓冲区
        flushBuffer();
        //读取流中的数据
        return byteArrayOutputStream.toByteArray();
    }

    /**
     * @Param str
     * @Return void
     * @Description 写入数据
     * @Author lizelin
     * @Date 2024/5/29 17:33
     **/
    public void setBody(String str) throws IOException {
        flushBuffer();
        byteArrayOutputStream.reset();
        for (byte item : str.getBytes()) {
            servletOutputStream.write(item);
        }
    }

    /**
     * @Param
     * @Return
     * @Description 内部类,对 ServletOutputStream 进行包装,方便日志记录
     * @Author lizelin
     * @Date 2023/11/3 16:43
     **/
    private class ResponseWrapperOutputStream extends ServletOutputStream {
        private ByteArrayOutputStream bos = null;

        public ResponseWrapperOutputStream(ByteArrayOutputStream stream) {
            bos = stream;
        }

        /**
         * @Param b
         * @Return void
         * @Description 将写暂存一份方便日志
         * @Author lizelin
         * @Date 2023/11/3 17:31
         **/
        @Override
        public void write(int b) throws IOException {
            bos.write(b);
        }
        /**
         * @Param b
         * @Return void
         * @Description 将写暂存一份方便日志
         * @Author lizelin
         * @Date 2023/11/3 17:32
         **/
        @Override
        public void write(byte[] b) throws IOException {
            bos.write(b, 0, b.length);
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setWriteListener(WriteListener writeListener) {
            //不需要重新
        }
    }
}

2.4、实现加解密

这部分的步骤实际上和日志输出的思路基本上一样

java 复制代码
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @ClassName LogHandlerInterceptor
 * @Author lizelin
 * @Description 日志请求入参过滤器
 * @Date 2023/11/3 12:15
 * @Version 1.0
 */
@Slf4j
@Component
@AllArgsConstructor
public class LogRecordRequestFilter implements Filter, Ordered {


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        //这里主要是由于流不可重复读取,采用包装类的方式
        LogHttpServletRequestWrapper requestWrapper = new LogHttpServletRequestWrapper(httpServletRequest);
        String requestWrapperBody = requestWrapper.getBody();
        //数据解密
        requestWrapper.setBody(ParametersSecureUtil.requestDecrypt((String)requestWrapperBody));

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        LogHttpServletResponseWrapper responseWrapper = new LogHttpServletResponseWrapper(httpServletResponse);
        chain.doFilter(requestWrapper, responseWrapper);

        //未加密数据
        String responseWrapperBody = responseWrapper.getBody();
        //数据加密
        responseWrapper.setBody(ParametersSecureUtil.responseEncrypt((String)responseWrapperBody));
        byte[] bodyBytes = responseWrapper.getBodyBytes();
        response.setContentLength(bodyBytes.length);
        //输出加密数据
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(bodyBytes);
        outputStream.flush();
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

2.5、效果展示

请求数据为加密数据

控制台数据为解密数据

响应结果为加密数据

对加密的响应结果进行解密,输出结果为 P2 中希望返回的数据

三、总结

SpringBoot 的前后端加解密内容基本上就完成了,整体比较简单,基本上就是日志的思路。

只是需要注意的是示例中的内容只对请求 body 中的内容进行解密操作。

也就是我没写路径传参加解密,主要是因为如果代码比较严格的话,是不允许 POST 请求的时候带 QueryString 的,然后 GET 请求一般 url 参数也没必要加密,但是不排除奇怪的需求或者是屎山代码的情况。

判断一下请求的 Method 然后获取 QueryString 就能做了

下一章再更新 通过 SpringRequestBodyAdviceResponseBodyAdvice 的实现。

相关推荐
java小吕布7 分钟前
Java集合框架之Collection集合遍历
java
一二小选手9 分钟前
【Java Web】分页查询
java·开发语言
大G哥9 分钟前
python 数据类型----可变数据类型
linux·服务器·开发语言·前端·python
网安-轩逸20 分钟前
【网络安全】身份认证
网络·安全·web安全
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ21 分钟前
idea 弹窗 delete remote branch origin/develop-deploy
java·elasticsearch·intellij-idea
Code成立23 分钟前
《Java核心技术 卷I》用户图形界面鼠标事件
java·开发语言·计算机外设
Xiao Fei Xiangζั͡ޓއއ43 分钟前
一觉睡醒,全世界计算机水平下降100倍,而我却精通C语言——scanf函数
c语言·开发语言·笔记·程序人生·面试·蓝桥杯·学习方法
记录无知岁月1 小时前
【MATLAB】目标检测初探
开发语言·yolo·目标检测·matlab·yolov3·yolov2
鸽鸽程序猿1 小时前
【算法】【优选算法】二分查找算法(下)
java·算法·二分查找算法
远望清一色1 小时前
基于MATLAB身份证号码识别
开发语言·图像处理·算法·matlab