diff --git a/.gitignore b/.gitignore
index 6143e53..3f695ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,22 +1,25 @@
-# Compiled class file
-*.class
+*/target/*
+!.mvn/wrapper/maven-wrapper.jar
-# Log file
-*.log
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
-# BlueJ files
-*.ctxt
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-*.war
-*.ear
-*.zip
-*.tar.gz
-*.rar
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
+### NetBeans ###
+nbproject/private/
+build/
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
\ No newline at end of file
diff --git a/ClientDemo/.gitignore b/ClientDemo/.gitignore
new file mode 100644
index 0000000..82eca33
--- /dev/null
+++ b/ClientDemo/.gitignore
@@ -0,0 +1,25 @@
+/target/
+!.mvn/wrapper/maven-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/build/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
\ No newline at end of file
diff --git a/ClientDemo/.mvn/wrapper/maven-wrapper.jar b/ClientDemo/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..9cc84ea
Binary files /dev/null and b/ClientDemo/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/ClientDemo/.mvn/wrapper/maven-wrapper.properties b/ClientDemo/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..b573bb5
--- /dev/null
+++ b/ClientDemo/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip
diff --git a/ClientDemo/mvnw b/ClientDemo/mvnw
new file mode 100644
index 0000000..5bf251c
--- /dev/null
+++ b/ClientDemo/mvnw
@@ -0,0 +1,225 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Migwn, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+ # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+echo $MAVEN_PROJECTBASEDIR
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/ClientDemo/mvnw.cmd b/ClientDemo/mvnw.cmd
new file mode 100644
index 0000000..019bd74
--- /dev/null
+++ b/ClientDemo/mvnw.cmd
@@ -0,0 +1,143 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/ClientDemo/pom.xml b/ClientDemo/pom.xml
new file mode 100644
index 0000000..2fdf94a
--- /dev/null
+++ b/ClientDemo/pom.xml
@@ -0,0 +1,165 @@
+
+
+ 4.0.0
+
+ cn.zifangsky
+ clientdemo
+ 0.0.1-SNAPSHOT
+ jar
+
+ ClientDemo
+ Demo project for License
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.3.RELEASE
+
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+ true
+
+ 4.12
+
+ 3.7
+ 2.6
+ 1.11
+
+ 4.5.5
+
+ 1.2.47
+
+ 4.0.1
+
+ 1.33
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ junit
+ junit
+ ${junit}
+ test
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3}
+
+
+ commons-io
+ commons-io
+ ${commons-io}
+
+
+ commons-codec
+ commons-codec
+ ${commons-codec}
+
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ ${httpclient}
+
+
+
+
+ com.alibaba
+ fastjson
+ ${fastjson}
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ ${javax.servlet-api}
+ provided
+
+
+
+
+ com.fasterxml.jackson.jaxrs
+ jackson-jaxrs-json-provider
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.slf4j
+ jcl-over-slf4j
+
+
+
+
+ de.schlichtherle.truelicense
+ truelicense-core
+ ${truelicense}
+
+
+
+ net.sourceforge.nekohtml
+ nekohtml
+ 1.9.22
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+ src/main/resources
+
+
+ src/main/java
+
+ **/*.properties
+ **/*.xml
+ **/*.tld
+
+ false
+
+
+
+
+
diff --git a/ClientDemo/src/main/java/cn/zifangsky/ClientDemoApplication.java b/ClientDemo/src/main/java/cn/zifangsky/ClientDemoApplication.java
new file mode 100644
index 0000000..c1037a1
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/ClientDemoApplication.java
@@ -0,0 +1,19 @@
+package cn.zifangsky;
+
+import org.springframework.boot.Banner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.ServletComponentScan;
+import org.springframework.context.annotation.PropertySource;
+
+@SpringBootApplication
+@ServletComponentScan
+@PropertySource({"license-config.properties"}) //加载额外的配置
+public class ClientDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication application = new SpringApplication(ClientDemoApplication.class);
+ application.setBannerMode(Banner.Mode.CONSOLE);
+ application.run(args);
+ }
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/common/SpringContextUtils.java b/ClientDemo/src/main/java/cn/zifangsky/common/SpringContextUtils.java
new file mode 100644
index 0000000..f801168
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/common/SpringContextUtils.java
@@ -0,0 +1,72 @@
+package cn.zifangsky.common;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+/**
+ * 自定义Spring工具类
+ * @author zifangsky
+ */
+@Component
+public class SpringContextUtils implements ApplicationContextAware {
+ private static ApplicationContext applicationContext;
+
+ @Override
+ public void setApplicationContext(ApplicationContext context) throws BeansException {
+ applicationContext = context;
+ }
+
+ /**
+ * 获取ApplicationContext对象
+ * @return
+ */
+ public static ApplicationContext getApplicationContext(){
+ return applicationContext;
+ }
+
+ /**
+ * 根据bean的名称获取bean
+ * @param name
+ * @return
+ */
+ public static Object getBeanByName(String name){
+ return applicationContext.getBean(name);
+ }
+
+ /**
+ * 根据bean的class来查找对象
+ * @param
+ * @param c
+ * @return
+ */
+ public static T getBeanByClass(Class c){
+ return applicationContext.getBean(c);
+ }
+
+ /**
+ * 根据bean的class来查找所有的对象(包括子类)
+ * @param
+ * @param c
+ * @return
+ */
+ public static Map getBeansByClass(Class c){
+ return applicationContext.getBeansOfType(c);
+ }
+
+ /**
+ * 获取HttpServletRequest
+ * @return
+ */
+ public static HttpServletRequest getRequest() {
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
+ HttpServletRequest request = attributes.getRequest();
+ return request;
+ }
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/config/RestTemplateConfig.java b/ClientDemo/src/main/java/cn/zifangsky/config/RestTemplateConfig.java
new file mode 100644
index 0000000..623b57d
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/config/RestTemplateConfig.java
@@ -0,0 +1,94 @@
+package cn.zifangsky.config;
+
+import org.apache.http.Header;
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.message.BasicHeader;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Configuration
+public class RestTemplateConfig {
+
+ /**
+ * 返回RestTemplate
+ * @param factory ClientHttpRequestFactory
+ * @return RestTemplate
+ */
+ @Bean
+ public RestTemplate restTemplate(ClientHttpRequestFactory factory){
+ return new RestTemplate(factory);
+ }
+
+ /**
+ * ClientHttpRequestFactory接口的第一种实现方式,即:
+ * SimpleClientHttpRequestFactory:底层使用java.net包提供的方式创建Http连接请求
+ * @return
+ */
+// @Bean
+// public SimpleClientHttpRequestFactory simpleClientHttpRequestFactory(){
+// SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
+//
+// requestFactory.setReadTimeout(5000);
+// requestFactory.setConnectTimeout(5000);
+//
+// return requestFactory;
+// }
+
+ /**
+ * ClientHttpRequestFactory接口的另一种实现方式(推荐使用),即:
+ * HttpComponentsClientHttpRequestFactory:底层使用Httpclient连接池的方式创建Http连接请求
+ * @return HttpComponentsClientHttpRequestFactory
+ */
+ @Bean
+ public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory(){
+ //Httpclient连接池,长连接保持30秒
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
+
+ //设置总连接数
+ connectionManager.setMaxTotal(1000);
+ //设置同路由的并发数
+ connectionManager.setDefaultMaxPerRoute(1000);
+
+ //设置header
+ List headers = new ArrayList();
+ headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.04"));
+ headers.add(new BasicHeader("Accept-Encoding", "gzip, deflate"));
+ headers.add(new BasicHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3"));
+ headers.add(new BasicHeader("Connection", "keep-alive"));
+
+ //创建HttpClient
+ HttpClient httpClient = HttpClientBuilder.create()
+ .setConnectionManager(connectionManager)
+ .setDefaultHeaders(headers)
+ .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)) //设置重试次数
+ .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy()) //设置保持长连接
+ .build();
+
+ //创建HttpComponentsClientHttpRequestFactory实例
+ HttpComponentsClientHttpRequestFactory requestFactory =
+ new HttpComponentsClientHttpRequestFactory(httpClient);
+
+ //设置客户端和服务端建立连接的超时时间
+ requestFactory.setConnectTimeout(5000);
+ //设置客户端从服务端读取数据的超时时间
+ requestFactory.setReadTimeout(5000);
+ //设置从连接池获取连接的超时时间,不宜过长
+ requestFactory.setConnectionRequestTimeout(200);
+ //缓冲请求数据,默认为true。通过POST或者PUT大量发送数据时,建议将此更改为false,以免耗尽内存
+ requestFactory.setBufferRequestBody(false);
+
+ return requestFactory;
+ }
+
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/config/WebMvcConfig.java b/ClientDemo/src/main/java/cn/zifangsky/config/WebMvcConfig.java
new file mode 100644
index 0000000..5942406
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/config/WebMvcConfig.java
@@ -0,0 +1,34 @@
+package cn.zifangsky.config;
+
+import cn.zifangsky.license.LicenseCheckInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * Web相关配置
+ * @author zifangsky
+ * @date 2018/7/9
+ * @since 1.0.0
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ /**
+ * 视图控制器
+ */
+ @Override
+ public void addViewControllers(ViewControllerRegistry registry) {
+ registry.addViewController("/index").setViewName("index");
+ registry.addViewController("/login").setViewName("login");
+ }
+
+ /**
+ * 添加拦截器
+ */
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(new LicenseCheckInterceptor()).addPathPatterns("/check");
+ }
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/controller/LoginController.java b/ClientDemo/src/main/java/cn/zifangsky/controller/LoginController.java
new file mode 100644
index 0000000..d816618
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/controller/LoginController.java
@@ -0,0 +1,56 @@
+package cn.zifangsky.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 登录
+ * @author zifangsky
+ * @date 2018/7/9
+ * @since 1.0.0
+ */
+@Controller
+public class LoginController {
+
+ /**
+ * 模拟登录验证
+ * @author zifangsky
+ * @date 2018/7/9 17:09
+ * @since 1.0.0
+ * @param username 用户名
+ * @param password 密码
+ * @return java.util.Map
+ */
+ @PostMapping("/check")
+ @ResponseBody
+ public Map test(@RequestParam(required = true) String username, @RequestParam(required = true) String password){
+ Map result = new HashMap<>(1);
+ System.out.println(MessageFormat.format("用户名:{0},密码:{1}",username,password));
+
+ //模拟登录
+ System.out.println("模拟登录流程");
+ result.put("code",200);
+
+ return result;
+ }
+
+ /**
+ * 用户首页
+ * @author zifangsky
+ * @date 2018/7/9 17:10
+ * @since 1.0.0
+ * @return java.lang.String
+ */
+ @RequestMapping("/userIndex")
+ public String userIndex(){
+ return "userIndex";
+ }
+
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/AbstractServerInfos.java b/ClientDemo/src/main/java/cn/zifangsky/license/AbstractServerInfos.java
new file mode 100644
index 0000000..4424a1c
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/AbstractServerInfos.java
@@ -0,0 +1,144 @@
+package cn.zifangsky.license;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * 用于获取客户服务器的基本信息,如:IP、Mac地址、CPU序列号、主板序列号等
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public abstract class AbstractServerInfos {
+ private static Logger logger = LogManager.getLogger(AbstractServerInfos.class);
+
+ /**
+ * 组装需要额外校验的License参数
+ * @author zifangsky
+ * @date 2018/4/23 14:23
+ * @since 1.0.0
+ * @return demo.LicenseCheckModel
+ */
+ public LicenseCheckModel getServerInfos(){
+ LicenseCheckModel result = new LicenseCheckModel();
+
+ try {
+ result.setIpAddress(this.getIpAddress());
+ result.setMacAddress(this.getMacAddress());
+ result.setCpuSerial(this.getCPUSerial());
+ result.setMainBoardSerial(this.getMainBoardSerial());
+ }catch (Exception e){
+ logger.error("获取服务器硬件信息失败",e);
+ }
+
+ return result;
+ }
+
+ /**
+ * 获取IP地址
+ * @author zifangsky
+ * @date 2018/4/23 11:32
+ * @since 1.0.0
+ * @return java.util.List
+ */
+ protected abstract List getIpAddress() throws Exception;
+
+ /**
+ * 获取Mac地址
+ * @author zifangsky
+ * @date 2018/4/23 11:32
+ * @since 1.0.0
+ * @return java.util.List
+ */
+ protected abstract List getMacAddress() throws Exception;
+
+ /**
+ * 获取CPU序列号
+ * @author zifangsky
+ * @date 2018/4/23 11:35
+ * @since 1.0.0
+ * @return java.lang.String
+ */
+ protected abstract String getCPUSerial() throws Exception;
+
+ /**
+ * 获取主板序列号
+ * @author zifangsky
+ * @date 2018/4/23 11:35
+ * @since 1.0.0
+ * @return java.lang.String
+ */
+ protected abstract String getMainBoardSerial() throws Exception;
+
+ /**
+ * 获取当前服务器所有符合条件的InetAddress
+ * @author zifangsky
+ * @date 2018/4/23 17:38
+ * @since 1.0.0
+ * @return java.util.List
+ */
+ protected List getLocalAllInetAddress() throws Exception {
+ List result = new ArrayList<>(4);
+
+ // 遍历所有的网络接口
+ for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) {
+ NetworkInterface iface = (NetworkInterface) networkInterfaces.nextElement();
+ // 在所有的接口下再遍历IP
+ for (Enumeration inetAddresses = iface.getInetAddresses(); inetAddresses.hasMoreElements(); ) {
+ InetAddress inetAddr = (InetAddress) inetAddresses.nextElement();
+
+ //排除LoopbackAddress、SiteLocalAddress、LinkLocalAddress、MulticastAddress类型的IP地址
+ if(!inetAddr.isLoopbackAddress() /*&& !inetAddr.isSiteLocalAddress()*/
+ && !inetAddr.isLinkLocalAddress() && !inetAddr.isMulticastAddress()){
+ result.add(inetAddr);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * 获取某个网络接口的Mac地址
+ * @author zifangsky
+ * @date 2018/4/23 18:08
+ * @since 1.0.0
+ * @param
+ * @return void
+ */
+ protected String getMacByInetAddress(InetAddress inetAddr){
+ try {
+ byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();
+ StringBuffer stringBuffer = new StringBuffer();
+
+ for(int i=0;i
+ * 用于将公私钥存储文件存放到其他磁盘位置而不是项目中
+ * @author zifangsky
+ * @date 2018/4/26 18:28
+ * @since 1.0.0
+ * @param
+ * @return java.io.InputStream
+ */
+ @Override
+ public InputStream getStream() throws IOException {
+ final InputStream in = new FileInputStream(new File(storePath));
+ if (null == in){
+ throw new FileNotFoundException(storePath);
+ }
+
+ return in;
+ }
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/CustomLicenseManager.java b/ClientDemo/src/main/java/cn/zifangsky/license/CustomLicenseManager.java
new file mode 100644
index 0000000..ce90b97
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/CustomLicenseManager.java
@@ -0,0 +1,292 @@
+package cn.zifangsky.license;
+
+import de.schlichtherle.license.LicenseContent;
+import de.schlichtherle.license.LicenseContentException;
+import de.schlichtherle.license.LicenseManager;
+import de.schlichtherle.license.LicenseNotary;
+import de.schlichtherle.license.LicenseParam;
+import de.schlichtherle.license.NoLicenseInstalledException;
+import de.schlichtherle.xml.GenericCertificate;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.beans.XMLDecoder;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 自定义LicenseManager,用于增加额外的服务器硬件信息校验
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public class CustomLicenseManager extends LicenseManager{
+ private static Logger logger = LogManager.getLogger(CustomLicenseManager.class);
+
+ //XML编码
+ private static final String XML_CHARSET = "UTF-8";
+ //默认BUFSIZE
+ private static final int DEFAULT_BUFSIZE = 8 * 1024;
+
+ public CustomLicenseManager() {
+
+ }
+
+ public CustomLicenseManager(LicenseParam param) {
+ super(param);
+ }
+
+ /**
+ * 复写create方法
+ * @author zifangsky
+ * @date 2018/4/23 10:36
+ * @since 1.0.0
+ * @param
+ * @return byte[]
+ */
+ @Override
+ protected synchronized byte[] create(
+ LicenseContent content,
+ LicenseNotary notary)
+ throws Exception {
+ initialize(content);
+ this.validateCreate(content);
+ final GenericCertificate certificate = notary.sign(content);
+ return getPrivacyGuard().cert2key(certificate);
+ }
+
+ /**
+ * 复写install方法,其中validate方法调用本类中的validate方法,校验IP地址、Mac地址等其他信息
+ * @author zifangsky
+ * @date 2018/4/23 10:40
+ * @since 1.0.0
+ * @param
+ * @return de.schlichtherle.license.LicenseContent
+ */
+ @Override
+ protected synchronized LicenseContent install(
+ final byte[] key,
+ final LicenseNotary notary)
+ throws Exception {
+ final GenericCertificate certificate = getPrivacyGuard().key2cert(key);
+
+ notary.verify(certificate);
+ final LicenseContent content = (LicenseContent)this.load(certificate.getEncoded());
+ this.validate(content);
+ setLicenseKey(key);
+ setCertificate(certificate);
+
+ return content;
+ }
+
+ /**
+ * 复写verify方法,调用本类中的validate方法,校验IP地址、Mac地址等其他信息
+ * @author zifangsky
+ * @date 2018/4/23 10:40
+ * @since 1.0.0
+ * @param
+ * @return de.schlichtherle.license.LicenseContent
+ */
+ @Override
+ protected synchronized LicenseContent verify(final LicenseNotary notary)
+ throws Exception {
+ GenericCertificate certificate = getCertificate();
+
+ // 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)this.load(certificate.getEncoded());
+ this.validate(content);
+ setCertificate(certificate);
+
+ return content;
+ }
+
+ /**
+ * 校验生成证书的参数信息
+ * @author zifangsky
+ * @date 2018/5/2 15:43
+ * @since 1.0.0
+ * @param content 证书正文
+ */
+ protected synchronized void validateCreate(final LicenseContent content)
+ throws LicenseContentException {
+ final LicenseParam param = getLicenseParam();
+
+ final Date now = new Date();
+ final Date notBefore = content.getNotBefore();
+ final Date notAfter = content.getNotAfter();
+ if (null != notAfter && now.after(notAfter)){
+ throw new LicenseContentException("证书失效时间不能早于当前时间");
+ }
+ if (null != notBefore && null != notAfter && notAfter.before(notBefore)){
+ throw new LicenseContentException("证书生效时间不能晚于证书失效时间");
+ }
+ final String consumerType = content.getConsumerType();
+ if (null == consumerType){
+ throw new LicenseContentException("用户类型不能为空");
+ }
+ }
+
+
+ /**
+ * 复写validate方法,增加IP地址、Mac地址等其他信息校验
+ * @author zifangsky
+ * @date 2018/4/23 10:40
+ * @since 1.0.0
+ * @param content LicenseContent
+ */
+ @Override
+ protected synchronized void validate(final LicenseContent content)
+ throws LicenseContentException {
+ //1. 首先调用父类的validate方法
+ super.validate(content);
+
+ //2. 然后校验自定义的License参数
+ //License中可被允许的参数信息
+ LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();
+ //当前服务器真实的参数信息
+ LicenseCheckModel serverCheckModel = getServerInfos();
+
+ if(expectedCheckModel != null && serverCheckModel != null){
+ //校验IP地址
+ if(!checkIpAddress(expectedCheckModel.getIpAddress(),serverCheckModel.getIpAddress())){
+ throw new LicenseContentException("当前服务器的IP没在授权范围内");
+ }
+
+ //校验Mac地址
+ if(!checkIpAddress(expectedCheckModel.getMacAddress(),serverCheckModel.getMacAddress())){
+ throw new LicenseContentException("当前服务器的Mac地址没在授权范围内");
+ }
+
+ //校验主板序列号
+ if(!checkSerial(expectedCheckModel.getMainBoardSerial(),serverCheckModel.getMainBoardSerial())){
+ throw new LicenseContentException("当前服务器的主板序列号没在授权范围内");
+ }
+
+ //校验CPU序列号
+ if(!checkSerial(expectedCheckModel.getCpuSerial(),serverCheckModel.getCpuSerial())){
+ throw new LicenseContentException("当前服务器的CPU序列号没在授权范围内");
+ }
+ }else{
+ throw new LicenseContentException("不能获取服务器硬件信息");
+ }
+ }
+
+
+ /**
+ * 重写XMLDecoder解析XML
+ * @author zifangsky
+ * @date 2018/4/25 14:02
+ * @since 1.0.0
+ * @param encoded XML类型字符串
+ * @return java.lang.Object
+ */
+ private Object load(String encoded){
+ BufferedInputStream inputStream = null;
+ XMLDecoder decoder = null;
+ try {
+ inputStream = new BufferedInputStream(new ByteArrayInputStream(encoded.getBytes(XML_CHARSET)));
+
+ decoder = new XMLDecoder(new BufferedInputStream(inputStream, DEFAULT_BUFSIZE),null,null);
+
+ return decoder.readObject();
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ if(decoder != null){
+ decoder.close();
+ }
+ if(inputStream != null){
+ inputStream.close();
+ }
+ } catch (Exception e) {
+ logger.error("XMLDecoder解析XML失败",e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 获取当前服务器需要额外校验的License参数
+ * @author zifangsky
+ * @date 2018/4/23 14:33
+ * @since 1.0.0
+ * @return demo.LicenseCheckModel
+ */
+ private LicenseCheckModel getServerInfos(){
+ //操作系统类型
+ String osName = System.getProperty("os.name").toLowerCase();
+ AbstractServerInfos abstractServerInfos = null;
+
+ //根据不同操作系统类型选择不同的数据获取方法
+ if (osName.startsWith("windows")) {
+ abstractServerInfos = new WindowsServerInfos();
+ } else if (osName.startsWith("linux")) {
+ abstractServerInfos = new LinuxServerInfos();
+ }else{//其他服务器类型
+ abstractServerInfos = new LinuxServerInfos();
+ }
+
+ return abstractServerInfos.getServerInfos();
+ }
+
+ /**
+ * 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内
+ * 如果存在IP在可被允许的IP/Mac地址范围内,则返回true
+ * @author zifangsky
+ * @date 2018/4/24 11:44
+ * @since 1.0.0
+ * @return boolean
+ */
+ private boolean checkIpAddress(List expectedList,List serverList){
+ if(expectedList != null && expectedList.size() > 0){
+ if(serverList != null && serverList.size() > 0){
+ for(String expected : expectedList){
+ if(serverList.contains(expected.trim())){
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }else {
+ return true;
+ }
+ }
+
+ /**
+ * 校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内
+ * @author zifangsky
+ * @date 2018/4/24 14:38
+ * @since 1.0.0
+ * @param
+ * @return boolean
+ */
+ private boolean checkSerial(String expectedSerial,String serverSerial){
+ if(StringUtils.isNotBlank(expectedSerial)){
+ if(StringUtils.isNotBlank(serverSerial)){
+ if(expectedSerial.equals(serverSerial)){
+ return true;
+ }
+ }
+
+ return false;
+ }else{
+ return true;
+ }
+ }
+
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/LicenseCheckInterceptor.java b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseCheckInterceptor.java
new file mode 100644
index 0000000..1d8829d
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseCheckInterceptor.java
@@ -0,0 +1,43 @@
+package cn.zifangsky.license;
+
+import com.alibaba.fastjson.JSON;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * LicenseCheckInterceptor
+ *
+ * @author zifangsky
+ * @date 2018/4/25
+ * @since 1.0.0
+ */
+public class LicenseCheckInterceptor extends HandlerInterceptorAdapter{
+ private static Logger logger = LogManager.getLogger(LicenseCheckInterceptor.class);
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ LicenseVerify licenseVerify = new LicenseVerify();
+
+ //校验证书是否有效
+ boolean verifyResult = licenseVerify.verify();
+
+ if(verifyResult){
+ return true;
+ }else{
+ response.setCharacterEncoding("utf-8");
+ Map result = new HashMap<>(1);
+ result.put("result","您的证书无效,请核查服务器是否取得授权或重新申请证书!");
+
+ response.getWriter().write(JSON.toJSONString(result));
+
+ return false;
+ }
+ }
+
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/LicenseCheckListener.java b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseCheckListener.java
new file mode 100644
index 0000000..4adfaa0
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseCheckListener.java
@@ -0,0 +1,76 @@
+package cn.zifangsky.license;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.stereotype.Component;
+
+/**
+ * 在项目启动时安装证书
+ *
+ * @author zifangsky
+ * @date 2018/4/24
+ * @since 1.0.0
+ */
+@Component
+public class LicenseCheckListener implements ApplicationListener {
+ private static Logger logger = LogManager.getLogger(LicenseCheckListener.class);
+
+ /**
+ * 证书subject
+ */
+ @Value("${license.subject}")
+ private String subject;
+
+ /**
+ * 公钥别称
+ */
+ @Value("${license.publicAlias}")
+ private String publicAlias;
+
+ /**
+ * 访问公钥库的密码
+ */
+ @Value("${license.storePass}")
+ private String storePass;
+
+ /**
+ * 证书生成路径
+ */
+ @Value("${license.licensePath}")
+ private String licensePath;
+
+ /**
+ * 密钥库存储路径
+ */
+ @Value("${license.publicKeysStorePath}")
+ private String publicKeysStorePath;
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ //root application context 没有parent
+ ApplicationContext context = event.getApplicationContext().getParent();
+ if(context == null){
+ if(StringUtils.isNotBlank(licensePath)){
+ logger.info("++++++++ 开始安装证书 ++++++++");
+
+ LicenseVerifyParam param = new LicenseVerifyParam();
+ param.setSubject(subject);
+ param.setPublicAlias(publicAlias);
+ param.setStorePass(storePass);
+ param.setLicensePath(licensePath);
+ param.setPublicKeysStorePath(publicKeysStorePath);
+
+ LicenseVerify licenseVerify = new LicenseVerify();
+ //安装证书
+ licenseVerify.install(param);
+
+ logger.info("++++++++ 证书安装结束 ++++++++");
+ }
+ }
+ }
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/LicenseCheckModel.java b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseCheckModel.java
new file mode 100644
index 0000000..7ce412c
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseCheckModel.java
@@ -0,0 +1,77 @@
+package cn.zifangsky.license;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 自定义需要校验的License参数
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public class LicenseCheckModel implements Serializable{
+
+ private static final long serialVersionUID = 8600137500316662317L;
+ /**
+ * 可被允许的IP地址
+ */
+ private List ipAddress;
+
+ /**
+ * 可被允许的MAC地址
+ */
+ private List macAddress;
+
+ /**
+ * 可被允许的CPU序列号
+ */
+ private String cpuSerial;
+
+ /**
+ * 可被允许的主板序列号
+ */
+ private String mainBoardSerial;
+
+ public List getIpAddress() {
+ return ipAddress;
+ }
+
+ public void setIpAddress(List ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ public List getMacAddress() {
+ return macAddress;
+ }
+
+ public void setMacAddress(List macAddress) {
+ this.macAddress = macAddress;
+ }
+
+ public String getCpuSerial() {
+ return cpuSerial;
+ }
+
+ public void setCpuSerial(String cpuSerial) {
+ this.cpuSerial = cpuSerial;
+ }
+
+ public String getMainBoardSerial() {
+ return mainBoardSerial;
+ }
+
+ public void setMainBoardSerial(String mainBoardSerial) {
+ this.mainBoardSerial = mainBoardSerial;
+ }
+
+ @Override
+ public String toString() {
+ return "LicenseCheckModel{" +
+ "ipAddress=" + ipAddress +
+ ", macAddress=" + macAddress +
+ ", cpuSerial='" + cpuSerial + '\'' +
+ ", mainBoardSerial='" + mainBoardSerial + '\'' +
+ '}';
+ }
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/LicenseManagerHolder.java b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseManagerHolder.java
new file mode 100644
index 0000000..2f8dd04
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseManagerHolder.java
@@ -0,0 +1,29 @@
+package cn.zifangsky.license;
+
+import de.schlichtherle.license.LicenseManager;
+import de.schlichtherle.license.LicenseParam;
+
+/**
+ * de.schlichtherle.license.LicenseManager的单例
+ *
+ * @author zifangsky
+ * @date 2018/4/19
+ * @since 1.0.0
+ */
+public class LicenseManagerHolder {
+
+ private static volatile LicenseManager LICENSE_MANAGER;
+
+ public static LicenseManager getInstance(LicenseParam param){
+ if(LICENSE_MANAGER == null){
+ synchronized (LicenseManagerHolder.class){
+ if(LICENSE_MANAGER == null){
+ LICENSE_MANAGER = new CustomLicenseManager(param);
+ }
+ }
+ }
+
+ return LICENSE_MANAGER;
+ }
+
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/LicenseVerify.java b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseVerify.java
new file mode 100644
index 0000000..90823b2
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseVerify.java
@@ -0,0 +1,96 @@
+package cn.zifangsky.license;
+
+import de.schlichtherle.license.*;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.File;
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.prefs.Preferences;
+
+/**
+ * License校验类
+ *
+ * @author zifangsky
+ * @date 2018/4/20
+ * @since 1.0.0
+ */
+public class LicenseVerify {
+ private static Logger logger = LogManager.getLogger(LicenseVerify.class);
+
+ /**
+ * 安装License证书
+ * @author zifangsky
+ * @date 2018/4/20 16:26
+ * @since 1.0.0
+ */
+ public synchronized LicenseContent install(LicenseVerifyParam param){
+ LicenseContent result = null;
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ //1. 安装证书
+ try{
+ LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));
+ licenseManager.uninstall();
+
+ result = licenseManager.install(new File(param.getLicensePath()));
+ logger.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",format.format(result.getNotBefore()),format.format(result.getNotAfter())));
+ }catch (Exception e){
+ logger.error("证书安装失败!",e);
+ }
+
+ return result;
+ }
+
+ /**
+ * 校验License证书
+ * @author zifangsky
+ * @date 2018/4/20 16:26
+ * @since 1.0.0
+ * @return boolean
+ */
+ public boolean verify(){
+ LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ //2. 校验证书
+ try {
+ LicenseContent licenseContent = licenseManager.verify();
+// System.out.println(licenseContent.getSubject());
+
+ logger.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter())));
+ return true;
+ }catch (Exception e){
+ logger.error("证书校验失败!",e);
+ return false;
+ }
+ }
+
+ /**
+ * 初始化证书生成参数
+ * @author zifangsky
+ * @date 2018/4/20 10:56
+ * @since 1.0.0
+ * @param param License校验类需要的参数
+ * @return de.schlichtherle.license.LicenseParam
+ */
+ private LicenseParam initLicenseParam(LicenseVerifyParam param){
+ Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);
+
+ CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
+
+ KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class
+ ,param.getPublicKeysStorePath()
+ ,param.getPublicAlias()
+ ,param.getStorePass()
+ ,null);
+
+ return new DefaultLicenseParam(param.getSubject()
+ ,preferences
+ ,publicStoreParam
+ ,cipherParam);
+ }
+
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/LicenseVerifyParam.java b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseVerifyParam.java
new file mode 100644
index 0000000..7f45ba3
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/LicenseVerifyParam.java
@@ -0,0 +1,99 @@
+package cn.zifangsky.license;
+
+/**
+ * License校验类需要的参数
+ *
+ * @author zifangsky
+ * @date 2018/4/20
+ * @since 1.0.0
+ */
+public class LicenseVerifyParam {
+
+ /**
+ * 证书subject
+ */
+ private String subject;
+
+ /**
+ * 公钥别称
+ */
+ private String publicAlias;
+
+ /**
+ * 访问公钥库的密码
+ */
+ private String storePass;
+
+ /**
+ * 证书生成路径
+ */
+ private String licensePath;
+
+ /**
+ * 密钥库存储路径
+ */
+ private String publicKeysStorePath;
+
+ public LicenseVerifyParam() {
+
+ }
+
+ public LicenseVerifyParam(String subject, String publicAlias, String storePass, String licensePath, String publicKeysStorePath) {
+ this.subject = subject;
+ this.publicAlias = publicAlias;
+ this.storePass = storePass;
+ this.licensePath = licensePath;
+ this.publicKeysStorePath = publicKeysStorePath;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public String getPublicAlias() {
+ return publicAlias;
+ }
+
+ public void setPublicAlias(String publicAlias) {
+ this.publicAlias = publicAlias;
+ }
+
+ public String getStorePass() {
+ return storePass;
+ }
+
+ public void setStorePass(String storePass) {
+ this.storePass = storePass;
+ }
+
+ public String getLicensePath() {
+ return licensePath;
+ }
+
+ public void setLicensePath(String licensePath) {
+ this.licensePath = licensePath;
+ }
+
+ public String getPublicKeysStorePath() {
+ return publicKeysStorePath;
+ }
+
+ public void setPublicKeysStorePath(String publicKeysStorePath) {
+ this.publicKeysStorePath = publicKeysStorePath;
+ }
+
+ @Override
+ public String toString() {
+ return "LicenseVerifyParam{" +
+ "subject='" + subject + '\'' +
+ ", publicAlias='" + publicAlias + '\'' +
+ ", storePass='" + storePass + '\'' +
+ ", licensePath='" + licensePath + '\'' +
+ ", publicKeysStorePath='" + publicKeysStorePath + '\'' +
+ '}';
+ }
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/LinuxServerInfos.java b/ClientDemo/src/main/java/cn/zifangsky/license/LinuxServerInfos.java
new file mode 100644
index 0000000..1735f6e
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/LinuxServerInfos.java
@@ -0,0 +1,90 @@
+package cn.zifangsky.license;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.InetAddress;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 用于获取客户Linux服务器的基本信息
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public class LinuxServerInfos extends AbstractServerInfos{
+
+ @Override
+ protected List getIpAddress() throws Exception {
+ List result = null;
+
+ //获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected List getMacAddress() throws Exception {
+ List result = null;
+
+ //1. 获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ //2. 获取所有网络接口的Mac地址
+ result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected String getCPUSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用dmidecode命令获取CPU序列号
+ String[] shell = {"/bin/bash","-c","dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
+ Process process = Runtime.getRuntime().exec(shell);
+ process.getOutputStream().close();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+
+ String line = reader.readLine().trim();
+ if(StringUtils.isNotBlank(line)){
+ serialNumber = line;
+ }
+
+ reader.close();
+ return serialNumber;
+ }
+
+ @Override
+ protected String getMainBoardSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用dmidecode命令获取主板序列号
+ String[] shell = {"/bin/bash","-c","dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};
+ Process process = Runtime.getRuntime().exec(shell);
+ process.getOutputStream().close();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+
+ String line = reader.readLine().trim();
+ if(StringUtils.isNotBlank(line)){
+ serialNumber = line;
+ }
+
+ reader.close();
+ return serialNumber;
+ }
+}
diff --git a/ClientDemo/src/main/java/cn/zifangsky/license/WindowsServerInfos.java b/ClientDemo/src/main/java/cn/zifangsky/license/WindowsServerInfos.java
new file mode 100644
index 0000000..c9d3cd5
--- /dev/null
+++ b/ClientDemo/src/main/java/cn/zifangsky/license/WindowsServerInfos.java
@@ -0,0 +1,89 @@
+package cn.zifangsky.license;
+
+import java.net.InetAddress;
+import java.util.List;
+import java.util.Scanner;
+import java.util.stream.Collectors;
+
+/**
+ * 用于获取客户Windows服务器的基本信息
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public class WindowsServerInfos extends AbstractServerInfos{
+
+ @Override
+ protected List getIpAddress() throws Exception {
+ List result = null;
+
+ //获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected List getMacAddress() throws Exception {
+ List result = null;
+
+ //1. 获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ //2. 获取所有网络接口的Mac地址
+ result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected String getCPUSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用WMIC获取CPU序列号
+ Process process = Runtime.getRuntime().exec("wmic cpu get processorid");
+ process.getOutputStream().close();
+ Scanner scanner = new Scanner(process.getInputStream());
+
+ if(scanner != null && scanner.hasNext()){
+ scanner.next();
+ }
+
+ if(scanner.hasNext()){
+ serialNumber = scanner.next().trim();
+ }
+
+ scanner.close();
+ return serialNumber;
+ }
+
+ @Override
+ protected String getMainBoardSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用WMIC获取主板序列号
+ Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");
+ process.getOutputStream().close();
+ Scanner scanner = new Scanner(process.getInputStream());
+
+ if(scanner != null && scanner.hasNext()){
+ scanner.next();
+ }
+
+ if(scanner.hasNext()){
+ serialNumber = scanner.next().trim();
+ }
+
+ scanner.close();
+ return serialNumber;
+ }
+}
diff --git a/ClientDemo/src/main/resources/application-dev.properties b/ClientDemo/src/main/resources/application-dev.properties
new file mode 100644
index 0000000..1eb99ce
--- /dev/null
+++ b/ClientDemo/src/main/resources/application-dev.properties
@@ -0,0 +1,18 @@
+server.port=7080
+
+#session超时时间
+server.servlet.session.timeout=300
+
+#是否启用debug模式
+#debug=true
+
+logging.level.org.springframework.web=DEBUG
+#logging.file=D:/runtime/test.log
+
+#thymeleaf start
+spring.thymeleaf.mode=LEGACYHTML5
+spring.thymeleaf.prefix=classpath:/templates/
+spring.thymeleaf.suffix=.html
+spring.thymeleaf.template-resolver-order=0
+spring.thymeleaf.cache=false
+#thymeleaf end
\ No newline at end of file
diff --git a/ClientDemo/src/main/resources/application-prod.properties b/ClientDemo/src/main/resources/application-prod.properties
new file mode 100644
index 0000000..a3ac65c
--- /dev/null
+++ b/ClientDemo/src/main/resources/application-prod.properties
@@ -0,0 +1 @@
+server.port=8080
\ No newline at end of file
diff --git a/ClientDemo/src/main/resources/application.properties b/ClientDemo/src/main/resources/application.properties
new file mode 100644
index 0000000..66d6d0c
--- /dev/null
+++ b/ClientDemo/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+#使用哪个环境的配置
+spring.profiles.active=dev
diff --git a/ClientDemo/src/main/resources/banner.txt b/ClientDemo/src/main/resources/banner.txt
new file mode 100644
index 0000000..f7febe4
--- /dev/null
+++ b/ClientDemo/src/main/resources/banner.txt
@@ -0,0 +1,22 @@
+${AnsiColor.BLUE}
+ _ooOoo_
+ o8888888o
+ 88" . "88
+ (| ^_^ |)
+ O\ = /O
+ ____/`---'\____
+ .' \\| | `.
+ / \\||| : ||| \
+ / _||||| -:- |||||- \
+ | | \\\ - / | |
+ | \_| ''\---/'' | |
+ \ .-\__ `-` ___/-. /
+ ___`. .' /--.--\ `. . ___
+ ."" '< `.___\_<|>_/___.' >'"".
+ | | : `- \`.;`\ _ /`;.`/ - ` : | |
+ \ \ `-. \_ __\ /__ _/ .-` / /
+ ========`-.____`-.___\_____/___.-`____.-'========
+ `=---='
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 佛祖保佑 永无BUG
+
\ No newline at end of file
diff --git a/ClientDemo/src/main/resources/license-config.properties b/ClientDemo/src/main/resources/license-config.properties
new file mode 100644
index 0000000..8de928f
--- /dev/null
+++ b/ClientDemo/src/main/resources/license-config.properties
@@ -0,0 +1,6 @@
+#License相关配置
+license.subject=license_demo
+license.publicAlias=publicCert
+license.storePass=public_password1234
+license.licensePath=C:/Users/zifangsky/Desktop/license_demo/license.lic
+license.publicKeysStorePath=C:/Users/zifangsky/Desktop/license_demo/publicCerts.keystore
diff --git a/ClientDemo/src/main/resources/static/css/style.css b/ClientDemo/src/main/resources/static/css/style.css
new file mode 100644
index 0000000..a49c452
--- /dev/null
+++ b/ClientDemo/src/main/resources/static/css/style.css
@@ -0,0 +1,24 @@
+body{
+ background: url("../img/3.jpg");
+ animation-name:myfirst;
+ animation-duration:20s;
+ /*变换时间*/
+ animation-delay:5s;
+ /*动画开始时间*/
+ animation-iteration-count:infinite;
+ /*下一周期循环播放*/
+ animation-play-state:running;
+ /*动画开始运行*/
+}
+@keyframes myfirst
+{
+ 0% {background:url("../img/3.jpg");}
+ 34% {background:url("../img/2.jpg");}
+ 67% {background:url("../img/1.jpg");}
+ 100% {background:url("../img/3.jpg");}
+}
+.form{background: rgba(255,255,255,0.2);width:400px;margin:120px auto;}
+/*阴影*/
+.fa{display: inline-block;top: 27px;left: 6px;position: relative;color: #ccc;}
+input[type="text"],input[type="password"]{padding-left:26px;}
+.errMsg{color: red;}
\ No newline at end of file
diff --git a/ClientDemo/src/main/resources/static/favicon.ico b/ClientDemo/src/main/resources/static/favicon.ico
new file mode 100644
index 0000000..0ad0e67
Binary files /dev/null and b/ClientDemo/src/main/resources/static/favicon.ico differ
diff --git a/ClientDemo/src/main/resources/static/img/1.jpg b/ClientDemo/src/main/resources/static/img/1.jpg
new file mode 100644
index 0000000..23fd153
Binary files /dev/null and b/ClientDemo/src/main/resources/static/img/1.jpg differ
diff --git a/ClientDemo/src/main/resources/static/img/2.jpg b/ClientDemo/src/main/resources/static/img/2.jpg
new file mode 100644
index 0000000..b794cfe
Binary files /dev/null and b/ClientDemo/src/main/resources/static/img/2.jpg differ
diff --git a/ClientDemo/src/main/resources/static/img/3.jpg b/ClientDemo/src/main/resources/static/img/3.jpg
new file mode 100644
index 0000000..b88bbbb
Binary files /dev/null and b/ClientDemo/src/main/resources/static/img/3.jpg differ
diff --git a/ClientDemo/src/main/resources/static/img/4.jpg b/ClientDemo/src/main/resources/static/img/4.jpg
new file mode 100644
index 0000000..8bea6bd
Binary files /dev/null and b/ClientDemo/src/main/resources/static/img/4.jpg differ
diff --git a/ClientDemo/src/main/resources/static/img/5.jpg b/ClientDemo/src/main/resources/static/img/5.jpg
new file mode 100644
index 0000000..eff17a2
Binary files /dev/null and b/ClientDemo/src/main/resources/static/img/5.jpg differ
diff --git a/ClientDemo/src/main/resources/static/img/6.jpg b/ClientDemo/src/main/resources/static/img/6.jpg
new file mode 100644
index 0000000..0422284
Binary files /dev/null and b/ClientDemo/src/main/resources/static/img/6.jpg differ
diff --git a/ClientDemo/src/main/resources/static/img/7.jpg b/ClientDemo/src/main/resources/static/img/7.jpg
new file mode 100644
index 0000000..f26e836
Binary files /dev/null and b/ClientDemo/src/main/resources/static/img/7.jpg differ
diff --git a/ClientDemo/src/main/resources/templates/index.html b/ClientDemo/src/main/resources/templates/index.html
new file mode 100644
index 0000000..2ffc3ee
--- /dev/null
+++ b/ClientDemo/src/main/resources/templates/index.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ 首页
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ClientDemo/src/main/resources/templates/login.html b/ClientDemo/src/main/resources/templates/login.html
new file mode 100644
index 0000000..40cd92c
--- /dev/null
+++ b/ClientDemo/src/main/resources/templates/login.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+ 登录页面
+ <
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ClientDemo/src/main/resources/templates/userIndex.html b/ClientDemo/src/main/resources/templates/userIndex.html
new file mode 100644
index 0000000..1e989dd
--- /dev/null
+++ b/ClientDemo/src/main/resources/templates/userIndex.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ 用户首页
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ClientDemo/src/test/java/cn/zifangsky/ClientDemoApplicationTests.java b/ClientDemo/src/test/java/cn/zifangsky/ClientDemoApplicationTests.java
new file mode 100644
index 0000000..e471e40
--- /dev/null
+++ b/ClientDemo/src/test/java/cn/zifangsky/ClientDemoApplicationTests.java
@@ -0,0 +1,16 @@
+package cn.zifangsky;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class ClientDemoApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/README.md b/README.md
index d8e0aad..74dd864 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,65 @@
# LicenseDemo
#### 项目介绍
-有关基于Spring的项目中使用 TrueLicense 生成和验证License(服务器许可)的示例代码
+在基于Spring的项目中使用 `TrueLicense `生成和验证`License证书`(服务器许可)的示例代码
-#### 软件架构
-软件架构说明
+#### 技术依赖:
+* `Spring Boot`:项目基础架构,包括提供`基本Web服务`和`定时调度服务`
+* `TrueLicense `:基于`Java`实现的生成和验证服务器许可的简单框架
+#### 环境依赖:
+* `JDK8+`
-#### 安装教程
+#### 两个子项目说明: ####
-1. xxxx
-2. xxxx
-3. xxxx
+- `ServerDemo`:用于**开发者**给客户生成`License证书`的示例代码
+- `ClientDemo`:**模拟需要给客户部署的业务项目**
-#### 使用说明
+#### ServerDemo项目: ####
-1. xxxx
-2. xxxx
-3. xxxx
+对外发布了两个RESTful接口:
-#### 参与贡献
+(1)获取服务器硬件信息 :
-1. Fork 本项目
-2. 新建 Feat_xxx 分支
-3. 提交代码
-4. 新建 Pull Request
+请求地址:`http://127.0.0.1:7000/license/getServerInfos`
+![获取服务器硬件信息](https://www.zifangsky.cn/wp-content/uploads/2018/07/20180710140711.png)
-#### 码云特技
+(2)生成证书 :
+
+请求地址:`http://127.0.0.1:7000/license/generateLicense`
+
+请求时需要在Header中添加一个 **Content-Type** ,其值为:**application/json;charset=UTF-8**。请求参数如下:
+
+```json
+{
+ "subject": "license_demo",
+ "privateAlias": "privateKey",
+ "keyPass": "private_password1234",
+ "storePass": "public_password1234",
+ "licensePath": "C:/Users/zifangsky/Desktop/license_demo/license.lic",
+ "privateKeysStorePath": "C:/Users/zifangsky/Desktop/license_demo/privateKeys.keystore",
+ "issuedTime": "2018-07-10 00:00:01",
+ "expiryTime": "2019-12-31 23:59:59",
+ "consumerType": "User",
+ "consumerAmount": 1,
+ "description": "这是证书描述信息",
+ "licenseCheckModel": {
+ "ipAddress": ["192.168.245.1", "10.0.5.22"],
+ "macAddress": ["00-50-56-C0-00-01", "50-7B-9D-F9-18-41"],
+ "cpuSerial": "BFEBFBFF000406E3",
+ "mainBoardSerial": "L1HF65E00X9"
+ }
+}
+```
+
+![生成证书](https://www.zifangsky.cn/wp-content/uploads/2018/07/20180710141528.png)
+
+#### ClientDemo项目: ####
+
+项目启动时安装证书,通过`cn/zifangsky/license/LicenseCheckListener.java`类实现。用户登录时校验证书的可用性,通过`cn/zifangsky/license/LicenseCheckInterceptor.java`类实现。
+
+#### 特别说明: ####
+
+详细开发思路可以参考我写的这篇文章:[https://www.zifangsky.cn/1277.html](https://www.zifangsky.cn/1277.html)
-1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
-2. 码云官方博客 [blog.gitee.com](https://blog.gitee.com)
-3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解码云上的优秀开源项目
-4. [GVP](https://gitee.com/gvp) 全称是码云最有价值开源项目,是码云综合评定出的优秀开源项目
-5. 码云官方提供的使用手册 [http://git.mydoc.io/](http://git.mydoc.io/)
-6. 码云封面人物是一档用来展示码云会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
\ No newline at end of file
diff --git a/ServerDemo/.gitignore b/ServerDemo/.gitignore
new file mode 100644
index 0000000..82eca33
--- /dev/null
+++ b/ServerDemo/.gitignore
@@ -0,0 +1,25 @@
+/target/
+!.mvn/wrapper/maven-wrapper.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/build/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
\ No newline at end of file
diff --git a/ServerDemo/.mvn/wrapper/maven-wrapper.jar b/ServerDemo/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..9cc84ea
Binary files /dev/null and b/ServerDemo/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/ServerDemo/.mvn/wrapper/maven-wrapper.properties b/ServerDemo/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..b573bb5
--- /dev/null
+++ b/ServerDemo/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip
diff --git a/ServerDemo/mvnw b/ServerDemo/mvnw
new file mode 100644
index 0000000..5bf251c
--- /dev/null
+++ b/ServerDemo/mvnw
@@ -0,0 +1,225 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Migwn, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+ # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+echo $MAVEN_PROJECTBASEDIR
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/ServerDemo/mvnw.cmd b/ServerDemo/mvnw.cmd
new file mode 100644
index 0000000..019bd74
--- /dev/null
+++ b/ServerDemo/mvnw.cmd
@@ -0,0 +1,143 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/ServerDemo/pom.xml b/ServerDemo/pom.xml
new file mode 100644
index 0000000..56f41f2
--- /dev/null
+++ b/ServerDemo/pom.xml
@@ -0,0 +1,165 @@
+
+
+ 4.0.0
+
+ cn.zifangsky
+ serverdemo
+ 0.0.1-SNAPSHOT
+ jar
+
+ ServerDemo
+ Demo project for License
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.3.RELEASE
+
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+ true
+
+ 4.12
+
+ 3.7
+ 2.6
+ 1.11
+
+ 4.5.5
+
+ 1.2.47
+
+ 4.0.1
+
+ 1.33
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ junit
+ junit
+ ${junit}
+ test
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3}
+
+
+ commons-io
+ commons-io
+ ${commons-io}
+
+
+ commons-codec
+ commons-codec
+ ${commons-codec}
+
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ ${httpclient}
+
+
+
+
+ com.alibaba
+ fastjson
+ ${fastjson}
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ ${javax.servlet-api}
+ provided
+
+
+
+
+ com.fasterxml.jackson.jaxrs
+ jackson-jaxrs-json-provider
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.slf4j
+ jcl-over-slf4j
+
+
+
+
+ de.schlichtherle.truelicense
+ truelicense-core
+ ${truelicense}
+
+
+
+ net.sourceforge.nekohtml
+ nekohtml
+ 1.9.22
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+ src/main/resources
+
+
+ src/main/java
+
+ **/*.properties
+ **/*.xml
+ **/*.tld
+
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/ServerDemo/src/main/java/cn/zifangsky/ServerDemoApplication.java b/ServerDemo/src/main/java/cn/zifangsky/ServerDemoApplication.java
new file mode 100644
index 0000000..95dbd34
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/ServerDemoApplication.java
@@ -0,0 +1,16 @@
+package cn.zifangsky;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.ServletComponentScan;
+import org.springframework.context.annotation.PropertySource;
+
+@SpringBootApplication
+@ServletComponentScan
+@PropertySource({"license-config.properties"}) //加载额外的配置
+public class ServerDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ServerDemoApplication.class, args);
+ }
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/common/SpringContextUtils.java b/ServerDemo/src/main/java/cn/zifangsky/common/SpringContextUtils.java
new file mode 100644
index 0000000..f801168
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/common/SpringContextUtils.java
@@ -0,0 +1,72 @@
+package cn.zifangsky.common;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+/**
+ * 自定义Spring工具类
+ * @author zifangsky
+ */
+@Component
+public class SpringContextUtils implements ApplicationContextAware {
+ private static ApplicationContext applicationContext;
+
+ @Override
+ public void setApplicationContext(ApplicationContext context) throws BeansException {
+ applicationContext = context;
+ }
+
+ /**
+ * 获取ApplicationContext对象
+ * @return
+ */
+ public static ApplicationContext getApplicationContext(){
+ return applicationContext;
+ }
+
+ /**
+ * 根据bean的名称获取bean
+ * @param name
+ * @return
+ */
+ public static Object getBeanByName(String name){
+ return applicationContext.getBean(name);
+ }
+
+ /**
+ * 根据bean的class来查找对象
+ * @param
+ * @param c
+ * @return
+ */
+ public static T getBeanByClass(Class c){
+ return applicationContext.getBean(c);
+ }
+
+ /**
+ * 根据bean的class来查找所有的对象(包括子类)
+ * @param
+ * @param c
+ * @return
+ */
+ public static Map getBeansByClass(Class c){
+ return applicationContext.getBeansOfType(c);
+ }
+
+ /**
+ * 获取HttpServletRequest
+ * @return
+ */
+ public static HttpServletRequest getRequest() {
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
+ HttpServletRequest request = attributes.getRequest();
+ return request;
+ }
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/config/RestTemplateConfig.java b/ServerDemo/src/main/java/cn/zifangsky/config/RestTemplateConfig.java
new file mode 100644
index 0000000..623b57d
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/config/RestTemplateConfig.java
@@ -0,0 +1,94 @@
+package cn.zifangsky.config;
+
+import org.apache.http.Header;
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.message.BasicHeader;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Configuration
+public class RestTemplateConfig {
+
+ /**
+ * 返回RestTemplate
+ * @param factory ClientHttpRequestFactory
+ * @return RestTemplate
+ */
+ @Bean
+ public RestTemplate restTemplate(ClientHttpRequestFactory factory){
+ return new RestTemplate(factory);
+ }
+
+ /**
+ * ClientHttpRequestFactory接口的第一种实现方式,即:
+ * SimpleClientHttpRequestFactory:底层使用java.net包提供的方式创建Http连接请求
+ * @return
+ */
+// @Bean
+// public SimpleClientHttpRequestFactory simpleClientHttpRequestFactory(){
+// SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
+//
+// requestFactory.setReadTimeout(5000);
+// requestFactory.setConnectTimeout(5000);
+//
+// return requestFactory;
+// }
+
+ /**
+ * ClientHttpRequestFactory接口的另一种实现方式(推荐使用),即:
+ * HttpComponentsClientHttpRequestFactory:底层使用Httpclient连接池的方式创建Http连接请求
+ * @return HttpComponentsClientHttpRequestFactory
+ */
+ @Bean
+ public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory(){
+ //Httpclient连接池,长连接保持30秒
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
+
+ //设置总连接数
+ connectionManager.setMaxTotal(1000);
+ //设置同路由的并发数
+ connectionManager.setDefaultMaxPerRoute(1000);
+
+ //设置header
+ List headers = new ArrayList();
+ headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.04"));
+ headers.add(new BasicHeader("Accept-Encoding", "gzip, deflate"));
+ headers.add(new BasicHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3"));
+ headers.add(new BasicHeader("Connection", "keep-alive"));
+
+ //创建HttpClient
+ HttpClient httpClient = HttpClientBuilder.create()
+ .setConnectionManager(connectionManager)
+ .setDefaultHeaders(headers)
+ .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)) //设置重试次数
+ .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy()) //设置保持长连接
+ .build();
+
+ //创建HttpComponentsClientHttpRequestFactory实例
+ HttpComponentsClientHttpRequestFactory requestFactory =
+ new HttpComponentsClientHttpRequestFactory(httpClient);
+
+ //设置客户端和服务端建立连接的超时时间
+ requestFactory.setConnectTimeout(5000);
+ //设置客户端从服务端读取数据的超时时间
+ requestFactory.setReadTimeout(5000);
+ //设置从连接池获取连接的超时时间,不宜过长
+ requestFactory.setConnectionRequestTimeout(200);
+ //缓冲请求数据,默认为true。通过POST或者PUT大量发送数据时,建议将此更改为false,以免耗尽内存
+ requestFactory.setBufferRequestBody(false);
+
+ return requestFactory;
+ }
+
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/config/WebMvcConfig.java b/ServerDemo/src/main/java/cn/zifangsky/config/WebMvcConfig.java
new file mode 100644
index 0000000..0235e3e
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/config/WebMvcConfig.java
@@ -0,0 +1,23 @@
+package cn.zifangsky.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * Web相关配置
+ * @author zifangsky
+ * @date 2018/7/9
+ * @since 1.0.0
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ /**
+ * 视图控制器
+ */
+ @Override
+ public void addViewControllers(ViewControllerRegistry registry) {
+ registry.addViewController("/index").setViewName("index");
+ }
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/controller/LicenseCreatorController.java b/ServerDemo/src/main/java/cn/zifangsky/controller/LicenseCreatorController.java
new file mode 100644
index 0000000..83b38ea
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/controller/LicenseCreatorController.java
@@ -0,0 +1,97 @@
+package cn.zifangsky.controller;
+
+import cn.zifangsky.license.LicenseCreator;
+import cn.zifangsky.license.LinuxServerInfos;
+import cn.zifangsky.license.AbstractServerInfos;
+import cn.zifangsky.license.LicenseCheckModel;
+import cn.zifangsky.license.LicenseCreatorParam;
+import cn.zifangsky.license.WindowsServerInfos;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *
+ * 用于生成证书文件,不能放在给客户部署的代码里
+ * @author zifangsky
+ * @date 2018/4/26
+ * @since 1.0.0
+ */
+@RestController
+@RequestMapping("/license")
+public class LicenseCreatorController {
+
+ /**
+ * 证书生成路径
+ */
+ @Value("${license.licensePath}")
+ private String licensePath;
+
+ /**
+ * 获取服务器硬件信息
+ * @author zifangsky
+ * @date 2018/4/26 13:13
+ * @since 1.0.0
+ * @param osName 操作系统类型,如果为空则自动判断
+ * @return com.ccx.models.license.LicenseCheckModel
+ */
+ @RequestMapping(value = "/getServerInfos",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
+ public LicenseCheckModel getServerInfos(@RequestParam(value = "osName",required = false) String osName) {
+ //操作系统类型
+ if(StringUtils.isBlank(osName)){
+ osName = System.getProperty("os.name");
+ }
+ osName = osName.toLowerCase();
+
+ AbstractServerInfos abstractServerInfos = null;
+
+ //根据不同操作系统类型选择不同的数据获取方法
+ if (osName.startsWith("windows")) {
+ abstractServerInfos = new WindowsServerInfos();
+ } else if (osName.startsWith("linux")) {
+ abstractServerInfos = new LinuxServerInfos();
+ }else{//其他服务器类型
+ abstractServerInfos = new LinuxServerInfos();
+ }
+
+ return abstractServerInfos.getServerInfos();
+ }
+
+ /**
+ * 生成证书
+ * @author zifangsky
+ * @date 2018/4/26 13:13
+ * @since 1.0.0
+ * @param param 生成证书需要的参数,如:{"subject":"ccx-models","privateAlias":"privateKey","keyPass":"5T7Zz5Y0dJFcqTxvzkH5LDGJJSGMzQ","storePass":"3538cef8e7","licensePath":"C:/Users/zifangsky/Desktop/license.lic","privateKeysStorePath":"C:/Users/zifangsky/Desktop/privateKeys.keystore","issuedTime":"2018-04-26 14:48:12","expiryTime":"2018-12-31 00:00:00","consumerType":"User","consumerAmount":1,"description":"这是证书描述信息","licenseCheckModel":{"ipAddress":["192.168.245.1","10.0.5.22"],"macAddress":["00-50-56-C0-00-01","50-7B-9D-F9-18-41"],"cpuSerial":"BFEBFBFF000406E3","mainBoardSerial":"L1HF65E00X9"}}
+ * @return java.util.Map
+ */
+ @RequestMapping(value = "/generateLicense",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
+ public Map generateLicense(@RequestBody(required = true) LicenseCreatorParam param) {
+ Map resultMap = new HashMap<>(2);
+
+ if(StringUtils.isBlank(param.getLicensePath())){
+ param.setLicensePath(licensePath);
+ }
+
+ LicenseCreator licenseCreator = new LicenseCreator(param);
+ boolean result = licenseCreator.generateLicense();
+
+ if(result){
+ resultMap.put("result","ok");
+ resultMap.put("msg",param);
+ }else{
+ resultMap.put("result","error");
+ resultMap.put("msg","证书文件生成失败!");
+ }
+
+ return resultMap;
+ }
+
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/license/AbstractServerInfos.java b/ServerDemo/src/main/java/cn/zifangsky/license/AbstractServerInfos.java
new file mode 100644
index 0000000..4424a1c
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/license/AbstractServerInfos.java
@@ -0,0 +1,144 @@
+package cn.zifangsky.license;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * 用于获取客户服务器的基本信息,如:IP、Mac地址、CPU序列号、主板序列号等
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public abstract class AbstractServerInfos {
+ private static Logger logger = LogManager.getLogger(AbstractServerInfos.class);
+
+ /**
+ * 组装需要额外校验的License参数
+ * @author zifangsky
+ * @date 2018/4/23 14:23
+ * @since 1.0.0
+ * @return demo.LicenseCheckModel
+ */
+ public LicenseCheckModel getServerInfos(){
+ LicenseCheckModel result = new LicenseCheckModel();
+
+ try {
+ result.setIpAddress(this.getIpAddress());
+ result.setMacAddress(this.getMacAddress());
+ result.setCpuSerial(this.getCPUSerial());
+ result.setMainBoardSerial(this.getMainBoardSerial());
+ }catch (Exception e){
+ logger.error("获取服务器硬件信息失败",e);
+ }
+
+ return result;
+ }
+
+ /**
+ * 获取IP地址
+ * @author zifangsky
+ * @date 2018/4/23 11:32
+ * @since 1.0.0
+ * @return java.util.List
+ */
+ protected abstract List getIpAddress() throws Exception;
+
+ /**
+ * 获取Mac地址
+ * @author zifangsky
+ * @date 2018/4/23 11:32
+ * @since 1.0.0
+ * @return java.util.List
+ */
+ protected abstract List getMacAddress() throws Exception;
+
+ /**
+ * 获取CPU序列号
+ * @author zifangsky
+ * @date 2018/4/23 11:35
+ * @since 1.0.0
+ * @return java.lang.String
+ */
+ protected abstract String getCPUSerial() throws Exception;
+
+ /**
+ * 获取主板序列号
+ * @author zifangsky
+ * @date 2018/4/23 11:35
+ * @since 1.0.0
+ * @return java.lang.String
+ */
+ protected abstract String getMainBoardSerial() throws Exception;
+
+ /**
+ * 获取当前服务器所有符合条件的InetAddress
+ * @author zifangsky
+ * @date 2018/4/23 17:38
+ * @since 1.0.0
+ * @return java.util.List
+ */
+ protected List getLocalAllInetAddress() throws Exception {
+ List result = new ArrayList<>(4);
+
+ // 遍历所有的网络接口
+ for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) {
+ NetworkInterface iface = (NetworkInterface) networkInterfaces.nextElement();
+ // 在所有的接口下再遍历IP
+ for (Enumeration inetAddresses = iface.getInetAddresses(); inetAddresses.hasMoreElements(); ) {
+ InetAddress inetAddr = (InetAddress) inetAddresses.nextElement();
+
+ //排除LoopbackAddress、SiteLocalAddress、LinkLocalAddress、MulticastAddress类型的IP地址
+ if(!inetAddr.isLoopbackAddress() /*&& !inetAddr.isSiteLocalAddress()*/
+ && !inetAddr.isLinkLocalAddress() && !inetAddr.isMulticastAddress()){
+ result.add(inetAddr);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * 获取某个网络接口的Mac地址
+ * @author zifangsky
+ * @date 2018/4/23 18:08
+ * @since 1.0.0
+ * @param
+ * @return void
+ */
+ protected String getMacByInetAddress(InetAddress inetAddr){
+ try {
+ byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();
+ StringBuffer stringBuffer = new StringBuffer();
+
+ for(int i=0;i
+ * 用于将公私钥存储文件存放到其他磁盘位置而不是项目中
+ * @author zifangsky
+ * @date 2018/4/26 18:28
+ * @since 1.0.0
+ * @param
+ * @return java.io.InputStream
+ */
+ @Override
+ public InputStream getStream() throws IOException {
+ final InputStream in = new FileInputStream(new File(storePath));
+ if (null == in){
+ throw new FileNotFoundException(storePath);
+ }
+
+ return in;
+ }
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/license/CustomLicenseManager.java b/ServerDemo/src/main/java/cn/zifangsky/license/CustomLicenseManager.java
new file mode 100644
index 0000000..af82889
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/license/CustomLicenseManager.java
@@ -0,0 +1,291 @@
+package cn.zifangsky.license;
+
+import de.schlichtherle.license.LicenseContent;
+import de.schlichtherle.license.LicenseContentException;
+import de.schlichtherle.license.LicenseManager;
+import de.schlichtherle.license.LicenseNotary;
+import de.schlichtherle.license.LicenseParam;
+import de.schlichtherle.license.NoLicenseInstalledException;
+import de.schlichtherle.xml.GenericCertificate;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.beans.XMLDecoder;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 自定义LicenseManager,用于增加额外的服务器硬件信息校验
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public class CustomLicenseManager extends LicenseManager{
+ private static Logger logger = LogManager.getLogger(CustomLicenseManager.class);
+
+ //XML编码
+ private static final String XML_CHARSET = "UTF-8";
+ //默认BUFSIZE
+ private static final int DEFAULT_BUFSIZE = 8 * 1024;
+
+ public CustomLicenseManager() {
+
+ }
+
+ public CustomLicenseManager(LicenseParam param) {
+ super(param);
+ }
+
+ /**
+ * 复写create方法
+ * @author zifangsky
+ * @date 2018/4/23 10:36
+ * @since 1.0.0
+ * @param
+ * @return byte[]
+ */
+ @Override
+ protected synchronized byte[] create(
+ LicenseContent content,
+ LicenseNotary notary)
+ throws Exception {
+ initialize(content);
+ this.validateCreate(content);
+ final GenericCertificate certificate = notary.sign(content);
+ return getPrivacyGuard().cert2key(certificate);
+ }
+
+ /**
+ * 复写install方法,其中validate方法调用本类中的validate方法,校验IP地址、Mac地址等其他信息
+ * @author zifangsky
+ * @date 2018/4/23 10:40
+ * @since 1.0.0
+ * @param
+ * @return de.schlichtherle.license.LicenseContent
+ */
+ @Override
+ protected synchronized LicenseContent install(
+ final byte[] key,
+ final LicenseNotary notary)
+ throws Exception {
+ final GenericCertificate certificate = getPrivacyGuard().key2cert(key);
+
+ notary.verify(certificate);
+ final LicenseContent content = (LicenseContent)this.load(certificate.getEncoded());
+ this.validate(content);
+ setLicenseKey(key);
+ setCertificate(certificate);
+
+ return content;
+ }
+
+ /**
+ * 复写verify方法,调用本类中的validate方法,校验IP地址、Mac地址等其他信息
+ * @author zifangsky
+ * @date 2018/4/23 10:40
+ * @since 1.0.0
+ * @param
+ * @return de.schlichtherle.license.LicenseContent
+ */
+ @Override
+ protected synchronized LicenseContent verify(final LicenseNotary notary)
+ throws Exception {
+ GenericCertificate certificate = getCertificate();
+
+ // 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)this.load(certificate.getEncoded());
+ this.validate(content);
+ setCertificate(certificate);
+
+ return content;
+ }
+
+ /**
+ * 校验生成证书的参数信息
+ * @author zifangsky
+ * @date 2018/5/2 15:43
+ * @since 1.0.0
+ * @param content 证书正文
+ */
+ protected synchronized void validateCreate(final LicenseContent content)
+ throws LicenseContentException {
+ final LicenseParam param = getLicenseParam();
+
+ final Date now = new Date();
+ final Date notBefore = content.getNotBefore();
+ final Date notAfter = content.getNotAfter();
+ if (null != notAfter && now.after(notAfter)){
+ throw new LicenseContentException("证书失效时间不能早于当前时间");
+ }
+ if (null != notBefore && null != notAfter && notAfter.before(notBefore)){
+ throw new LicenseContentException("证书生效时间不能晚于证书失效时间");
+ }
+ final String consumerType = content.getConsumerType();
+ if (null == consumerType){
+ throw new LicenseContentException("用户类型不能为空");
+ }
+ }
+
+
+ /**
+ * 复写validate方法,增加IP地址、Mac地址等其他信息校验
+ * @author zifangsky
+ * @date 2018/4/23 10:40
+ * @since 1.0.0
+ * @param content LicenseContent
+ */
+ @Override
+ protected synchronized void validate(final LicenseContent content)
+ throws LicenseContentException {
+ //1. 首先调用父类的validate方法
+ super.validate(content);
+
+ //2. 然后校验自定义的License参数
+ //License中可被允许的参数信息
+ LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();
+ //当前服务器真实的参数信息
+ LicenseCheckModel serverCheckModel = getServerInfos();
+
+ if(expectedCheckModel != null && serverCheckModel != null){
+ //校验IP地址
+ if(!checkIpAddress(expectedCheckModel.getIpAddress(),serverCheckModel.getIpAddress())){
+ throw new LicenseContentException("当前服务器的IP没在授权范围内");
+ }
+
+ //校验Mac地址
+ if(!checkIpAddress(expectedCheckModel.getMacAddress(),serverCheckModel.getMacAddress())){
+ throw new LicenseContentException("当前服务器的Mac地址没在授权范围内");
+ }
+
+ //校验主板序列号
+ if(!checkSerial(expectedCheckModel.getMainBoardSerial(),serverCheckModel.getMainBoardSerial())){
+ throw new LicenseContentException("当前服务器的主板序列号没在授权范围内");
+ }
+
+ //校验CPU序列号
+ if(!checkSerial(expectedCheckModel.getCpuSerial(),serverCheckModel.getCpuSerial())){
+ throw new LicenseContentException("当前服务器的CPU序列号没在授权范围内");
+ }
+ }else{
+ throw new LicenseContentException("不能获取服务器硬件信息");
+ }
+ }
+
+
+ /**
+ * 重写XMLDecoder解析XML
+ * @author zifangsky
+ * @date 2018/4/25 14:02
+ * @since 1.0.0
+ * @param encoded XML类型字符串
+ * @return java.lang.Object
+ */
+ private Object load(String encoded){
+ BufferedInputStream inputStream = null;
+ XMLDecoder decoder = null;
+ try {
+ inputStream = new BufferedInputStream(new ByteArrayInputStream(encoded.getBytes(XML_CHARSET)));
+
+ decoder = new XMLDecoder(new BufferedInputStream(inputStream, DEFAULT_BUFSIZE),null,null);
+
+ return decoder.readObject();
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ if(decoder != null){
+ decoder.close();
+ }
+ if(inputStream != null){
+ inputStream.close();
+ }
+ } catch (Exception e) {
+ logger.error("XMLDecoder解析XML失败",e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 获取当前服务器需要额外校验的License参数
+ * @author zifangsky
+ * @date 2018/4/23 14:33
+ * @since 1.0.0
+ * @return demo.LicenseCheckModel
+ */
+ private LicenseCheckModel getServerInfos(){
+ //操作系统类型
+ String osName = System.getProperty("os.name").toLowerCase();
+ AbstractServerInfos abstractServerInfos = null;
+
+ //根据不同操作系统类型选择不同的数据获取方法
+ if (osName.startsWith("windows")) {
+ abstractServerInfos = new WindowsServerInfos();
+ } else if (osName.startsWith("linux")) {
+ abstractServerInfos = new LinuxServerInfos();
+ }else{//其他服务器类型
+ abstractServerInfos = new LinuxServerInfos();
+ }
+
+ return abstractServerInfos.getServerInfos();
+ }
+
+ /**
+ * 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内
+ * 如果存在IP在可被允许的IP/Mac地址范围内,则返回true
+ * @author zifangsky
+ * @date 2018/4/24 11:44
+ * @since 1.0.0
+ * @return boolean
+ */
+ private boolean checkIpAddress(List expectedList,List serverList){
+ if(expectedList != null && expectedList.size() > 0){
+ if(serverList != null && serverList.size() > 0){
+ for(String expected : expectedList){
+ if(serverList.contains(expected.trim())){
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }else {
+ return true;
+ }
+ }
+
+ /**
+ * 校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内
+ * @author zifangsky
+ * @date 2018/4/24 14:38
+ * @since 1.0.0
+ * @return boolean
+ */
+ private boolean checkSerial(String expectedSerial,String serverSerial){
+ if(StringUtils.isNotBlank(expectedSerial)){
+ if(StringUtils.isNotBlank(serverSerial)){
+ if(expectedSerial.equals(serverSerial)){
+ return true;
+ }
+ }
+
+ return false;
+ }else{
+ return true;
+ }
+ }
+
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/license/LicenseCheckModel.java b/ServerDemo/src/main/java/cn/zifangsky/license/LicenseCheckModel.java
new file mode 100644
index 0000000..7ce412c
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/license/LicenseCheckModel.java
@@ -0,0 +1,77 @@
+package cn.zifangsky.license;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 自定义需要校验的License参数
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public class LicenseCheckModel implements Serializable{
+
+ private static final long serialVersionUID = 8600137500316662317L;
+ /**
+ * 可被允许的IP地址
+ */
+ private List ipAddress;
+
+ /**
+ * 可被允许的MAC地址
+ */
+ private List macAddress;
+
+ /**
+ * 可被允许的CPU序列号
+ */
+ private String cpuSerial;
+
+ /**
+ * 可被允许的主板序列号
+ */
+ private String mainBoardSerial;
+
+ public List getIpAddress() {
+ return ipAddress;
+ }
+
+ public void setIpAddress(List ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ public List getMacAddress() {
+ return macAddress;
+ }
+
+ public void setMacAddress(List macAddress) {
+ this.macAddress = macAddress;
+ }
+
+ public String getCpuSerial() {
+ return cpuSerial;
+ }
+
+ public void setCpuSerial(String cpuSerial) {
+ this.cpuSerial = cpuSerial;
+ }
+
+ public String getMainBoardSerial() {
+ return mainBoardSerial;
+ }
+
+ public void setMainBoardSerial(String mainBoardSerial) {
+ this.mainBoardSerial = mainBoardSerial;
+ }
+
+ @Override
+ public String toString() {
+ return "LicenseCheckModel{" +
+ "ipAddress=" + ipAddress +
+ ", macAddress=" + macAddress +
+ ", cpuSerial='" + cpuSerial + '\'' +
+ ", mainBoardSerial='" + mainBoardSerial + '\'' +
+ '}';
+ }
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/license/LicenseCreator.java b/ServerDemo/src/main/java/cn/zifangsky/license/LicenseCreator.java
new file mode 100644
index 0000000..91e8d9b
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/license/LicenseCreator.java
@@ -0,0 +1,108 @@
+package cn.zifangsky.license;
+
+import de.schlichtherle.license.CipherParam;
+import de.schlichtherle.license.DefaultCipherParam;
+import de.schlichtherle.license.DefaultLicenseParam;
+import de.schlichtherle.license.KeyStoreParam;
+import de.schlichtherle.license.LicenseContent;
+import de.schlichtherle.license.LicenseManager;
+import de.schlichtherle.license.LicenseParam;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.File;
+import java.text.MessageFormat;
+import java.util.prefs.Preferences;
+
+/**
+ * License生成类
+ *
+ * @author zifangsky
+ * @date 2018/4/19
+ * @since 1.0.0
+ */
+public class LicenseCreator {
+ private static Logger logger = LogManager.getLogger(LicenseCreator.class);
+ private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN");
+ private LicenseCreatorParam param;
+
+ public LicenseCreator(LicenseCreatorParam param) {
+ this.param = param;
+ }
+
+ /**
+ * 生成License证书
+ * @author zifangsky
+ * @date 2018/4/20 10:58
+ * @since 1.0.0
+ * @return boolean
+ */
+ public boolean generateLicense(){
+ try {
+ LicenseManager licenseManager = new CustomLicenseManager(initLicenseParam());
+ LicenseContent licenseContent = initLicenseContent();
+
+ licenseManager.store(licenseContent,new File(param.getLicensePath()));
+
+ return true;
+ }catch (Exception e){
+ logger.error(MessageFormat.format("证书生成失败:{0}",param),e);
+ return false;
+ }
+ }
+
+ /**
+ * 初始化证书生成参数
+ * @author zifangsky
+ * @date 2018/4/20 10:56
+ * @since 1.0.0
+ * @return de.schlichtherle.license.LicenseParam
+ */
+ private LicenseParam initLicenseParam(){
+ Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);
+
+ //设置对证书内容加密的秘钥
+ CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
+
+ KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class
+ ,param.getPrivateKeysStorePath()
+ ,param.getPrivateAlias()
+ ,param.getStorePass()
+ ,param.getKeyPass());
+
+ LicenseParam licenseParam = new DefaultLicenseParam(param.getSubject()
+ ,preferences
+ ,privateStoreParam
+ ,cipherParam);
+
+ return licenseParam;
+ }
+
+ /**
+ * 设置证书生成正文信息
+ * @author zifangsky
+ * @date 2018/4/20 10:57
+ * @since 1.0.0
+ * @return de.schlichtherle.license.LicenseContent
+ */
+ private LicenseContent initLicenseContent(){
+ LicenseContent licenseContent = new LicenseContent();
+ licenseContent.setHolder(DEFAULT_HOLDER_AND_ISSUER);
+ licenseContent.setIssuer(DEFAULT_HOLDER_AND_ISSUER);
+
+ licenseContent.setSubject(param.getSubject());
+ licenseContent.setIssued(param.getIssuedTime());
+ licenseContent.setNotBefore(param.getIssuedTime());
+ licenseContent.setNotAfter(param.getExpiryTime());
+ licenseContent.setConsumerType(param.getConsumerType());
+ licenseContent.setConsumerAmount(param.getConsumerAmount());
+ licenseContent.setInfo(param.getDescription());
+
+ //扩展校验服务器硬件信息
+ licenseContent.setExtra(param.getLicenseCheckModel());
+
+ return licenseContent;
+ }
+
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/license/LicenseCreatorParam.java b/ServerDemo/src/main/java/cn/zifangsky/license/LicenseCreatorParam.java
new file mode 100644
index 0000000..6519091
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/license/LicenseCreatorParam.java
@@ -0,0 +1,193 @@
+package cn.zifangsky.license;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * License生成类需要的参数
+ *
+ * @author zifangsky
+ * @date 2018/4/19
+ * @since 1.0.0
+ */
+public class LicenseCreatorParam implements Serializable {
+
+ private static final long serialVersionUID = -7793154252684580872L;
+ /**
+ * 证书subject
+ */
+ private String subject;
+
+ /**
+ * 密钥别称
+ */
+ private String privateAlias;
+
+ /**
+ * 密钥密码(需要妥善保管,不能让使用者知道)
+ */
+ private String keyPass;
+
+ /**
+ * 访问秘钥库的密码
+ */
+ private String storePass;
+
+ /**
+ * 证书生成路径
+ */
+ private String licensePath;
+
+ /**
+ * 密钥库存储路径
+ */
+ private String privateKeysStorePath;
+
+ /**
+ * 证书生效时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+ private Date issuedTime = new Date();
+
+ /**
+ * 证书失效时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+ private Date expiryTime;
+
+ /**
+ * 用户类型
+ */
+ private String consumerType = "user";
+
+ /**
+ * 用户数量
+ */
+ private Integer consumerAmount = 1;
+
+ /**
+ * 描述信息
+ */
+ private String description = "";
+
+ /**
+ * 额外的服务器硬件校验信息
+ */
+ private LicenseCheckModel licenseCheckModel;
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public String getPrivateAlias() {
+ return privateAlias;
+ }
+
+ public void setPrivateAlias(String privateAlias) {
+ this.privateAlias = privateAlias;
+ }
+
+ public String getKeyPass() {
+ return keyPass;
+ }
+
+ public void setKeyPass(String keyPass) {
+ this.keyPass = keyPass;
+ }
+
+ public String getStorePass() {
+ return storePass;
+ }
+
+ public void setStorePass(String storePass) {
+ this.storePass = storePass;
+ }
+
+ public String getLicensePath() {
+ return licensePath;
+ }
+
+ public void setLicensePath(String licensePath) {
+ this.licensePath = licensePath;
+ }
+
+ public String getPrivateKeysStorePath() {
+ return privateKeysStorePath;
+ }
+
+ public void setPrivateKeysStorePath(String privateKeysStorePath) {
+ this.privateKeysStorePath = privateKeysStorePath;
+ }
+
+ public Date getIssuedTime() {
+ return issuedTime;
+ }
+
+ public void setIssuedTime(Date issuedTime) {
+ this.issuedTime = issuedTime;
+ }
+
+ public Date getExpiryTime() {
+ return expiryTime;
+ }
+
+ public void setExpiryTime(Date expiryTime) {
+ this.expiryTime = expiryTime;
+ }
+
+ public String getConsumerType() {
+ return consumerType;
+ }
+
+ public void setConsumerType(String consumerType) {
+ this.consumerType = consumerType;
+ }
+
+ public Integer getConsumerAmount() {
+ return consumerAmount;
+ }
+
+ public void setConsumerAmount(Integer consumerAmount) {
+ this.consumerAmount = consumerAmount;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public LicenseCheckModel getLicenseCheckModel() {
+ return licenseCheckModel;
+ }
+
+ public void setLicenseCheckModel(LicenseCheckModel licenseCheckModel) {
+ this.licenseCheckModel = licenseCheckModel;
+ }
+
+ @Override
+ public String toString() {
+ return "LicenseCreatorParam{" +
+ "subject='" + subject + '\'' +
+ ", privateAlias='" + privateAlias + '\'' +
+ ", keyPass='" + keyPass + '\'' +
+ ", storePass='" + storePass + '\'' +
+ ", licensePath='" + licensePath + '\'' +
+ ", privateKeysStorePath='" + privateKeysStorePath + '\'' +
+ ", issuedTime=" + issuedTime +
+ ", expiryTime=" + expiryTime +
+ ", consumerType='" + consumerType + '\'' +
+ ", consumerAmount=" + consumerAmount +
+ ", description='" + description + '\'' +
+ ", licenseCheckModel=" + licenseCheckModel +
+ '}';
+ }
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/license/LinuxServerInfos.java b/ServerDemo/src/main/java/cn/zifangsky/license/LinuxServerInfos.java
new file mode 100644
index 0000000..42ac30b
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/license/LinuxServerInfos.java
@@ -0,0 +1,90 @@
+package cn.zifangsky.license;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.InetAddress;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 用于获取客户Linux服务器的基本信息
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public class LinuxServerInfos extends AbstractServerInfos {
+
+ @Override
+ protected List getIpAddress() throws Exception {
+ List result = null;
+
+ //获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected List getMacAddress() throws Exception {
+ List result = null;
+
+ //1. 获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ //2. 获取所有网络接口的Mac地址
+ result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected String getCPUSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用dmidecode命令获取CPU序列号
+ String[] shell = {"/bin/bash","-c","dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
+ Process process = Runtime.getRuntime().exec(shell);
+ process.getOutputStream().close();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+
+ String line = reader.readLine().trim();
+ if(StringUtils.isNotBlank(line)){
+ serialNumber = line;
+ }
+
+ reader.close();
+ return serialNumber;
+ }
+
+ @Override
+ protected String getMainBoardSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用dmidecode命令获取主板序列号
+ String[] shell = {"/bin/bash","-c","dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};
+ Process process = Runtime.getRuntime().exec(shell);
+ process.getOutputStream().close();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+
+ String line = reader.readLine().trim();
+ if(StringUtils.isNotBlank(line)){
+ serialNumber = line;
+ }
+
+ reader.close();
+ return serialNumber;
+ }
+}
diff --git a/ServerDemo/src/main/java/cn/zifangsky/license/WindowsServerInfos.java b/ServerDemo/src/main/java/cn/zifangsky/license/WindowsServerInfos.java
new file mode 100644
index 0000000..bec7359
--- /dev/null
+++ b/ServerDemo/src/main/java/cn/zifangsky/license/WindowsServerInfos.java
@@ -0,0 +1,89 @@
+package cn.zifangsky.license;
+
+import java.net.InetAddress;
+import java.util.List;
+import java.util.Scanner;
+import java.util.stream.Collectors;
+
+/**
+ * 用于获取客户Windows服务器的基本信息
+ *
+ * @author zifangsky
+ * @date 2018/4/23
+ * @since 1.0.0
+ */
+public class WindowsServerInfos extends AbstractServerInfos {
+
+ @Override
+ protected List getIpAddress() throws Exception {
+ List result = null;
+
+ //获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected List getMacAddress() throws Exception {
+ List result = null;
+
+ //1. 获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ //2. 获取所有网络接口的Mac地址
+ result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected String getCPUSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用WMIC获取CPU序列号
+ Process process = Runtime.getRuntime().exec("wmic cpu get processorid");
+ process.getOutputStream().close();
+ Scanner scanner = new Scanner(process.getInputStream());
+
+ if(scanner.hasNext()){
+ scanner.next();
+ }
+
+ if(scanner.hasNext()){
+ serialNumber = scanner.next().trim();
+ }
+
+ scanner.close();
+ return serialNumber;
+ }
+
+ @Override
+ protected String getMainBoardSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用WMIC获取主板序列号
+ Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");
+ process.getOutputStream().close();
+ Scanner scanner = new Scanner(process.getInputStream());
+
+ if(scanner.hasNext()){
+ scanner.next();
+ }
+
+ if(scanner.hasNext()){
+ serialNumber = scanner.next().trim();
+ }
+
+ scanner.close();
+ return serialNumber;
+ }
+}
diff --git a/ServerDemo/src/main/resources/application-dev.properties b/ServerDemo/src/main/resources/application-dev.properties
new file mode 100644
index 0000000..a99b60a
--- /dev/null
+++ b/ServerDemo/src/main/resources/application-dev.properties
@@ -0,0 +1,18 @@
+server.port=7000
+
+#session超时时间
+server.servlet.session.timeout=300
+
+#是否启用debug模式
+#debug=true
+
+logging.level.org.springframework.web=DEBUG
+#logging.file=D:/runtime/test.log
+
+#thymeleaf start
+spring.thymeleaf.mode=LEGACYHTML5
+spring.thymeleaf.prefix=classpath:/templates/
+spring.thymeleaf.suffix=.html
+spring.thymeleaf.template-resolver-order=0
+spring.thymeleaf.cache=false
+#thymeleaf end
\ No newline at end of file
diff --git a/ServerDemo/src/main/resources/application-prod.properties b/ServerDemo/src/main/resources/application-prod.properties
new file mode 100644
index 0000000..205ed95
--- /dev/null
+++ b/ServerDemo/src/main/resources/application-prod.properties
@@ -0,0 +1 @@
+server.port=8000
\ No newline at end of file
diff --git a/ServerDemo/src/main/resources/application.properties b/ServerDemo/src/main/resources/application.properties
new file mode 100644
index 0000000..66d6d0c
--- /dev/null
+++ b/ServerDemo/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+#使用哪个环境的配置
+spring.profiles.active=dev
diff --git a/ServerDemo/src/main/resources/banner.txt b/ServerDemo/src/main/resources/banner.txt
new file mode 100644
index 0000000..f7febe4
--- /dev/null
+++ b/ServerDemo/src/main/resources/banner.txt
@@ -0,0 +1,22 @@
+${AnsiColor.BLUE}
+ _ooOoo_
+ o8888888o
+ 88" . "88
+ (| ^_^ |)
+ O\ = /O
+ ____/`---'\____
+ .' \\| | `.
+ / \\||| : ||| \
+ / _||||| -:- |||||- \
+ | | \\\ - / | |
+ | \_| ''\---/'' | |
+ \ .-\__ `-` ___/-. /
+ ___`. .' /--.--\ `. . ___
+ ."" '< `.___\_<|>_/___.' >'"".
+ | | : `- \`.;`\ _ /`;.`/ - ` : | |
+ \ \ `-. \_ __\ /__ _/ .-` / /
+ ========`-.____`-.___\_____/___.-`____.-'========
+ `=---='
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 佛祖保佑 永无BUG
+
\ No newline at end of file
diff --git a/ServerDemo/src/main/resources/license-config.properties b/ServerDemo/src/main/resources/license-config.properties
new file mode 100644
index 0000000..2ab483d
--- /dev/null
+++ b/ServerDemo/src/main/resources/license-config.properties
@@ -0,0 +1,2 @@
+#License相关配置
+license.licensePath=C:/Users/zifangsky/Desktop/license_demo/license.lic
diff --git a/ServerDemo/src/main/resources/static/favicon.ico b/ServerDemo/src/main/resources/static/favicon.ico
new file mode 100644
index 0000000..0ad0e67
Binary files /dev/null and b/ServerDemo/src/main/resources/static/favicon.ico differ
diff --git a/ServerDemo/src/main/resources/static/img/1.jpg b/ServerDemo/src/main/resources/static/img/1.jpg
new file mode 100644
index 0000000..23fd153
Binary files /dev/null and b/ServerDemo/src/main/resources/static/img/1.jpg differ
diff --git a/ServerDemo/src/main/resources/static/img/2.jpg b/ServerDemo/src/main/resources/static/img/2.jpg
new file mode 100644
index 0000000..b794cfe
Binary files /dev/null and b/ServerDemo/src/main/resources/static/img/2.jpg differ
diff --git a/ServerDemo/src/main/resources/static/img/3.jpg b/ServerDemo/src/main/resources/static/img/3.jpg
new file mode 100644
index 0000000..b88bbbb
Binary files /dev/null and b/ServerDemo/src/main/resources/static/img/3.jpg differ
diff --git a/ServerDemo/src/main/resources/static/img/4.jpg b/ServerDemo/src/main/resources/static/img/4.jpg
new file mode 100644
index 0000000..8bea6bd
Binary files /dev/null and b/ServerDemo/src/main/resources/static/img/4.jpg differ
diff --git a/ServerDemo/src/main/resources/static/img/5.jpg b/ServerDemo/src/main/resources/static/img/5.jpg
new file mode 100644
index 0000000..eff17a2
Binary files /dev/null and b/ServerDemo/src/main/resources/static/img/5.jpg differ
diff --git a/ServerDemo/src/main/resources/static/img/6.jpg b/ServerDemo/src/main/resources/static/img/6.jpg
new file mode 100644
index 0000000..0422284
Binary files /dev/null and b/ServerDemo/src/main/resources/static/img/6.jpg differ
diff --git a/ServerDemo/src/main/resources/static/img/7.jpg b/ServerDemo/src/main/resources/static/img/7.jpg
new file mode 100644
index 0000000..f26e836
Binary files /dev/null and b/ServerDemo/src/main/resources/static/img/7.jpg differ
diff --git a/ServerDemo/src/main/resources/templates/index.html b/ServerDemo/src/main/resources/templates/index.html
new file mode 100644
index 0000000..2f1c8fa
--- /dev/null
+++ b/ServerDemo/src/main/resources/templates/index.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ 首页
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ServerDemo/src/test/java/cn/zifangsky/ServerDemoApplicationTests.java b/ServerDemo/src/test/java/cn/zifangsky/ServerDemoApplicationTests.java
new file mode 100644
index 0000000..104d429
--- /dev/null
+++ b/ServerDemo/src/test/java/cn/zifangsky/ServerDemoApplicationTests.java
@@ -0,0 +1,16 @@
+package cn.zifangsky;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class ServerDemoApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}