1、需求来源:
项目结构:
springboot调用trueLicense生成license证书
遇到问题:
但是在docker容器环境中部署,只要容器重新构建,都需要重新上传license证书
解决方案:
因此需要找到容器内部环境中—license证书安装后存储的位置,并把这个位置下所有文件进行持久化,便不用每次构建都需要重新上传license文件了
2、解析过程:
- 从读取license中的详细信息方法入手,发现有两个大步骤需要完成
1.1 )初始化证书读取环境
1.2 )读取证书具体信息
package com.abc.license;
public class LicenseVerify {
public LicenseContent readAndVerify(LicenseVerifyParam param) {
//1.初始化证书读取环境,具体看下边的initLicenseParam方法
LicenseManager licenseManager = LicenseManagerHolder.getInstance(this.initLicenseParam(param));
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
//2.读取证书具体信息,具体看第【6】步
LicenseContent licenseContent = licenseManager.verify();
if (null != licenseContent.getNotAfter()) {
log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}", format.format(licenseContent.getNotBefore()), format.format(licenseContent.getNotAfter())));
} else {
log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}", format.format(licenseContent.getNotBefore()), "永久"));
}
return licenseContent;
} catch (Exception var5) {
log.error("证书校验失败!", var5);
return null;
}
}
private LicenseParam initLicenseParam(LicenseVerifyParam param) {
//初始化证书读取环境,此时进入Preferences.userNodeForPackage具体方法中
Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);
CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class, param.getPublicKeysStorePath(), param.getPublicAlias(), param.getStorePass(), (String)null);
return new DefaultLicenseParam(param.getSubject(), preferences, publicStoreParam, cipherParam);
}
}
- 开始初始化证书读取环境,进入Preferences.userNodeForPackage具体方法中
package java.util.prefs;
public abstract class Preferences {
/**
* 此处的c 就是 LicenseVerify.clas
* @param c the class for whose package a user preference node is desired.
* @return the user preference node associated with the package of which
* <tt>c</tt> is a member.
* @throws NullPointerException if <tt>c</tt> is <tt>null</tt>.
* @throws SecurityException if a security manager is present and
* it denies <tt>RuntimePermission("preferences")</tt>.
* @see RuntimePermission
*/
public static Preferences userNodeForPackage(Class<?> c) {
return userRoot().node(nodeName(c));
}
}
- 开始调用Preferences的userRoot()方法,最后进入到【factory】的userRoot方法。
而【factory】是调用factory()方法进行初始化的,factory()方法继续调用factory1()方法
在factory1()方法中的返回值发现,因为我本地是Windows,所以最终userRoot()返回的Preferences
是【WindowsPreferencesFactory】
package java.util.prefs;
public abstract class Preferences {
private static final PreferencesFactory factory = factory();
/**
* Returns the root preference node for the calling user.
*
* @return the root preference node for the calling user.
* @throws SecurityException If a security manager is present and
* it denies <tt>RuntimePermission("preferences")</tt>.
* @see RuntimePermission
*/
public static Preferences userRoot() {
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkPermission(prefsPerm);
//继续到factory的userRoot方法中,
return factory.userRoot();
}
private static PreferencesFactory factory() {
// 1. Try user-specified system property
String factoryName = AccessController.doPrivileged(
new PrivilegedAction<String>() {
public String run() {
return System.getProperty(
"java.util.prefs.PreferencesFactory");}});
if (factoryName != null) {
// FIXME: This code should be run in a doPrivileged and
// not use the context classloader, to avoid being
// dependent on the invoking thread.
// Checking AllPermission also seems wrong.
try {
return (PreferencesFactory)
Class.forName(factoryName, false,
ClassLoader.getSystemClassLoader())
.newInstance();
} catch (Exception ex) {
try {
// workaround for javaws, plugin,
// load factory class using non-system classloader
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new java.security.AllPermission());
}
return (PreferencesFactory)
Class.forName(factoryName, false,
Thread.currentThread()
.getContextClassLoader())
.newInstance();
} catch (Exception e) {
throw new InternalError(
"Can't instantiate Preferences factory "
+ factoryName, e);
}
}
}
return AccessController.doPrivileged(
new PrivilegedAction<PreferencesFactory>() {
public PreferencesFactory run() {
return factory1();}});
}
private static PreferencesFactory factory1() {
// 2. Try service provider interface
Iterator<PreferencesFactory> itr = ServiceLoader
.load(PreferencesFactory.class, ClassLoader.getSystemClassLoader())
.iterator();
// choose first provider instance
while (itr.hasNext()) {
try {
return itr.next();
} catch (ServiceConfigurationError sce) {
if (sce.getCause() instanceof SecurityException) {
// Ignore the security exception, try the next provider
continue;
}
throw sce;
}
}
// 3. Use platform-specific system-wide default
String osName = System.getProperty("os.name");
String platformFactory;
if (osName.startsWith("Windows")) {
platformFactory = "java.util.prefs.WindowsPreferencesFactory";
} else if (osName.contains("OS X")) {
platformFactory = "java.util.prefs.MacOSXPreferencesFactory";
} else {
platformFactory = "java.util.prefs.FileSystemPreferencesFactory";
}
try {
return (PreferencesFactory)
Class.forName(platformFactory, false,
Preferences.class.getClassLoader()).newInstance();
} catch (Exception e) {
throw new InternalError(
"Can't instantiate platform default Preferences factory "
+ platformFactory, e);
}
}
}
- 在【WindowsPreferencesFactory】类中,发现【2】中调用的是【WindowsPreferencesFactory】 的userRoot() ,返回的是 WindowsPreferences.userRoot;
package java.util.prefs;
/**
* Implementation of <tt>PreferencesFactory</tt> to return
* WindowsPreferences objects.
*
* @author Konstantin Kladko
* @see Preferences
* @see WindowsPreferences
* @since 1.4
*/
class WindowsPreferencesFactory implements PreferencesFactory {
/**
* 最后调用的是这个方法
* Returns WindowsPreferences.userRoot
*/
public Preferences userRoot() {
return WindowsPreferences.userRoot;
}
/**
* Returns WindowsPreferences.systemRoot
*/
public Preferences systemRoot() {
return WindowsPreferences.systemRoot;
}
}
- WindowsPreferences.userRoot返回的结果如下,最终拼接起来就是
HKEY_CURRENT_USER\Software\JavaSoft\Prefs
package java.util.prefs;
class WindowsPreferences extends AbstractPreferences{
/**
* Mount point for <tt>Preferences</tt>' user root.
*/
private static final int USER_ROOT_NATIVE_HANDLE = HKEY_CURRENT_USER;
/**
* Windows registry path to <tt>Preferences</tt>'s root nodes.
*/
private static final byte[] WINDOWS_ROOT_PATH =
stringToByteArray("Software\\JavaSoft\\Prefs");
/**
* User root node.
*/
static final Preferences userRoot =
new WindowsPreferences(USER_ROOT_NATIVE_HANDLE, WINDOWS_ROOT_PATH);
//node方法会调用此方法处理子节点
protected AbstractPreferences childSpi(String name) {
return new WindowsPreferences(this, name);
}
/**
* Constructs a <tt>WindowsPreferences</tt> node, creating underlying
* Windows registry node and all its Windows parents, if they are not yet
* created.
* Logs a warning message, if Windows Registry is unavailable.
*/
private WindowsPreferences(WindowsPreferences parent, String name) {
super(parent, name);
int parentNativeHandle = parent.openKey(KEY_CREATE_SUB_KEY, KEY_READ);
if (parentNativeHandle == NULL_NATIVE_HANDLE) {
// if here, openKey failed and logged
isBackingStoreAvailable = false;
return;
}
int[] result =
WindowsRegCreateKeyEx1(parentNativeHandle, toWindowsName(name));
if (result[ERROR_CODE] != ERROR_SUCCESS) {
logger().warning("Could not create windows registry node " +
byteArrayToString(windowsAbsolutePath()) +
" at root 0x" + Integer.toHexString(rootNativeHandle()) +
". Windows RegCreateKeyEx(...) returned error code " +
result[ERROR_CODE] + ".");
isBackingStoreAvailable = false;
return;
}
newNode = (result[DISPOSITION] == REG_CREATED_NEW_KEY);
closeKey(parentNativeHandle);
closeKey(result[NATIVE_HANDLE]);
}
}
看完userRoot()再去看它的node方法: userRoot().node(nodeName©);
node() 方法是对传进来的class的路径进行递归处理,一层一层的拼接
我们传进来的class是【LicenseVerify.class】,这个类的路径假设是【com.abc.license】
最终,node() 方法执行完,会得到一个完整的路径(此路径便是下面要查询的路径): HKEY_CURRENT_USER\Software\JavaSoft\Prefs\com\abc\license
package java.util.prefs;
public abstract class AbstractPreferences extends Preferences {
public Preferences node(String path) {
synchronized(lock) {
if (removed)
throw new IllegalStateException("Node has been removed.");
if (path.equals(""))
return this;
if (path.equals("/"))
return root;
if (path.charAt(0) != '/')
return node(new StringTokenizer(path, "/", true));
}
// Absolute path. Note that we've dropped our lock to avoid deadlock
return root.node(new StringTokenizer(path.substring(1), "/", true));
}
}
/**
* tokenizer contains <name> {'/' <name>}*
*/
private Preferences node(StringTokenizer path) {
String token = path.nextToken();
if (token.equals("/")) // Check for consecutive slashes
throw new IllegalArgumentException("Consecutive slashes in path");
synchronized(lock) {
AbstractPreferences child = kidCache.get(token);
if (child == null) {
if (token.length() > MAX_NAME_LENGTH)
throw new IllegalArgumentException(
"Node name " + token + " too long");
child = childSpi(token);
if (child.newNode)
enqueueNodeAddedEvent(child);
kidCache.put(token, child);
}
if (!path.hasMoreTokens())
return child;
path.nextToken(); // Consume slash
if (!path.hasMoreTokens())
throw new IllegalArgumentException("Path ends with slash");
//针对class路径的递归处理
return child.node(path);
}
}
- 至此第一步:
初始化证书读取环境已经完成,读取到了license在Windows注册表存放的路径
然后开始:
读取证书具体信息
getLicenseKey() 方法便是读取具体的证书字节信息也就是详细信息(只不过此时读到的只是字节,后期还要进行各种转换,但是我们此时只关注从哪里读取,不关注后期怎么转换)
/**
* Decrypts, decompresses, decodes and verifies the current license key,
* validates its license content and returns it.
*
* @param notary the license notary used to verify the current license key
* - may <em>not</em> be {@code null}.
* @throws NoLicenseInstalledException if no license key is installed.
* @throws Exception for any other reason.
* Note that you should always use
* {@link Throwable#getLocalizedMessage()} to get a (possibly
* localized) meaningful detail message.
* @return A clone of the verified and validated content of the license key
* - {@code null} is never returned.
* @see #validate(LicenseContent)
*/
protected synchronized LicenseContent verify(final LicenseNotary notary)
throws Exception {
GenericCertificate certificate = getCertificate();
//此时读取内容是空白的,所以不会直接return
if (null != certificate)
return (LicenseContent) certificate.getContent();
// Load license key from preferences,
//这一步便是读取具体的证书字节信息,读取到之后进行各种转换格式化
final byte[] key = getLicenseKey();
if (null == key)
throw new NoLicenseInstalledException(getLicenseParam().getSubject());
certificate = getPrivacyGuard().key2cert(key);
notary.verify(certificate);
final LicenseContent content = (LicenseContent) certificate.getContent();
validate(content);
setCertificate(certificate);
return content;
}
- getLicenseKey()往下走,最终会调用到【AbstractPreferences】 的 getByteArray() 方法中的 get() 方法,继续调用 get() 方法中的 getSpi() 方法
package de.schlichtherle.license;
public class LicenseManager implements LicenseCreator, LicenseVerifier {
//
// Methods for license keys.
// Note that in contrast to the methods of the privacy guard,
// the following methods may have side effects (preferences, file system).
//
/**
* 直接调用到此方法,此时的PREFERENCES_KEY = "license"
* Returns the current license key.
*/
protected synchronized byte[] getLicenseKey() {
return getLicenseParam().getPreferences().getByteArray(PREFERENCES_KEY, null);
}
}
package java.util.prefs;
public abstract class AbstractPreferences extends Preferences {
/**
* Implements the <tt>getByteArray</tt> method as per the specification in
* {@link Preferences#getByteArray(String,byte[])}.
*
* @param key key whose associated value is to be returned as a byte array.
* @param def the value to be returned in the event that this
* preference node has no value associated with <tt>key</tt>
* or the associated value cannot be interpreted as a byte array.
* @return the byte array value represented by the string associated with
* <tt>key</tt> in this preference node, or <tt>def</tt> if the
* associated value does not exist or cannot be interpreted as
* a byte array.
* @throws IllegalStateException if this node (or an ancestor) has been
* removed with the {@link #removeNode()} method.
* @throws NullPointerException if <tt>key</tt> is <tt>null</tt>. (A
* <tt>null</tt> value for <tt>def</tt> <i>is</i> permitted.)
*/
public byte[] getByteArray(String key, byte[] def) {
byte[] result = def;
//最终调用到此get方法
String value = get(key, null);
try {
if (value != null)
result = Base64.base64ToByteArray(value);
}
catch (RuntimeException e) {
// Ignoring exception causes specified default to be returned
}
return result;
}
/**
* Implements the <tt>get</tt> method as per the specification in
* {@link Preferences#get(String,String)}.
*
* <p>This implementation first checks to see if <tt>key</tt> is
* <tt>null</tt> throwing a <tt>NullPointerException</tt> if this is
* the case. Then it obtains this preference node's lock,
* checks that the node has not been removed, invokes {@link
* #getSpi(String)}, and returns the result, unless the <tt>getSpi</tt>
* invocation returns <tt>null</tt> or throws an exception, in which case
* this invocation returns <tt>def</tt>.
*
* @param key key whose associated value is to be returned.
* @param def the value to be returned in the event that this
* preference node has no value associated with <tt>key</tt>.
* @return the value associated with <tt>key</tt>, or <tt>def</tt>
* if no value is associated with <tt>key</tt>.
* @throws IllegalStateException if this node (or an ancestor) has been
* removed with the {@link #removeNode()} method.
* @throws NullPointerException if key is <tt>null</tt>. (A
* <tt>null</tt> default <i>is</i> permitted.)
*/
public String get(String key, String def) {
if (key==null)
throw new NullPointerException("Null key");
synchronized(lock) {
if (removed)
throw new IllegalStateException("Node has been removed.");
String result = null;
try {
result = getSpi(key);
} catch (Exception e) {
// Ignoring exception causes default to be returned
}
return (result==null ? def : result);
}
}
}
-
【AbstractPreferences】 的 getSpi() 调用会走到【WindowsPreferences】的 getSpi() 方法,此方法中最重要的一句就是 WindowsRegQueryValueEx,这个方法是:为windows提供的查询注册表的api接口,因为是native,就不继续往下了
WindowsRegQueryValueEx方法有两个参数:1)javaName 往上追溯便是: “license”
2)nativeHandle :要查询的注册表的key,我们在第【5】步的时候已经得出了一个明文的路径,不过在这个方法中是转换为int了
因此得出我们最终要去注册表查询 HKEY_CURRENT_USER\Software\JavaSoft\Prefs\com\abc\license 路径下的文件,便是license存储证书的位置。
package java.util.prefs;
class WindowsPreferences extends AbstractPreferences{
/**
* Implements <tt>AbstractPreferences</tt> <tt>getSpi()</tt> method.
* Gets a string value from the underlying Windows registry node.
* Logs a warning, if Windows registry is unavailable.
* @see #putSpi(String, String)
*/
protected String getSpi(String javaName) {
int nativeHandle = openKey(KEY_QUERY_VALUE);
if (nativeHandle == NULL_NATIVE_HANDLE) {
return null;
}
Object resultObject = WindowsRegQueryValueEx(nativeHandle,
toWindowsName(javaName));
if (resultObject == null) {
closeKey(nativeHandle);
return null;
}
closeKey(nativeHandle);
return toJavaValueString((byte[]) resultObject);
}
/**
* java为windows提供的查询注册表的api接口
* Java wrapper for Windows registry API RegQueryValueEx()
*/
private static native byte[] WindowsRegQueryValueEx(int hKey,
byte[] valueName);
}
关于native的一些解释
- 使用 native 关键字说明这个方法是原生函数,也就是这个方法是用 C/C++ 语言实现的,并且被编译成了 DLL,由 Java去调用
- native 的意思就是通知操作系统,这个函数你必须给我实现,因为我要使用。
- 所以 native 关键字的函数都是操作系统实现的,Java 只能调用
关于怎么在linux中找出存储位置,此处不再具体说明
可以根据windows中找到的存储路径的文件夹名字,比如直接搜索此案例中windows路径是:HKEY_CURRENT_USER\Software\JavaSoft\Prefs\com\abc\license
在linux中搜索【abc】 文件夹的位置 find / -name abc
注册表格式如图所示