作者归档:Rick

BaseDAOImpl最佳实践:前端JSON/Params后台POJO接收

实体 接收前端参数,然后再持久化,有的时候前端参数可能是需要 结构化 的JSON 这对前端不友好。

下面就以一个例子展现如何优雅地前端扁平化传递JSON让实体类接收。假如书只有一位作者,多个标签。那么,N 书 —> 1 作者, N书 <---> N标签

实体

Book.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_book", comment = "书")
public class Book extends BaseEntity {

    private String title;

    /**
     * 书的拥有者
     */
    @ManyToOne(value = "person_id", parentTable = "t_person")
    private Person person;

    /**
     * 标签
     */
    @ManyToMany(thirdPartyTable = "t_book_tag",
            referenceTable = "t_tag", referenceColumnName = "tag_id", columnDefinition="book_id")
    private List<Tag> tagList;

}

Person.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table("t_person")
public class Person extends BaseEntity {

    private String name;

}

Tag.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_tag", comment = "tag")
public class Tag extends BaseEntity {

    private String title;

}

实体创建完成后,通过 tableGenerator 生成表结构

tableGenerator.createTable(Tag.class);
tableGenerator.createTable(Person.class);
tableGenerator.createTable(Book.class);

Junit 测试

  • 添加Tag
 tagDAO.insertOrUpdate(Arrays.asList(
          Tag.builder().title("文学").build(),
          Tag.builder().title("欧美").build(),
          Tag.builder().title("医学").build()
  ));

617083724562964480,文学
617083724571353088,欧美
617083724571353089,医学

  • 添加Person
personDAO.insert(Arrays.asList(
        Person.builder().name("列夫托尔斯泰").build(),
        Person.builder().name("歌德").build(),
        Person.builder().name("莎士比亚").build()
        ));

617123167953149952,列夫托尔斯泰
617123167957344256,歌德
617123167957344257,莎士比亚

  • 添加Book《威尼斯商人》 作者是莎士比亚,标签为文学、科技。
bookDAO.insertOrUpdate(Book.builder()
         .title("威尼斯商人")
         .person(Person.builder().id(617123167957344257L).build())
         .tagList(Arrays.asList(
                 Tag.builder().id(617083724562964480L).build(),
                 Tag.builder().id(617083724571353088L).build()
         ))
         .build());

前端传参并持久化

POST结构化参数JSON

{
    "title": "威尼斯商人",
    "person": {
        "id": "617123167957344257"
    },
    "tagList": [
        {
            "id": "617083724562964480"
        },
        {
            "id": "617083724571353088"
        }
    ]
}

可以看到结构化参数,对前端相当不友好,非常地丑陋。

POST扁平化参数JSON

我们期望JSON格式如下:

{
    "title": "威尼斯商人",
    "personId": "617123167957344257",
    "tagIds": ["617083724562964480", "617083724571353088"]
}

方案1:

我们需要一个 BookDTO 类继承 Book,这是这篇文章的核心。
BookDTO.java

@Setter
public class BookDTO extends Book {

    private Long personId;

    private List<Long> tagIds;

    /**
     * 扩展字段
     */
    private SexEnum sex;

    /**
     * 扩展字段
     */
    private School.TypeEnum type;

    @Override
    public Person getPerson() {
        if (personId != null) {
            return Person.builder().id(personId).build();
        }
        return super.getPerson();
    }

    @Override
    public List<Tag> getTagList() {
        if (CollectionUtils.isNotEmpty(tagIds)) {
            List<Tag> tagList = new ArrayList<>();

            for (Long tagId : tagIds) {
                tagList.add(Tag.builder().id(tagId).build());
            }
            return tagList;
        }
        return super.getTagList();
    }

}

或者

@Setter
public class BookDTO extends Book {

    private Long personId;

    private List<Long> tagIds;

    /**
     * 扩展字段
     */
    private SexEnum sex;

    /**
     * 扩展字段
     */
    private School.TypeEnum type;

    public void setPersonId(Long personId) {
        this.setPerson(Person.builder().id(personId).build());
    }

    public void setTagIds(List<Long> tagIds) {
        if (CollectionUtils.isNotEmpty(tagIds)) {
            List<Tag> tagList = new ArrayList<>();

            for (Long tagId : tagIds) {
                tagList.add(Tag.builder().id(tagId).build());
            }
            setTagList(tagList);
        }
    }

}

BookController.java

@RestController
@RequestMapping("books")
public class BookController {

    @Autowired
    private BookDAO bookDAO;

    @PostMapping
    public BookDTO postSave(@RequestBody BookDTO book) {
        bookDAO.insert(book);
        return book;
    }

    @GetMapping
    public BookDTO getSave(BookDTO book) {
        bookDAO.insert(book);
        return book;
    }

    @GetMapping("{id}")
    public Book queryById(@PathVariable Long id) {
        return bookDAO.selectById(id).get();
    }
}

请求 http://localhost:8080/books 并提交扁平化的json就可以正确持久化。

GET 请求也能奏效:

curl -X GET \
  'http://localhost:8080/books?title=威尼斯商人&personId=617123167957344257&tagIds=617083724562964480,617083724571353088'

curl -X GET \
  'http://localhost:8080/books?title=威尼斯商人&personId=617123167957344257&tagIds=617083724562964480&tagIds=617083724571353088'

或用person tagList属性接收,id会自动映射到对象

curl -X GET \
  'http://localhost:8080/books?title=威尼斯商人&type=PUBLIC&sex=2&person=617327246029365249&tagList=617320872469864448,617320872469864449'

方案2: 实体字段自定义反序列化,添加注解 @JsonAlias @JsonDeserialize,这种方式不需要继承一个类 BookDTO, 会更加简洁优雅。如果需要扩展字段,继承类 BookDTO 还是有必要的,比如 BookDTO 中的 sex type

  • @JsonAlias 反序列化的时候,将JSON属性myPage映射POJO上,序列化的不受影响。

  • @ParamName Message请求对映射关系,支持下划线转驼峰

  • Jackson默认是SNAKE_CASE,属性tagList不会别映射(tag_list才能映射),如需可以在 JsonAlias 指定。

Book.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_book", comment = "书")
public class Book extends BaseEntity {

    private String title;

    /**
     * 书的拥有者
     */
    @ParamName("person_id")
    @JsonAlias("personId")
    @JsonDeserialize(using = EntityWithLongIdPropertyDeserializer.class)
    @ManyToOne(value = "person_id", parentTable = "t_person")
    private Person person;

    /**
     * 标签
     */
    @ParamName("tag_ids")
    @JsonAlias({"tagIds", "tagList"})
    @JsonDeserialize(using = EntityWithLongIdPropertyDeserializer.class)
    @ManyToMany(thirdPartyTable = "t_book_tag",
            referenceTable = "t_tag", referenceColumnName = "tag_id", columnDefinition="book_id")
    private List<Tag> tagList;

}

查看新创建的对象

curl -X GET \

http://127.0.0.1:8080/books/617084709792391168

{
    "id": "617084709792391168",
    "createdBy": 0,
    "createdAt": "2022-10-27T10:07:18Z",
    "updatedBy": 0,
    "updatedAt": "2022-10-27T13:04:14Z",
    "deleted": false,
    "title": "威尼斯商人",
    "person": {
        "id": "552098712424472576",
        "createdBy": 0,
        "createdAt": "2022-05-01T02:16:08Z",
        "updatedBy": 0,
        "updatedAt": "2022-05-01T02:16:08Z",
        "deleted": false,
        "name": "莎士比亚",
    },
    "tagList": [
        {
            "id": "617083724562964480",
            "createdBy": 0,
            "createdAt": "2022-10-27T10:03:23Z",
            "updatedBy": 0,
            "updatedAt": "2022-10-27T10:03:23Z",
            "deleted": false,
            "title": "文学"
        },
        {
            "id": "617083724571353088",
            "createdBy": 0,
            "createdAt": "2022-10-27T10:03:23Z",
            "updatedBy": 0,
            "updatedAt": "2022-10-27T10:03:23Z",
            "deleted": false,
            "title": "欧美"
        }
    ]
}

Javascript中Array的4种遍历方式

let persons = [
  {id: 1, name: "Rick", age: 19},
  {id: 2, name: "Ashley", age: 19},
  {id: 3, name: "Jerry", age: 22},
  {id: 4, name: "Lucy", age: 25},
]

// for
const length = persons.length
for (let index = 0; index < length; index++) {
  if (index === 2) {
    return; // 可以中断遍历
  }
  p = persons[index]
  console.log(p.id, ":" ,p.name, index)
}

console.log('-----------')

// forEach
persons.forEach((p, index) => {
  if (index === 2) {
    return; // 不可以 中断遍历,这地方也不能使用 `break`。这个一个函数,函数在循环中
  }
  console.log(p.id, ":" ,p.name, index)
})

console.log('-----------')

// for...in
for (index in persons) {
  if (index === "2") {
    return; // 可以中断遍历
  }
  p = persons[index]
  console.log(p.id, ":" ,p.name, index)
}

// for...of
console.log('-----------')
for (p of persons) {
  if (p.id ===3 ) {
     return; // 可以中断遍历
  }
  console.log(p.id, ":" ,p.name)
}

类继承在typescript编译后prototype的实现方法

extends.ts

class E {

  constructor(private name: string, private age: number) {
  }

  sayHello() {}
}

class F extends E {

  constructor(name: string, age: number, private score: number) {
    super(name, age)
  }

  sayHello() {}
  sayGoodbye(){}
}

let f = new F('Rick.Xu', 33, 100)
let e = new E('Ashley', 23);

extends.js

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());

        // test by Rick
        console.log(__.prototype == b.prototype) // true
        console.log(d.prototype instanceof __) // true
        var a = new __()
        a.sayHello()
        // a.sayGoodbye() // a.sayGoodbye is not a function
        // test by Rick end
    };
})();
var E = /** @class */ (function () {
    function E(name, age) {
        this.name = name;
        this.age = age;
    }
    E.prototype.sayHello = function () { };
    return E;
}());
var F = /** @class */ (function (_super) {
    __extends(F, _super);
    function F(name, age, score) {
        var _this = _super.call(this, name, age) || this;
        _this.score = score;
        return _this;
    }
    F.prototype.sayHello = function () { };
    F.prototype.sayGoodbye = function () { };
    return F;
}(E));
var f = new F('Rick.Xu', 33, 100);
var e = new E('Ashley', 23);

typescript 编译后成 ES3后,原型链中加了一个中间对象 __ 。子类的原型对象是 ____的原型对象是父类的原型对象。和下面的方法等效:

d.prototype = Object.create(b.prototype); 

https://xhope.top/?p=687

Springboot集成Https双向验证

目的

如果现在当前系统需要开放一个接口 /say-hello 给第三方应用调用。对于认证可以有两种方式:

  • 提供username/password认证后返回token,当访问接口时携带token访问
  • 接口是通过客户端证书来分配权限的,把客户端的cert导入到服务端,客户端通过配置认证才能访问

本文章将讨论「如何使用客户端证书」访问接口。

概念

自签名证书

自签名证书是由不受信的CA机构颁发的数字证书,也就是自己签发的证书。与受信任的CA签发的传统数字证书不同,自签名证书是由一些公司或软件开发商创建、颁发和签名的。虽然自签名证书使用的是与X.509证书相同的加密密钥对架构,但是却缺少受信任第三方(如Sectigo)的验证。

创建自签名SSL证书

1、生成私钥
要创建SSL证书,需要私钥和证书签名请求(CSR)。您可以使用一些生成工具或向CA申请生成私钥,私钥是使用RSA和ECC等算法生成的加密密钥。生成RSA私钥的代码示例:openssl genrsa -aes256 -out servername.pass.key 4096,随后该命令会提示您输入密码。

2、生成CSR
私钥生成后,您的私钥文件现在将作为 servername.key 保存在您的当前目录中,并将用于生成CSR。自签名证书的CSR的代码示例:openssl req -nodes -new -key servername.key -out servername.csr。然后需要输入几条信息,包括组织、组织单位、国家、地区、城市和通用名称。通用名称即域名或IP地址。
输入此信息后,servername.csr 文件将位于当前目录中,其中包含 servername.key 私钥文件。

3、颁发证书
最后,使用server.key(私钥文件)和server.csr 文件生成新证书(.crt)。以下是生成新证书的命令示例:openssl x509 -req -sha256 -days 365 -in servername.csr -signkey servername.key -out servername.crt。最后,在您的当前目录中找到servername.crt文件即可。

参考链接:https://segmentfault.com/a/1190000040834174

SSL证书格式

  • .DER .CER,文件是二进制格式,只保存证书,不保存私钥。
  • .PEM,一般是文本格式,可保存证书,可保存私钥。
  • .CRT,可以是二进制格式,可以是文本格式,与 .DER 格式相同,不保存私钥。
  • .PFX .P12,二进制格式,同时包含证书和私钥,一般有密码保护。
  • .JKS,二进制格式,同时包含证书和私钥,一般有密码保护。
    参考链接:
    https://blog.csdn.net/farrellcn/article/details/119779348

对称加密与非对称加密

  • 对称加密(Symmetric Cryptography)
    对称加密是最快速、最简单的一种加密方式,加密(encryption)与解密(decryption)用的是同样的密钥(secret key)。
  • 非对称加密(Asymmetric Cryptography)
    非对称加密为数据的加密与解密提供了一个非常安全的方法,它使用了一对密钥,公钥(public key)和私钥(private key)。私钥只能由一方安全保管,不能外泄,而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密,而解密则需要另一个密钥。

常用的命令

  • OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。这个包广泛被应用在互联网的网页服务器上。
  • keytool 是个密钥和证书管理工具。它使用户能够管理自己的公钥/私钥对及相关证书,用于(通过数字签名)自我认证(用户向别的用户/服务认证自己)或数据完整性以及认证服务。它还允许用户储存他们的通信对等者的公钥(以证书形式)。
  • JKS和PKCS#12:都是比较常用的两种密钥库格式/标准。对于前者,搞Java开发,尤其是接触过HTTPS平台的朋友,并不陌生。JKS文件(通常为.jks或.keystore,扩展名无关)可以通过Java原生工具——KeyTool生成;而后者PKCS#12文件(通常为.p12或.pfx,意味个人信息交换文件),则是通过更为常用的OpenSSL工具产生。
    当然,这两者之间是可以通过导入/导出的方式进行转换的!当然,这种转换需要通过KeyTool工具进行!

生成私钥private.key

openssl genrsa -out private.key 2048

生成用于申请请求的证书文件csr,一般会将该文件发送给CA机构进行认证,本例使用自签名证书request.csr

openssl req -new -key private.key -out request.csr

自签名证书root.crt

openssl x509 -req -days 365 -in request.csr -signkey private.key -out root.crt

查看证书信息

openssl x509 -noout -text -in root.crt

openssl生成p12文件cert.p12

openssl pkcs12 -export -out cert.p12 -inkey private.key -in root.crt

p12文件转换为keystore(jks)文件my.keystore

keytool -importkeystore -srckeystore cert.p12 -destkeystore my.keystore -deststoretype pkcs12

keytool生成p12文件ssl-key.p12

keytool -genkeypair -alias serverssl -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ssl-key.p12 -validity 3650

生成keystore(jks)文件

keytool -genkeypair -dname "cn=clientAuth_vmi_th, ou=IS, o=SGM, c=CN" -alias serverssl -keypass Pass1234 -keystore serverssl.jks -validity 3600 -keyalg RSA -keysize 2048 -sigalg SHA256WithRSA

查看密钥库

keytool -list -rfc --keystore clientAuth_vmi_th.jks -storepass Pass1234

密钥库导出别名clientAuthCert证书clientAuthCert.cer

keytool -export -file clientAuthCert.cer -keystore clientAuthCert.jks -storepass Pass1234 -alias clientAuthCert

keytool -export -file clientAuthCert.cer -keystore clientAuthCert.p12 -storepass Pass1234 -alias clientAuthCert

导入证书到密钥库

keytool -import -alias localhost -file localhost.cer -keystore client.jks

参考链接:
https://www.cnblogs.com/qq931399960/p/11889349.html
http://t.zoukankan.com/zwh0910-p-15214672.html
https://www.cnblogs.com/kabi/p/6232966.html
https://www.jianshu.com/p/e83150254ef8
https://zhuanlan.zhihu.com/p/406815419

示例

后端代码SpringBoot

application.yml

server:
# p12密钥匙库:keytool -genkeypair -alias serverssl -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ssl-key.p12 -validity 3650
#  ssl:
#    key-store: classpath:ssl-key.p12
#    key-store-password: Pass1234
#    keyStoreType: PKCS12
#    keyAlias: serverssl
#    client-auth: need

# JKS密钥匙库:keytool -genkeypair -dname "cn=clientAuth_vmi_th, ou=IS, o=SGM, c=CN" -alias clientAuthCert -keypass Pass1234 -keystore clientAuthCert.jks -storepass Pass1234 -validity 3600 -keyalg RSA -keysize 2048 -sigalg SHA256WithRSA
  ssl:
    key-store: classpath:clientAuth_vmi_th.jks
    key-store-password: Pass1234
    keyStoreType: JKS
    keyAlias: clientAuthCert
    key-password: Pass1234

    client-auth: need # 需要客户端认证

  port: 8443

SslApplication.java

@SpringBootApplication
@RestController
public class SslApplication {

    public static void main(String[] args) {
        SpringApplication.run(SslApplication.class, args);
    }

    @GetMapping("say-hello")
    public String sayHello() {
        return "hello";
    }

}

ServerConfig.java http跳转https

@Configuration
public class ServerConfig {

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint = new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                collection.addPattern("/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(getHttpConnector());
        return tomcat;
    }

    private Connector getHttpConnector() {
        Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
        connector.setScheme("http");
        connector.setPort(8080);
        connector.setSecure(false);
        connector.setRedirectPort(8443);
        return connector;
    }

}

客户端请求

Postman请求

  • 生成公钥public.key
keytool -list -rfc --keystore clientAuth_vmi_th.jks | openssl x509 -inform pem –pubkey

显示的内容拷贝到文件 public.key

  • 生成私钥private.key
keytool -v -importkeystore -srckeystore clientAuth_vmi_th.jks -srcstoretype jks -srcstorepass Pass1234 -destkeystore clientAuth_vmi_th.pfx -deststoretype pkcs12 -deststorepass Pass1234 -destkeypass Pass1234
openssl pkcs12 -in clientAuth_vmi_th.pfx -nocerts -nodes -out private.key
  • 添加证书
    https://xhope.top//wp-content/uploads/2022/07/1.png

Java 代码OkHttp请求

SSLTest.java

class SSLTest {
    private static final String KEYSTOREPASS = "Pass1234";
    private static final String KEYPASS = "Pass1234";

    static KeyStore readStore() throws Exception {
        try (InputStream keyStoreStream = new FileInputStream("/Users/rick/jkxyx205/ca/clientAuth_vmi_th.jks")) {
            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(keyStoreStream, KEYSTOREPASS.toCharArray());
            return keyStore;
        }
    }

    static SSLSocketFactory getSSLSocketFactory() {
        SSLSocketFactory factory = null;
        try {
            //初始化SSLContext
            SSLContext sslContext = SSLContexts.custom()
                    .loadKeyMaterial(readStore(), KEYPASS.toCharArray())
                    .build();

            factory = sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return factory;
    }

    public static void main(String[] args) throws IOException {
        OkHttpClient client = new OkHttpClient.Builder()
                .sslSocketFactory(SSLHelper2.getSSLSocketFactory())
                .readTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .connectTimeout(6, TimeUnit.SECONDS)
                .hostnameVerifier(new NoopHostnameVerifier())
                .build();
        Request request = new Request.Builder()
                .url("https://test.com:8443/say-hello")
                .get()
                .build();

        Response response = client.newCall(request).execute();
        System.out.println(response.body().string()); // hello
    }
}

参考链接:
https://www.cnblogs.com/youngdeng/p/12868717.html

认证工具类

CertificateCoder.java

public class CertificateCoder {
    /**
     * Java密钥库(Java Key Store,JKS)KEY_STORE
     */
    public static final String KEY_STORE = "JKS";

    public static final String X509 = "X.509";

    /**
     * 由 KeyStore获得私钥
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @param aliasPassword
     * @return
     * @throws Exception
     */
    private static PrivateKey getPrivateKey(String keyStorePath,
                                            String keyStorePassword, String alias, String aliasPassword)
            throws Exception {
        KeyStore ks = getKeyStore(keyStorePath, keyStorePassword);
        PrivateKey key = (PrivateKey) ks.getKey(alias,
                aliasPassword.toCharArray());
        return key;
    }

    /**
     * 由 Certificate获得公钥
     *
     * @param certificatePath
     * @return
     * @throws Exception
     */
    private static PublicKey getPublicKey(String certificatePath)
            throws Exception {
        Certificate certificate = getCertificate(certificatePath);
        PublicKey key = certificate.getPublicKey();
        return key;
    }

    /**
     * 获得Certificate
     *
     * @param certificatePath
     * @return
     * @throws Exception
     */
    private static Certificate getCertificate(String certificatePath)
            throws Exception {
        CertificateFactory certificateFactory = CertificateFactory
                .getInstance(X509);
        FileInputStream in = new FileInputStream(certificatePath);

        Certificate certificate = certificateFactory.generateCertificate(in);
        in.close();

        return certificate;
    }

    /**
     * 获得Certificate
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @return
     * @throws Exception
     */
    private static Certificate getCertificate(String keyStorePath,
                                              String keyStorePassword, String alias) throws Exception {
        KeyStore ks = getKeyStore(keyStorePath, keyStorePassword);
        Certificate certificate = ks.getCertificate(alias);

        return certificate;
    }

    /**
     * 获得KeyStore
     *
     * @param keyStorePath
     * @param password
     * @return
     * @throws Exception
     */
    private static KeyStore getKeyStore(String keyStorePath, String password)
            throws Exception {
        FileInputStream is = new FileInputStream(keyStorePath);
        KeyStore ks = KeyStore.getInstance(KEY_STORE);
        ks.load(is, password.toCharArray());
        is.close();
        return ks;
    }

    /**
     * 私钥加密
     *
     * @param data
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @param aliasPassword
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPrivateKey(byte[] data, String keyStorePath,
                                             String keyStorePassword, String alias, String aliasPassword)
            throws Exception {
        // 取得私钥
        PrivateKey privateKey = getPrivateKey(keyStorePath, keyStorePassword,
                alias, aliasPassword);

        // 对数据加密
        Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);

        return cipher.doFinal(data);

    }

    /**
     * 私钥解密
     *
     * @param data
     * @param keyStorePath
     * @param alias
     * @param keyStorePassword
     * @param aliasPassword
     * @return
     * @throws Exception
     */
    public static byte[] decryptByPrivateKey(byte[] data, String keyStorePath,
                                             String alias, String keyStorePassword, String aliasPassword)
            throws Exception {
        // 取得私钥
        PrivateKey privateKey = getPrivateKey(keyStorePath, keyStorePassword,
                alias, aliasPassword);

        // 对数据加密
        Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        return cipher.doFinal(data);

    }

    /**
     * 公钥加密
     *
     * @param data
     * @param keyStorePath
     * @param alias
     * @param keyStorePassword
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPublicKey(byte[] data, String keyStorePath,
                                            String alias, String keyStorePassword)
            throws Exception {

        // 取得公钥
        PublicKey publicKey = getCertificate(keyStorePath, keyStorePassword, alias).getPublicKey();
        // 对数据加密
        Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        return cipher.doFinal(data);

    }

    /**
     * 公钥加密
     *
     * @param data
     * @param certificatePath
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPublicKey(byte[] data, String certificatePath)
            throws Exception {

        // 取得公钥
        PublicKey publicKey = getPublicKey(certificatePath);
        // 对数据加密
        Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        return cipher.doFinal(data);

    }

    /**
     * 公钥解密
     *
     * @param data
     * @param certificatePath
     * @return
     * @throws Exception
     */
    public static byte[] decryptByPublicKey(byte[] data, String certificatePath)
            throws Exception {
        // 取得公钥
        PublicKey publicKey = getPublicKey(certificatePath);

        // 对数据加密
        Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, publicKey);

        return cipher.doFinal(data);

    }

    /**
     * 验证Certificate
     *
     * @param certificatePath
     * @return
     */
    public static boolean verifyCertificate(String certificatePath) {
        return verifyCertificate(new Date(), certificatePath);
    }

    /**
     * 验证Certificate是否过期或无效
     *
     * @param date
     * @param certificatePath
     * @return
     */
    public static boolean verifyCertificate(Date date, String certificatePath) {
        boolean status = true;
        try {
            // 取得证书
            Certificate certificate = getCertificate(certificatePath);
            // 验证证书是否过期或无效
            status = verifyCertificate(date, certificate);
        } catch (Exception e) {
            status = false;
        }
        return status;
    }

    /**
     * 验证证书是否过期或无效
     *
     * @param date
     * @param certificate
     * @return
     */
    private static boolean verifyCertificate(Date date, Certificate certificate) {
        boolean status = true;
        try {
            X509Certificate x509Certificate = (X509Certificate) certificate;
            x509Certificate.checkValidity(date);
        } catch (Exception e) {
            status = false;
        }
        return status;
    }

    /**
     * 签名
     *
     * @param keyStorePath
     * @param alias
     * @param keyStorePassword
     * @param aliasPassword
     * @return
     * @throws Exception
     */
    public static byte[] sign(byte[] sign, String keyStorePath, String alias,
                              String keyStorePassword, String aliasPassword) throws Exception {
        // 获得证书
        X509Certificate x509Certificate = (X509Certificate) getCertificate(
                keyStorePath, keyStorePassword, alias);

        // 取得私钥
        PrivateKey privateKey = getPrivateKey(keyStorePath, keyStorePassword,
                alias, aliasPassword);

        // 构建签名
        Signature signature = Signature.getInstance(x509Certificate
                .getSigAlgName());
        signature.initSign(privateKey);
        signature.update(sign);
        return signature.sign();
    }

    /**
     * 验证签名
     *
     * @param data
     * @param sign
     * @param certificatePath
     * @return
     * @throws Exception
     */
    public static boolean verify(byte[] data, byte[] sign,
                                 String certificatePath) throws Exception {
        // 获得证书
        X509Certificate x509Certificate = (X509Certificate) getCertificate(certificatePath);
        // 获得公钥
        PublicKey publicKey = x509Certificate.getPublicKey();
        // 构建签名
        Signature signature = Signature.getInstance(x509Certificate
                .getSigAlgName());
        signature.initVerify(publicKey);
        signature.update(data);

        return signature.verify(sign);

    }

    /**
     * 验证Certificate
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @return
     */
    public static boolean verifyCertificate(Date date, String keyStorePath,
                                            String keyStorePassword, String alias) {
        boolean status = true;
        try {
            Certificate certificate = getCertificate(keyStorePath,
                    keyStorePassword, alias);
            status = verifyCertificate(date, certificate);
        } catch (Exception e) {
            status = false;
        }
        return status;
    }

    /**
     * 验证Certificate
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @return
     */
    public static boolean verifyCertificate(String keyStorePath,
                                            String keyStorePassword, String alias) {
        return verifyCertificate(new Date(), keyStorePath, keyStorePassword,
                alias);
    }
}

参考链接:
https://xhope.top/?p=1336

理解

  • 一般情况,公钥加密,私钥解密。证书中包含公钥发送给客户端用于加密。
  • 密钥库中既有公钥又有私钥
  • 证书有很多格式,服务器不一样需要的格式也不一样。大体分为文本和二进制格式,是否保存私钥,是否需要密码。

其他参考链接

https://www.thomasvitale.com/https-spring-boot-ssl-certificate/
https://www.cnblogs.com/xa-xiaochen/p/15671213.html

sharp-database实例详解

POJO

实体

总共4个实体类:School.java SchoolLicense.java Student.java Teacher.java

  • 「学校」和「证书」1对1外键
  • 「学校」和「学生」1对多
  • 「学校」和「老师」多对多

School.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_school", comment = "学校")
public class School extends BasePureEntity {

    @Column(comment = "学校名称")
    private String name;

    @Column(comment = "建校日期")
    private LocalDate buildDate;

    @Column(comment = "学校性质 PRIVATE:私立;PUBLIC:公立")
    private TypeEnum type;

    @Column(comment = "每年经费预算")
    private BigDecimal budget;

    @Column(comment = "专业数")
    private Integer score;

    @Column(comment = "学校地址")
    private Address address;

    @Column(comment = "其他信息")
    private Map<String, Object> additionalInfo;

    @Column(comment = "历届领导信息")
    private List<Map<String, Object>> leadershipInformationList;

    @Column(comment = "历届获奖信息")
    private Set<Map<String, Object>> awardsSet;

    @Column(comment = "历届学校评分")
    private List<Float> scoreList;

    @Column(comment = "学校评价")
    private Evaluate evaluate;

    /**
     * 「学校」和「证书」1对1外键
     *  1 <==> 1
     */
    @ManyToOne(value = "school_license_id", parentTable = "t_school_license", comment = "证书信息")
    private SchoolLicense schoolLicense;

    /**
     * 「学校」和「学生」1对多
     *  1 <==> N
     */
    @OneToMany(subTable = "t_school_student", joinValue = "school_id")
    private List<Student> studentList;

    /**
     * 「学校」和「老师」多对多
     *  N <==> N
     */
    @ManyToMany(thirdPartyTable = "t_school_teacher_related", columnDefinition = "school_id", referenceTable = "t_school_teacher", referenceColumnName = "teacher_id")
    private List<Teacher> teacherList;

    @AllArgsConstructor
    @Getter
    public enum TypeEnum {
        PRIVATE("私立"),
        PUBLIC("公立");

        @JsonValue
        public String getCode() {
            return this.name();
        }
        private final String label;
        public static TypeEnum valueOfCode(String code) {
            return valueOf(code);
        }
    }
}

SchoolLicense.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_school_license", comment = "学校证书")
public class SchoolLicense extends BasePureEntity {

    @Column(comment = "证书编号")
    private String number;

    @Column(comment = "备注")
    private String remark;

    @OneToMany(subTable = "t_school", joinValue = "school_license_id", oneToOne = true)
    private School school;

}

Student.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_school_student", comment = "学生")
public class Student extends BasePureEntity {

    @Column(comment = "姓名")
    private String name;

    @Column(comment = "年级")
    private Integer grade;

    @Column(comment = "性别")
    private SexEnum sex;

    @ManyToOne(value = "school_id", parentTable = "t_school")
    private School school;

    @AllArgsConstructor
    @Getter
    public enum SexEnum {
        MALE(0, "男"),
        FEMALE(1, "女");

        private static final Map<Integer, SexEnum> codeMap = new HashMap<>();

        static {
            for (SexEnum e : values()) {
                codeMap.put(e.code, e);
            }
        }

        private final int code;
        private final String label;

        @JsonValue
        public int getCode() {
            return code;
        }

        public static SexEnum valueOfCode(int code) {
            return codeMap.get(code);
        }
    }
}

Teacher.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_school_teacher", comment = "教师")
public class Teacher extends BasePureEntity {

    @Column(comment = "姓名")
    private String name;

    @Column(comment = "年龄")
    private Integer age;

    @ManyToMany(thirdPartyTable = "t_school_teacher_related", columnDefinition = "teacher_id", referenceTable = "t_school", referenceColumnName = "school_id")
    private List<School> schoolList;

}

存储Model

Evaluate.java

@AllArgsConstructor
@Getter
public class Evaluate {

    /**
     * 评价等级
     */
    private Integer grade;

    /**
     * 评价内容
     */
    private String description;

    @Override
    public String toString() {
        return "Evaluate{" +
                "grade=" + grade +
                ", description='" + description + '\'' +
                '}';
    }
}

Evaluate.java 没有实现接口 JsonStringToObjectConverterFactory.JsonValue,所以必须自定义converter反序列号字符串。
EvaluateConverterFactory.java

@Component
public class EvaluateConverterFactory implements ConverterFactory<String, Evaluate> {

    @Override
    public <T extends Evaluate> Converter<String, T> getConverter(Class<T> aClass) {
        return value -> {
            // value => Evaluate{grade=1, description='GREAT!'}

            String substring = value.substring(value.indexOf("{") + 1, value.length() - 1);
            String[] values = substring.split(",\\s+");

            return (T) new Evaluate(Integer.parseInt(values[0].split("=")[1]), values[1].split("=")[1]);
        };
    }
}

Address.java

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address implements JsonStringToObjectConverterFactory.JsonValue {

    private String code;

    private String detail;
}

生成SQL

运行 TableGenerator 生成SQL,参考sharp-database中的TableGenerator根据实体对象生成表

tableGenerator.createTable(School.class);
tableGenerator.createTable(Teacher.class);
tableGenerator.createTable(Student.class);
tableGenerator.createTable(SchoolLicense.class);

生成的SQL DDL

create table t_school_license
(
    id bigint not null comment '主键'
        primary key,
    number varchar(32) null comment '证书编号',
    remark varchar(32) null comment '备注',
    created_by bigint null,
    created_at datetime null,
    updated_by bigint null,
    updated_at datetime null,
    is_deleted bit null
)
comment '学校证书';

create table t_school
(
    id bigint not null comment '主键'
        primary key,
    name varchar(32) null comment '学校名称',
    build_date date null comment '建校日期',
    type varchar(16) null comment '学校性质 PRIVATE:私立;PUBLIC:公立',
    budget decimal(10,4) null comment '每年经费预算',
    score int null comment '专业数',
    address json null comment '学校地址',
    additional_info json null comment '其他信息',
    leadership_information_list json null comment '历届领导信息',
    awards_set varchar(32) null comment '历届获奖信息',
    score_list json null comment '历届学校评分',
    evaluate varchar(128) null comment '学校评价',
    school_license_id bigint null comment '证书信息',
    created_by bigint null,
    created_at datetime null,
    updated_by bigint null,
    updated_at datetime null,
    is_deleted bit null,
    constraint t_school_t_school_license_id_fk
        foreign key (school_license_id) references t_school_license (id)
)
comment '学校';

create table t_school_student
(
    id bigint not null comment '主键'
        primary key,
    name varchar(32) null comment '姓名',
    grade int null comment '年级',
    sex varchar(16) null comment '性别',
    school_id bigint null,
    created_by bigint null,
    created_at datetime null,
    updated_by bigint null,
    updated_at datetime null,
    is_deleted bit null,
    constraint t_school_student_t_school_id_fk
        foreign key (school_id) references t_school (id)
)
comment '学生';

create table t_school_teacher
(
    id bigint not null comment '主键'
        primary key,
    name varchar(32) null comment '姓名',
    age int null comment '年龄',
    created_by bigint null,
    created_at datetime null,
    updated_by bigint null,
    updated_at datetime null,
    is_deleted bit null
)
comment '教师';

create table t_school_teacher_related
(
    school_id bigint not null,
    teacher_id bigint not null,
    is_deleted bit default b'0' not null,
    constraint t_school_teacher_related_pk
        unique (school_id, teacher_id),
    constraint t_school_teacher_related_t_school_id_fk
        foreign key (school_id) references t_school (id),
    constraint t_school_teacher_related_t_school_teacher_id_fk
        foreign key (teacher_id) references t_school_teacher (id)
);

https://xhope.top/wp-content/uploads/2022/05/ii.png

生成DAO

SchoolDAO

@Repository
public class SchoolDAO extends BaseDAOImpl<School> {

}

SchoolLicenseDAO

@Repository
public class SchoolLicenseDAO extends BaseDAOImpl<SchoolLicense> {

}

StudentDAO

@Repository
public class StudentDAO extends BaseDAOImpl<Student> {

}

TeacherDAO

@Repository
public class TeacherDAO extends BaseDAOImpl<Teacher> {

}

测试

Junit5

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class SchoolTest {

    @Autowired
    private SchoolDAO schoolDAO;

    @Autowired
    private SchoolLicenseDAO schoolLicenseDAO;

    @Autowired
    private StudentDAO studentDAO;

    @Autowired
    private TeacherDAO teacherDAO;

    @Order(0)
    @Test
    public void testSave() {
        SchoolLicense schoolLicense = createSchoolLicense();
        schoolLicenseDAO.insert(schoolLicense);

        School school = createSchool(schoolLicense);
        schoolDAO.insert(school);

        Student student = createStudent(school);
        studentDAO.insert(student);

        Teacher teacher = createTeacher(Arrays.asList(school));
        teacherDAO.insert(teacher);
    }

    @Order(1)
    @Test
    public void testSchoolFindById() {
        Optional<School> option = schoolDAO.selectById(552173736070144000L);
        School school = option.get();
        checkSchool(school);
        checkSchoolLicense(school.getSchoolLicense());
        checkStudent(school.getStudentList().get(0));
        checkTeacher(school.getTeacherList().get(0));
    }

    @Order(1)
    @Test
    public void testStudentFindById() {
        Student student = studentDAO.selectById(552173736246304768L).get();
        checkStudent(student);
        checkSchool(student.getSchool());
    }

    @Order(2)
    @Test
    public void testTeacherFindById() {
        Teacher teacher = teacherDAO.selectById(552173736518934528L).get();
        checkTeacher(teacher);
        checkSchool(teacher.getSchoolList().get(0));
    }

    @Order(3)
    @Test
    public void testSchoolLicenseFindById() {
        SchoolLicense schoolLicense = schoolLicenseDAO.selectById(552173735432609792L).get();
        checkSchoolLicense(schoolLicense);
        checkSchool(schoolLicense.getSchool());
    }

    private School createSchool(SchoolLicense schoolLicense) {
        return School.builder()
                .name("清华大学")
                .buildDate(LocalDate.now())
                .type(School.TypeEnum.PUBLIC)
                .score(100)
                .budget(new BigDecimal("111323.23"))
                .address(Address.builder().code("100084").detail("中华人民共和国北京市海淀区清华园").build())
                .additionalInfo(Params.builder(1).pv("Teacher Count", 3641).build())
                .evaluate(new Evaluate(1, "GREAT!"))
                .leadershipInformationList(Arrays.asList(Params.builder(1).pv("Principal", "王希勤").build()))
                .awardsSet(Sets.newHashSet(Params.builder(1).pv("c", "C9联盟").build()))
                .scoreList(Arrays.asList(99.9f, 98f, 100f))
                .schoolLicense(schoolLicense)
                .build();
    }

    private Student createStudent(School school) {
        return Student.builder()
                .name("Rick.Xu")
                .sex(Student.SexEnum.MALE)
                .grade(1)
                .school(school)
                .build();
    }

    private Teacher createTeacher(List<School> schoolList) {
        return Teacher.builder()
                .name("姚期智")
                .age(50)
                .schoolList(schoolList)
                .build();
    }

    private SchoolLicense createSchoolLicense() {
        return SchoolLicense.builder()
                .number("124535354C34X")
                .remark("自强不息 厚德载物")
                .build();
    }

    private void checkSchool(School school) {
        assertThat(school.getName(),  equalTo("清华大学"));
        assertThat(school.getBuildDate(), notNullValue());
        assertThat(school.getType(), equalTo(School.TypeEnum.PUBLIC));
        assertThat(school.getScore(), equalTo(100));
        assertThat(school.getBudget(), equalTo(new BigDecimal("111323.2300")));
        assertThat(school.getAddress(), anyOf(
                hasProperty("code", equalTo("100084")),
                hasProperty("address", equalTo("中华人民共和国北京市海淀区清华园"))
        ));
        assertThat(school.getAdditionalInfo(), hasKey("Teacher Count"));
        assertThat(school.getEvaluate(), anyOf(
                hasProperty("grade", equalTo(1)),
                hasProperty("description", equalTo("GREAT!"))
        ));
        assertThat(school.getLeadershipInformationList().get(0).get("Principal"), equalTo("王希勤"));
        assertThat(school.getAwardsSet().iterator().next().get("c"), equalTo("C9联盟"));
        assertThat(school.getScoreList(), hasItem(99.9f));
    }

    private void checkStudent(Student student) {
        assertThat(student.getName(),  equalTo("Rick.Xu"));
        assertThat(student.getGrade(), equalTo(1));
        assertThat(student.getSex(), equalTo(Student.SexEnum.MALE));
    }

    private void checkTeacher(Teacher teacher) {
        assertThat(teacher.getName(),  equalTo("姚期智"));
        assertThat(teacher.getAge(), equalTo(50));
    }

    private void checkSchoolLicense(SchoolLicense schoolLicense) {
        assertThat(schoolLicense.getNumber(),  equalTo("124535354C34X"));
        assertThat(schoolLicense.getRemark(), equalTo("自强不息 厚德载物"));
    }
}

Postman

SchoolController.java

@RestController
@RequestMapping("schools")
@RequiredArgsConstructor
public class SchoolController {

    private final SchoolDAO schoolDAO;

    @PostMapping
    public School save(@RequestBody School school) {
        schoolDAO.insert(school);
        return schoolDAO.selectById(school.getId()).get();
    }
}

curl -X POST \
  http://localhost:8080/schools \
  -H 'Content-Type: application/json' \
  -H 'Postman-Token: 468b3efe-3014-4f6c-bb28-8cd5905e309e' \
  -H 'cache-control: no-cache' \
  -d '{
    "name": "北京大学",
    "buildDate": [2022, 5, 1],
    "type": "PUBLIC",
    "budget": 111323.2300,
    "score": 100,
    "address": {
        "code": "100084",
        "detail": "中华人民共和国北京市海淀区清华园"
    },
    "additionalInfo": {
        "Teacher Count": 3641
    },
    "leadershipInformationList": [{
        "Principal": "王希勤"
    }],
    "awardsSet": [{
        "c": "C9联盟"
    }],
    "scoreList": [99.9, 98.0, 100.0],
    "evaluate": {
        "grade": 1,
        "description": "'\''GREAT!'\''"
    }
}'

测试仓库

https://github.com/jkxyx205/sharp-project/tree/master/sharp-demo