Fork me on GitHub

Dubbo接口如何在Jmeter中测试,自研Dubbo Plugin for Apache JMeter

最近公司测试需要对DubboRPC接口进行测试,测试工具使用的是Jmeter,按照常规的做法需要包装一个Java请求,再配合JmeterJava Sample去做测试,这种做法是最简单最普遍的,但是这个方法不够灵活和方便,那我们能不能写一个Jmeter Plugin来解决这个问题?让Dubbo RPC接口测试更为方便一些?

那我们先了解一下Jmeter的插件机制

Jmeter Plugin

先来看一下Jmeter的核心组件

  1. Sample 取样器,这个是最主要的组件,测试的内容主要是靠Sample来实现,我们常见的Sample有,HttpSampleFTPSampleJavaSampleSMTPSampleLDAPSample等。
  2. Timer 定时器,主要用于配置sample之间的等待时间,可以查看:org.apache.jmeter.timers.RandomTimer
  3. ConfigElement 配置组件,主要用于定义前置配置。如数据库连接,csv输入数据集等。主要功能是将配置转换为变量设置到JMeter context中。
  4. Assertion 验证Sampler的结果是否符合预期
  5. PostProcessor 一般用于对Sampler结果进行二次加工
  6. Visualizer 将sampler的结果进行可视化展示。
  7. Controller 对sampler进行逻辑控制。
  8. SampleListener 负责处理监听,基于事件机制。一般用于保存sampler的结果等耗费时间的操作。

Jmeter的插件机制比较简单,Jmeter提供了扩展类来支持自定义插件的开发。 继承org.apache.jmeter.samplers.gui.AbstractSamplerGuiorg.apache.jmeter.samplers.AbstractSampler就可以完成一个插件开发。

JMeter的GUI机制

由于Jmeter是一个基于Swing的GUI工具,所以开发插件需要对Java Swing GUI框架有一定了解。 JMeter内部有两种GUI的实现方式。

第一种方式:

直接继承JMeterGUIComponent接口的抽象实现类:

org.apache.jmeter.config.gui.AbstractConfigGui
org.apache.jmeter.assertions.gui.AbstractAssertionGui
org.apache.jmeter.control.gui.AbstractControllerGui
org.apache.jmeter.timers.gui.AbstractTimerGui
org.apache.jmeter.visualizers.gui.AbstractVisualizer
org.apache.jmeter.samplers.gui.AbstractSamplerGui

通过Swing的Bean绑定机制

前者的好处是自由度高,可定制性强,但需要开发者关心GUI控件布局,以及从控件到Model的转换。后者基本不需要开发者接触到GUI层的东西,定义好Bean以及BeanInfo即可。但SampleListener不支持BeanInfo方式定义。

ps.如果java swing比较熟悉的话推荐使用第一种方式,自由度高。

下面是我写的插件DubboSample,主要用于Dubbo RPC接口测试。

Dubbo Plugin for Apache JMeter

jmeter-plugin-dubbo项目已经transfer到dubbo group下

github: jmeter-plugin-dubbo

码云: jmeter-plugin-dubbo

DubboSample使用

支持Jmeter版本

Jmeter版本:3.0

插件安装

插件包可以去github上下载。将插件包放入Jmeter的lib的ext下。

${Path}\apache-jmeter-3.0\lib\ext

如果使用的是:jmeter-plugins-dubbo-1.0.0-SNAPSHOT-jar-with-dependencies.jar包含所有依赖。

如果使用的是:jmeter-plugins-dubbo-1.0.0-SNAPSHOT.jar需要自定添加插件的依赖包,推荐使用上面的包,依赖包版本如下:

dubbo-2.5.3.jar
javassist-3.15.0-GA.jar
zookeeper-3.4.6.jar
zkclient-0.1.jar
jline-0.9.94.jar
netty-3.7.0-Final.jar
slf4j-api-1.7.5.jar
log4j-over-slf4j-1.7.5.jar

插件使用

启动Jmeter添加DubboSample如下图:

添加后能看到DubboSample的具体操作页面,如下图:

根据上图提示传入值即可。

接口以及接口依赖包请添加到classpath下,可以放在apache-jmeter-3.0\lib\ext下,也可以通过下图方式添加:

运行结果

注意事项

  1. 当使用zk,address填入zk地址(集群地址使用”,“分隔),使用dubbo直连,address填写直连地址和服务端口
  2. timeout:服务方法调用超时时间(毫秒)
  3. version:服务版本,与服务提供者的版本一致
  4. retries:远程服务调用重试次数,不包括第一次调用,不需要重试请设为0
  5. cluster:集群方式,可选:failover/failfast/failsafe/failback/forking
  6. 接口需要填写类型完全名称,含包名
  7. 参数支持任何类型,包装类直接使用java.lang下的包装类,小类型使用:int、float、shot、double、long、byte、boolean、char,自定义类使用类完全名称。
  8. 参数值,基础包装类和基础小类型直接使用值,例如:int为1,boolean为true等,自定义类与List或者Map等使用json格式数据。
  9. 更多dubbo参数查看官方文档:http://dubbo.io/books/dubbo-user-book/references/xml/dubbo-reference.html

到这里插件的就介绍完了。世界和平、keep real!

如何将Python脚本打包成可执行文件?

Published on:

我们有时候经常会使用python写一些小工具,在Linux环境下可以很方便运行,因为Linux默认都会有python环境,我们只需要添加python脚本依赖的类库即可执行。但是有的时候我们需要把小工具给到一些麻瓜去用的时候就会出现一些问题,他们大多是在Windows上运行工具,那就必须要先准备python的可运行环境才行,这就给麻瓜们带来了使用成本,我们能否将python脚本打包成windows下可执行文件呢?

接下来让我们先了解一下python有哪些类库可以帮助我们解决这个问题。

这是一个来自Freezing Your Code的统计

Solution Windows Linux OS X Python 3 License One-file mode Zipfile import Eggs pkg_resources support
bbFreeze yes yes yes no MIT no yes yes yes
py2exe yes no no yes MIT yes yes no no
pyInstaller yes yes yes yes GPL yes no yes no
cx_Freeze yes yes yes yes PSF no yes yes no
py2app no no yes yes MIT no yes yes yes

我们能看到有很多类库都可以解决我们的问题,其中pyInstallercx_FreezebbFreeze都不错,pkg_resources新版的pyInstaller貌似是支持的。

我们这里选用pyInstaller尝试一下,因为它各方面支持的是最好的。

PyInstaller原理介绍

PyInstaller其实就是把python解析器和脚本以及脚本的依赖库打包成一个可执行的文件,这和编译成真正的机器码是两回事,所以通过PyInstaller打包成一个可执行文件可能不会提高运行效率,相反可能会降低运行效率,但是它带来的好处就是在运行者的机器上不用安装python和你的脚本依赖的库。在Linux操作系统下,它主要用的binutil工具包里面的lddobjdump命令。

PyInstaller输入你指定的的脚本,首先分析脚本所依赖的其他脚本,然后去查找,复制,把所有相关的脚本收集起来,包括Python解析器,然后把这些文件放在一个目录下,再打包进一个可执行文件里。

这样就可以直接发布输出整个文件夹里面的文件,或者生成可执行文件。你只需要告诉用户,你的App是自我包含的,不需要安装其他包,或某个版本的Python,就可以直接运行。

但是需要注意的是,PyInstaller打包的执行文件,只能在和打包机器系统同样的环境下运行。它不具备可移植性,若需要在不同系统上运行,就必须针对不同平台进行打包。

安装PyInstaller

网络情况可以的话使用pip安装还是很方便的。

pip install pyinstaller

如果网络不稳定,尤其在天朝访问墙外站点是很痛苦的,我们还可以通过下载源码包来安装。

# 在源码包的根目录下执行
python setup.py install

安装完成后,检查安装版本。

pyinstaller --version

使用PyInstaller进行打包

pyinstaller的语法

pyinstaller [options] script [script ...] | specfile

具体命令如何使用可以通过help或官方文档去查询具体的用法。

我这里只说几个注意的点。

-F, --onefile Create a one-file bundled executable.创建一个可执行文件

-w, --windowed, --noconsole去除黑框

# A path to search for imports (like using PYTHONPATH). Multiple paths are allowed, separated by ‘:’, or use this option multiple times

-p DIR, --paths DIR

设置一个可搜索的入口路径,怎么理解呢?如果不指定这个参数打包出来的文件只能在生成它的目录下运行,如果打包时指定参数为-p .打包出来的文件可以放在任意路径下运行,如下示例:

pyinstaller -w -F -p . your.py

参考资料

到这里PyInstaller就简单介绍完毕,感兴趣的可以试一试,我以前使用的是py2exe,其实py2exe也蛮好只不过它需要创建一个py脚本来把需要打包的脚本包含进去,用起来没有PyInstaller方便,希望这个简单的入门可以帮助到需要的朋友。

Keep Real!

Git SSH Key settings and passphrase reset

Published on:

在使用github仓库的时候我们经常会看到clone有两种方式:httpssshhttps的方式使用起来非常简单但是每次在pullpush的时候需要输入密码,一两次还可以忍受但是作为常态是有点崩溃的,这个时候我们可以使用ssh的方式,ssh的好处就是在pullpush的时候可以使用密码也可以不使用密码,但是前提是要设置好ssh key,如果你是Repository的管理员那很好设置,如果不是管理员那就老老实实的使用https的方式,下来我们就说一下使用ssh遇到的问题。

修改用户主目录(home)

当出现下图问题时:

是说明你的.ssh目录设置的有问题,关于用户主目录(home)的问题,一般windows机器安装完git后home都会是C:\Users\用户名这种目录,但是打开Git bash时它无法识别home目录使用到了其他莫名其妙的目录(有的时候会是不存在的目录或是网络盘符),在这个时候就需要变更home目录,变更的方法如下:

环境:windows

Git version 1.x系列

如果是Git version 1.x系列,打开profile文件,文件位置:$\Git\etc\profile($替换成你的盘符)。 在profile中找到:HOME="$(cd "$HOME" ; pwd)"这个位置,在前面增加你想变成的home目录,例如:

# normalize HOME to unix path
HOME="C:\Users\用户名"
HOME="$(cd "$HOME" ; pwd)"

export PATH="$HOME/bin:$PATH"

export GNUPGHOME=~/.gnupg

当修改好之后,重启Git bash即可,输入cd ~/.ssh,会进入你设置好的目录,在这个目录下生成相关的配置文件,如:.ssh、.gnupg、.bash_history、.gitconfig等,如果以前已经有这些文件可以copy到这个目录下直接使用。

Git version 2.x系列

如果是Git version 2.x系列,请设置环境变量,增加HOME的环境变量,目录为:C:\Users\用户名(你想设置的目录),随后重启Git bash即可,输入cd ~/.ssh,会进入你设置好的目录。

按照上面步骤修改好之后,出现下图所示就证明修改完成了。如:

ssh key设置

输入cd ~/.ssh进入home目录使用如下如下方法生成ssh key

  • 可以在Git bash中使用ssh-keygen生成ssh key
  • 还可以使用eclipse的ssh2工具生成,操作如下:Window -> Preferences -> General -> Network Connections -> SSH -> Key Management -> Generate RSA Key
  • 还可以使用TortoiseGit的PuTTY Key Generator工具生成。

方法有很多,生成好的private key用文本编辑器打开复制出来,粘贴到git hub的settings中即可,操作如下:github -> Settings -> SSH and GPG keys -> New SSH key,起个名字粘贴key然后保存即可。

每次都输入passphrase问题

当我们在第一次git clone的时候会提示Enter passphrase,这个时候如果输入了密码,那以后pullpush都需要输入这个密码,就像我下图这样:

我们使用ssh就是图个方便不想输入密码,出现这个问题怎么办呢?

在第一次git clone的时候提示Enter passphrase的时候不要输入密码,直接回车即可,如果输入了密码那就需要重置密码才能解决这个问题。

重置passphrase

打开Git bash使用如下命令重置密码

ssh-keygen -p

输入后根据下图提示操作:

这样就完成了重置密码为空的操作了,后面再pullpush的时候都不会再提示输入密码。

关于凝雨

Published on:
Tags:

大家好,我是凝雨,我不是高手,我也不是牛人,我只是单纯的喜欢编程,我觉得做任何事情还是要跟着自己的心走。

网络信息

个人介绍

我是一个技术型带管理经验的IT从业者,目前在生鲜冷链供应链领域做架构师,擅长底层技术架构,框架编写,团队管理。对分布式、微服务、高并发、高性能、高可用有丰富经验积累和心得体会,喜欢在开源社区游走主要以学习为目的,保持空杯心态学无止境,写代码的时候有洁癖,完美主义者。

兴趣爱好:

  • 钓鱼(越来越专业)
  • 羽毛球(打的一般吧)
  • 台球(偶尔会一杆清)
  • 足球(和老友们没事会踢一场)
  • 唱歌(闲暇时会录几首歌)
  • 看书(技术类、文学类书籍)

我的微信

ps.添加时请备注来源博客,技术讨论否则拒绝

CAS Server强制踢人功能实现方式

前面写过一篇关于CAS Server使用的经验总结,主要总结了CAS Server在使用的时候遇到的一些常见问题,比如说:证书、SLO、集群session处理、自定义用户认证、Ticket持久化等问题,传送门:CAS使用经验总结,纯干货,这次在基础上又增加了一个很常见很普通的问题,那就是踢人功能。

在管理系统这个领域里面踢人功能并不陌生,为了更好的管理用户串用账号,安全等方面考虑,接下来我们就细说一下CAS如何实现踢人的功能。

先说一下踢人功能的场景:

用户A在机器A上登录了APP1,用户A在机器B上登录APP1,在这种情况下后者登录需要踢掉前者的登录状态。

用户A在机器A上登录了APP1,用户B在机器B上登录了APP1,在这种情况下不存在踢人操作。

用户A在机器A上登录了APP1,用户A在机器B上登录了APP2,在这种情况下要分情况了,可以踢也可以不踢,这个就根据产品情况来选择,我们本次测试不能解决这个场景,如何解决我还在摸索中。

要做踢人功能之前先了解一下CAS的认证授权机制是如何完成的?

我这里直接引用官网的架构图:

CAS Server与应用的Session交互图:

其实CAS就是生成维护Ticket信息和应用session做绑定,当然它的Ticket实现还是比较复杂的,有树形关系以及和Service关联关系,从Ticket的源码能看的出来它有root的判断和Service的映射列表。

根据上面对CAS的理解,接下来我们说CAS怎么操作踢人功能?

踢人功能实现思路

在登录认证的时候记录一下,在下次登录获取到登录的人员列表,然后去匹配找出是否存在相同的用户,如果存在相同的用户,就注销掉这个用户的登录信息,这个是常规的思路和做法,但是在CAS里如何去找到切入点来进行判断操作呢?

我们在上一篇中提到了自定义认证逻辑,那么我们就可以继续在认证的这个切入点去进一步分析。

这里要先搞清楚一个概念:AuthenticationAuthorization这两者是不同的。

Authentication:字面意思认证,怎么理解这个认证呢?举个例子:我们每个人都有身份证,比如你去买火车票,买火车票需要出示身份证,那这个身份证就是证明你是你自己的凭证,那这个证明的过程就是认证。

Authorization:字面意思授权,怎么理解这个授权呢?举个例子:继续拿买火车票来说,你刚才出示了身份证证明了你自己,然后给了钱买了一张火车票,铁道部给了你一张票,这个票授权了你可以乘坐X车次X座位的权限其他车次你无权乘坐,那么这张票就是证明你确实买了X车次X座位的凭证,这就是授权。

换回系统的角度来说,认证就是验证用户名密码,授权就是验证你能不能操作某个功能的权限。

理解完认证和授权的区别,我们就开始从认证这块的切入点去看如何操作,CAS提供了这个类TicketRegistry它是管理所有Ticket的接口,通过调用TicketRegistry.getTickets()方法可以获取到所有认证用户的凭证。

/**
 * Retrieve all tickets from the registry.
 *
 * @return collection of tickets currently stored in the registry. Tickets
 * might or might not be valid i.e. expired.
 */
Collection<Ticket> getTickets();

那有了凭证信息就好更进一步操作。

CAS提供了TicketGrantingTicket,这个类是Ticket接口的一个实现类,可以通过TicketGrantingTicket.getAuthentication().getPrincipal().getId()来获取用户的身份。

/**
 * @return the unique id for the Principal
 */
String getId();

getId()返回的是登录的用户名,那拿到了用户名就要考虑如何注销的事情了。

刚才说到了它TicketGrantingTicketTicket接口的实现类,它的t.markTicketExpired()方法就是标记Ticket过期的动作。

/**
 * Mark a ticket as expired.
 */
void markTicketExpired();

光标记过期还不能完成注销操作,还需要通过ticketRegistry.deleteTicket(t.getId())来删除Ticket信息。

/**
 * Remove a specific ticket from the registry.
 * If ticket to delete is TGT then related service tickets are removed as well.
 *
 * @param ticketId The id of the ticket to delete.
 * @return the number of tickets deleted including children.
 */
int deleteTicket(String ticketId);

上面的分析过程看上去是可行的,那我们就来测试一下是否可以达到踢人功能的目的。

踢人功能实现过程

话不多说直接帖实现代码

/**
 * 登录成功,踢掉前一个相同登录的人
 * 
 * @param username
 */
public void forceLogout(final String username) {
	TicketRegistry ticketRegistry = (TicketRegistry) ApplicationContextProvider.getApplicationContext().getBean("ticketRegistry");
	final Collection<Ticket> ticketsInCache = ticketRegistry.getTickets();
	for (final Ticket ticket : ticketsInCache) {
		TicketGrantingTicket t = null;
		try {
			log.info("cast TicketGrantingTicketImpl");
			t = (TicketGrantingTicketImpl) ticket;
		} catch (Exception e) {
			log.error("cast TicketGrantingTicketImpl is error:", e);
			t = ((ServiceTicketImpl) ticket).getGrantingTicket();
		}
		if (t.getAuthentication().getPrincipal().getId().equals(username) && t.getId() != null) {
			/***
			 * 注销方法一 涉及到cookie的删除,但是无法获取response 该方法有待考究 未测试
			 */
			// centralAuthenticationService.destroyTicketGrantingTicket(t.getId());
			/***
			 * 注销方法二
			 */
			// t.expire();
			t.markTicketExpired();
			ticketRegistry.deleteTicket(t.getId());
		}
	}
}

上面的代码放到认证的切入点上调用,切入的位置如下:

  1. 项目:cas-site
  2. 类:org.apereo.cas.adaptors.jdbc.QueryAndEncodeDatabaseAuthenticationHandler
  3. 方法:authenticateUsernamePasswordInternal()createHandlerResult()之前调用。

代码如下:

@Override
protected HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential transformedCredential)
        throws GeneralSecurityException, PreventedException {

    if (StringUtils.isBlank(this.sql) || StringUtils.isBlank(this.algorithmName) || getJdbcTemplate() == null) {
        throw new GeneralSecurityException("Authentication handler is not configured correctly");
    }

    final String username = transformedCredential.getUsername();
    try {
        // Get password and salt
        final Map<String, Object> rows = getJdbcTemplate().queryForMap(this.sql, username);
        final String encodedPassword = rows.get("password").toString();
        final String dbSalt = rows.get("salt").toString();
        SaltPasswordEncoder passwordEncoder = new SaltPasswordEncoder();
        passwordEncoder.setSalt(dbSalt);
        if (!passwordEncoder.matches(transformedCredential.getPassword(), encodedPassword)) {
            throw new FailedLoginException("Password does not match value on record.");
        }
		// 登录成功,踢掉前一个相同登录的人
        forceLogout(username);
        return createHandlerResult(transformedCredential, this.principalFactory.createPrincipal(username), null);

    } catch (final IncorrectResultSizeDataAccessException e) {
        if (e.getActualSize() == 0) {
            throw new AccountNotFoundException(username + " not found with SQL query");
        } else {
            throw new FailedLoginException("Multiple records found for " + username);
        }
    } catch (final DataAccessException e) {
        throw new PreventedException("SQL exception while executing query for " + username, e);
    }

}

cas-site项目我已经放入到了github,在这篇《CAS使用经验总结,纯干货》博文中可以找到。

万事俱备只欠东风了,接下来就是启动程序来验证它。

理想很美好,现实很骨感,出现了如下错误:

javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:282) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at com.sun.proxy.$Proxy175.remove(Unknown Source) ~[?:?]
	at org.apereo.cas.ticket.registry.JpaTicketRegistry.removeTicket(JpaTicketRegistry.java:72) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]
	at org.apereo.cas.ticket.registry.JpaTicketRegistry.deleteTicketsFromResultList(JpaTicketRegistry.java:214) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]
	at org.apereo.cas.ticket.registry.JpaTicketRegistry.deleteTicketGrantingTickets(JpaTicketRegistry.java:244) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]
	at org.apereo.cas.ticket.registry.JpaTicketRegistry.deleteSingleTicket(JpaTicketRegistry.java:158) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]
	at org.apereo.cas.ticket.registry.AbstractTicketRegistry.deleteTicket(AbstractTicketRegistry.java:125) ~[cas-server-core-tickets-5.0.4.jar:5.0.4]
	at org.apereo.cas.ticket.registry.AbstractTicketRegistry$$FastClassBySpringCGLIB$$d3c67a11.invoke(<generated>) ~[cas-server-core-tickets-5.0.4.jar:5.0.4]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:651) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.apereo.cas.ticket.registry.JpaTicketRegistry$$EnhancerBySpringCGLIB$$b6d104b8.deleteTicket(<generated>) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]
	at org.apereo.cas.ticket.registry.AbstractTicketRegistry$$FastClassBySpringCGLIB$$d3c67a11.invoke(<generated>) ~[cas-server-core-tickets-5.0.4.jar:5.0.4]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:651) ~[spring-aop-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at org.apereo.cas.ticket.registry.JpaTicketRegistry$$EnhancerBySpringCGLIB$$ef44b76a.deleteTicket(<generated>) ~[cas-server-support-jpa-ticket-registry-5.0.4.jar:5.0.4]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_31]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_31]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_31]
	at java.lang.reflect.Method.invoke(Method.java:483) ~[?:1.8.0_31]

ps.异常堆栈很长我只截了一部分展示出来。

这个错误是个什么鬼?从异常字面理解:在当前的线程中没有找到可用的事务,无法处理“删除”调用。

这个错误是JPA的错误,因为我的Ticket Registry配置的是JPA的方式,我猜测换成其他方式也会有类似的错误,我去掉JPA采用InMemroy的方式处理Ticket Registry,再次进行测试。

果然出现了类似的错误,如下:

javax.persistence.TransactionRequiredException: no transaction is in progress
	at org.hibernate.internal.SessionImpl.checkTransactionNeeded(SessionImpl.java:3428) ~[hibernate-core-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3362) ~[hibernate-core-5.2.2.Final.jar:5.2.2.Final]
	at org.hibernate.internal.SessionImpl.find(SessionImpl.java:3342) ~[hibernate-core-5.2.2.Final.jar:5.2.2.Final]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_31]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_31]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_31]
	at java.lang.reflect.Method.invoke(Method.java:483) ~[?:1.8.0_31]
	at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:347) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at com.sun.proxy.$Proxy175.find(Unknown Source) ~[?:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_31]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_31]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_31]
	at java.lang.reflect.Method.invoke(Method.java:483) ~[?:1.8.0_31]
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:298) ~[spring-orm-4.3.4.RELEASE.jar:4.3.4.RELEASE]
	at com.sun.proxy.$Proxy175.find(Unknown Source) ~[?:?]

说白了就是没有开启事务被禁止操作了。

这个怎么解决?cas-site采用的是overlays的方式构建,要看具体功能就要翻CAS的源码来看它是如何控制事务的。

于是去翻CAS的源码,翻源码也要讲究技巧的,要不然翻一天都翻不到关键点。

我们这里需要找如何开启事务的代码,还好CAS使用的是Spring来管理事务的,Spring的事务开启无非就这两种:一种是AOP方式,一种是手动方式。

那么AOP的方式可以使用注解(Annotation)也可以使用XML的配置去做。

CAS v5.0.4使用的Spring Boot的方式构建,说白了就是使用编程(Java Config)的方式替换XML的配置方式。而且我们使用的Ticket RegistryJPAJPA的操作肯定要处理事务的,因此我们就锁定到注解(Annotation)的方式和JPA的实现上去找。

最终目标定位到了cas-server-support-jpq-ticket-registry-5.0.4.jar这个包上。

查看这个包的org.apereo.cas.ticket.registry.JpaTicketRegistry类代码

/**
 * JPA implementation of a CAS {@link TicketRegistry}. This implementation of
 * ticket registry is suitable for HA environments.
 *
 * @author Scott Battaglia
 * @author Marvin S. Addison
 * @since 3.2.1
 */
@EnableTransactionManagement(proxyTargetClass = true)
@Transactional(transactionManager = "ticketTransactionManager", readOnly = false)
public class JpaTicketRegistry extends AbstractTicketRegistry {
.....................其余的省略..............................
}

很明显就是我们说的注解(Annotation)的使用方式,我们再次修改代码。

踢人功能代码重构

@EnableTransactionManagement(proxyTargetClass = true)开启代理的方式,那我们就要抽一个接口和一个实现类来做,这里的具体原因就不多说了做多了都明白。

@Transactional(transactionManager = "ticketTransactionManager", readOnly = false)在实现类上直接使用这个注解方式。

直接贴重构后的代码:

新建接口ForceLogoutManager

public interface ForceLogoutManager {

	public void doLogout(final String username);
}

新建实现类ForceLogoutManagerImpl

import java.util.Collection;
import org.apereo.cas.ticket.ServiceTicketImpl;
import org.apereo.cas.ticket.Ticket;
import org.apereo.cas.ticket.TicketGrantingTicket;
import org.apereo.cas.ticket.TicketGrantingTicketImpl;
import org.apereo.cas.ticket.registry.TicketRegistry;
import org.apereo.cas.util.ApplicationContextProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;

@EnableTransactionManagement(proxyTargetClass = true)
@Transactional(transactionManager = "ticketTransactionManager", readOnly = false)
@Component("forceLogoutManager")
public class ForceLogoutManagerImpl implements ForceLogoutManager {
	
	private final Logger log = LoggerFactory.getLogger(this.getClass());

	/**
	 * 登录成功,踢掉前一个相同登录的人
	 * 
	 * @param username
	 */
	public void doLogout(final String username) {
		TicketRegistry ticketRegistry = (TicketRegistry) ApplicationContextProvider.getApplicationContext()
				.getBean("ticketRegistry");
		final Collection<Ticket> ticketsInCache = ticketRegistry.getTickets();
		for (final Ticket ticket : ticketsInCache) {
			TicketGrantingTicket t = null;
			try {
				log.info("cast TicketGrantingTicketImpl");
				t = (TicketGrantingTicketImpl) ticket;
			} catch (Exception e) {
				log.error("cast TicketGrantingTicketImpl is error:", e);
				t = ((ServiceTicketImpl) ticket).getGrantingTicket();
			}
			if (t.getAuthentication().getPrincipal().getId().equals(username) && t.getId() != null) {
				/***
				 * 注销方法一 涉及到cookie的删除,但是无法获取response 该方法有待考究 未测试
				 */
				// centralAuthenticationService.destroyTicketGrantingTicket(t.getId());
				/***
				 * 注销方法二
				 */
				// t.expire();
				t.markTicketExpired();
				ticketRegistry.deleteTicket(t.getId());
			}
		}
	}
}

修改org.apereo.cas.adaptors.jdbc.QueryAndEncodeDatabaseAuthenticationHandlerauthenticateUsernamePasswordInternal方法

public ForceLogoutManager getForceLogoutManager() {
	return (ForceLogoutManager) ApplicationContextProvider.getApplicationContext().getBean("forceLogoutManager");
}

@Override
protected HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential transformedCredential)
        throws GeneralSecurityException, PreventedException {

    if (StringUtils.isBlank(this.sql) || StringUtils.isBlank(this.algorithmName) || getJdbcTemplate() == null) {
        throw new GeneralSecurityException("Authentication handler is not configured correctly");
    }

    final String username = transformedCredential.getUsername();
    try {
        // Get password and salt
        final Map<String, Object> rows = getJdbcTemplate().queryForMap(this.sql, username);
        final String encodedPassword = rows.get("password").toString();
        final String dbSalt = rows.get("salt").toString();
        SaltPasswordEncoder passwordEncoder = new SaltPasswordEncoder();
        passwordEncoder.setSalt(dbSalt);
        if (!passwordEncoder.matches(transformedCredential.getPassword(), encodedPassword)) {
            throw new FailedLoginException("Password does not match value on record.");
        }
		// 登录成功,踢掉前一个相同登录的人
        getForceLogoutManager().doLogout(username);
        return createHandlerResult(transformedCredential, this.principalFactory.createPrincipal(username), null);

    } catch (final IncorrectResultSizeDataAccessException e) {
        if (e.getActualSize() == 0) {
            throw new AccountNotFoundException(username + " not found with SQL query");
        } else {
            throw new FailedLoginException("Multiple records found for " + username);
        }
    } catch (final DataAccessException e) {
        throw new PreventedException("SQL exception while executing query for " + username, e);
    }

}

再次启动测试。

很顺利调用没有任何问题,到这里基于CAS v5.0.4的踢人功能的处理过程就整理完毕了。

最后还有一句话,我的愿望是:世界和平,快乐编程每一天,keep real!

Trouble Shooting —— HTTPS(SSL)站点使用WebSocket(ws)出现SecurityError问题

Published on:

最近发生了一个问题我觉得挺有意思的,所以针对这个问题总结一下。

最近公司服务上了https(SSL),在https(SSL)的环境下呢本因为可以愉快的玩耍,但是后来发现程序有使用websocket(ws://domain.com),这里就有朋友想了使用ws跟ssl有什么关系?我可以很明确的告诉你当然有关系。

当你的站点使用的是http的时候,使用ws可以很愉快的玩耍。当换成了https(SSL)那么问题来了。

在chrome下是测试没有问题可以正常使用,但是在ie下就出现了问题,报SecurityError的错误,那这个错误是什么原因呢?

WebSocket connection to 'ws://domain.com/websocket' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED

应该是每个浏览器对websocket的支持不一样或者说每个浏览器的安全沙箱不太一样,禁止了一些用法,各大浏览器对websocket的支持情况请看:https://caniuse.com/#search=websocket

无意中看到了mozilla的websocket支持详细说明如下:

Security considerations
WebSockets should not be used in a mixed content environment; that is, you shouldn’t open a non-secure WebSocket connection from a page loaded using HTTPS or vice-versa. In fact, some browsers explicitly forbid this, including Firefox 8 and later.

具体地址:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications

意思呢就是,ws与http对应,wss与https对应,如果站点使用的是https那就必须使用wss来做websocket请求不能使用ws来请求,不允许混合的方式使用。

看到这个就更加明确了问题所在:安全机制问题,最好不要混合使用避免奇怪的问题。

于是就开启了wss服务的使用路程。

如果你的wss服务是使用ip方式访问的,那么需要制作一个对应这个ip的证书,可以使用openssl生成自签名证书,但是不推荐使用ip的方式访问WebSocket。

如果你的wss服务是使用域名方式访问的,那么需要制作一个对应这个域名证书(最好是通配符域名证书),这样在构建wss服务的时候将证书配置进去。

构建wss服务有很多种方式,我这里提供一种比较简单的方式。

使用nginx提供ssl代理

保留以前的ws服务提供方式不做任何变更,增加一个nginx开启ssl代理,配置跟常规的ssl配置有一些细微的变化,那就是header会有一些变化,websocket需要指定header:Upgradehttp version:1.1 ,因此我这里给出配置详情:

server {
    listen       443 ssl;
	server_name  your.domain.com;#你的域名,如果没有域名就去掉
	ssl on;
	#ssl_certificate     127.0.0.1.crt;
    #ssl_certificate_key 127.0.0.1.key;
	ssl_certificate     your.domain.com.pem;#这里可以使用pem文件和crt文件
    ssl_certificate_key your.domain.com.key;
	ssl_session_timeout 5m;
	ssl_session_cache shared:SSL:50m;
	ssl_protocols SSLv3 SSLv2 TLSv1 TLSv1.1 TLSv1.2;
	ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;

	location / {
		proxy_pass http://127.0.0.1:19808;# 这里换成你想转发的ws服务地址即可
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "Upgrade";
		proxy_set_header X-Real-IP $remote_addr;
        }
}

将证书文件放到conf同级目录即可,如果证书放在其他目录,需要修改ssl_certificate、ssl_certificate_key指定的位置。

这样就可以不用修改以前的ws服务来提供wss服务。

修改ws的请求方式为wss

wss://your.domain.com

ws服务这里也简单的说一下,有很多服务都可以构建ws服务,nginx、Workerman都可以,或者自己写程序开启ws服务。方式很多看个人喜好和公司的项目背景。

附录

  1. nginx官方文档
  2. Openssl生成自签名证书,简单步骤
  3. mozilla的websocket支持说明
  4. 各大浏览器对websocket的支持情况

常见错误

如果在ie下报如下错误:

IE Network Error 12038, 证书中的主名称无效或不相符

那是因为证书与请求地址不匹配导致的错误,在chrome测试它的https验证不会这么严格,在ie下https验证很严格(坑爹的ie)。

生产环境如何快速跟踪、分析、定位问题-Java

Published on:

我相信做技术的都会遇到过这样的问题,生产环境服务遇到宕机的情况下如何去分析问题?比如说JVM内存爆掉、CPU持续高位运行、线程被夯住或线程deadlocks,面对这样的问题,如何在生产环境第一时间跟踪分析与定位问题很关键。下来让我们看看通过如下步骤在第一时间分析问题。

CPU占用较高场景

收集当前CPU占用较高的线程信息,执行如下命令:

top -H -p PID -b -d 1 -n 1 > top.log
或
top -H -p PID

结果如下:

上图显示的都是某一个进程内的线程信息,找到cpu消耗最高的线程id,再配合jstack来分析耗cpu的代码位置,那如何分析呢?

先执行jstack获取线程信息

jstack -l PID > jstackl.log

将PID(29978)转成16进制:0x751a,16进制转换工具很多可以在线随便搜索一个或者基本功好的自己计算。

打开jstackl.log,查找nid=0x751a的信息,这样就定位到了具体的代码位置,这里由于是安全原因我就不贴图了。

通过上面的步骤就可以轻松的定位那个线程导致cpu过高,当然也可以通过其他方式来定位,下面介绍一个快捷的方式

#线程cpu占用
#!/bin/bash

[ $# -ne 1 ] && exit 1

jstack $1 >/tmp/jstack.log

for cpu_tid in `ps -mp $1 -o THREAD,tid,time|sort -k2nr| sed -n '2,15p' |awk '{print$2"_"$(NF-1)}'`;do

cpu=`echo $cpu_tid | cut -d_ -f1`

tid=`echo $cpu_tid | cut -d_ -f2`

xtid=`printf "%x\n" $tid`

echo -e "\033[31m========================$xtid $cpu%\033[0m"

cat /tmp/jstack.log | sed -n -e "/0x$xtid/,/^$/ p"

#cat /tmp/jstack.log | grep "$xtid" -A15

done

rm /tmp/jstack.log

上述命令会以百分比的方式来显示每个线程的cpu消耗百分比,这里我就不贴图了,谁用谁知道。

内存消耗过高场景

收集当前活跃对象数据量信息,执行以下命令获取

jmap -histo:live pid > jmaplive.log

ps. jmap -histo:live 数据可以多进行几次,比如说间隔几分钟输出一次,然后对比两个文件的差异可以看出gc回收的对象,如果多次结果没有差异并且gc频繁执行,证明剩余对象在引用无法gc回收,这时就需要对服务进行限流给服务喘气的机会。

或者收集dump信息,通常这种获取方式需要较长时间执行,并产生大容量的dump文件,我们会考虑逐步废掉通过这个文件来分析。执行以下命令获取

jmap -dump:file=./dump.mdump pid

dump文件通过MAT工具来进行内存泄漏分析。

线程、内存分析工具

上面说过通过jstack生成的线程文件是可以通过工具来直接打开可视化分析的,这里我推荐使用:tda(Thread Dump Analyzer)这个工具可以自行搜索下载。

通过jmap -dump生成的dump文件也是可以通过工具来进行可视化分析的,这里我推荐使用MAT(Memory Analysis Tools)它可以通过eclipse plugin的方式使用或者独立的下载安装包使用。

CAS使用经验总结,纯干货

最近在处理公司项目对接到CAS server,在使用CAS发生了很多问题,下面整理一下遇到的问题与解决方式,希望可以帮助到需要的工程师们

CAS它是什么?它能做什么?这些我就不概述了,自行去搜索了解,https://baike.baidu.com/item/CAS/1329561

我们在使用CAS的时候基本都会遇到如下的几种问题:

  1. 证书问题
  2. Client接入配置
  3. SLO(Single Logout)
  4. CAS callback回调问题
  5. Cookie问题
  6. 用户数据源以及认证问题
  7. CAS Server Ticket持久化问题
  8. Client Server集群模式下session问题

还有一些是公司内部项目框架集成问题这里就不多说了。

以下总结都是基于CAS v5.0.4版本测试

我用的CAS Server是通过overlays改造后的项目,为什么需要修改原有的CAS Server呢?

我相信每个公司都有一些特殊的需求比如说:

  1. 对登录页面的修改
  2. 自有的密码加密验证方式
  3. 新老项目架构参差不齐
  4. 使用公司自有用户数据源

等等很多问题都需要对CAS Server进行改造

这里我将改造的CAS Server放到github上:

项目地址:cas-site

   

下面具体说一下上述的问题将如何来分析并解决

证书问题

如果你的服务不打算使用SSL那请跳过这段说明。

一般公司项目会有很多域名大概都是子域名的方式,例如:account.xxxx.com,login.xxxx.com,那么最好使用通配符证书,为什么呢?这样你的cas server上配置一个通配符证书即可,如果没有使用通配符证书那cas server上要配置所有授信域名的证书,这样就很麻烦,除非一些历史问题没办法才会导入多个证书,一般使用通配符证书。

我使用的是自签名的通配符证书,具体自签名证书如何生成可以查看我之前写的文章:

《Openssl生成自签名证书,简单步骤》中讲述了如何生成自签名证书。

《使用自签名证书,简单步骤》中讲述了如何使用自签名证书。

《Java访问SSL地址,使用证书方式和免验证证书方式》中讲述了Java访问ssl使用证书方式和免验证证书方式。

ps.这里需要注意的是在制作单域名证书和通配符域名证书的区别是在:Common Name输入的时候,例如:

单域名证书:Common Name:account.xxxx.ccom 通配符域名证书:Common Name:*.xxxx.com

将制作好的证书文件通过keytool导入到jdk下即可,或使用InstallCert来生成文件copy到jdk下,具体可以参考文章:《使用自签名证书,简单步骤》

证书放在:%JAVA_HOME%\jre\lib\security

我们cas server使用的jdk1.8,client服务大多是jdk1.7,因此在证书处理上要注意这个细节,上面文章中有明确说明

如果需要使用Docker构建,可以参考我写好的Dockerfile,在cas-site项目下Dockerfile文件

Client接入配置

接入cas的client端配置非常简单,可以使用spring framework对接cas方式,也可以使用spring security对接cas方式,或者其他支持cas的第三方框架,自己对接配置非常简单只需要配置SingleSignOutFilterSingleSignOutHttpSessionListener

  • org.jasig.cas.client.session.SingleSignOutFilter:解决Logout清空TGC和session信息
  • org.jasig.cas.client.session.SingleSignOutHttpSessionListener:session监听

这里在对接方面就不做过多的介绍了。

SLO(Single Logout)

SLO是个什么?

通俗点讲就是:浏览器多个tab页开启不同的APP(使用同一个用户登录),在某一个APP里进行登出操作,其余APP应该一起登出

CAS Server默认是开启SLO功能,如果想要关闭这个功能可以通过设置application.properties文件中的参数来关闭,具体如下:

# 是否禁用SLO功能,true为禁用SLO功能
cas.slo.disabled=true
# 使用采用异步方式进行callback
cas.slo.asynchronous=true

这里需要注意Logout时服务重定向需要开启:

# Logout时服务重定向
cas.logout.followServiceRedirects=true

CAS Server在进行异步回调时会忽略所有的错误来保证所有APP都能接收到Server发出的logout请求,因此在遇到错误时不开启trace级别日志是看不到错误信息的。

如果你的client端能看到接下来的章节(CAS callback回调问题) 说到的日志信息那就证明回调是没有问题的。

CAS callback回调问题

CAS认证过程需要server端和client端来回调用,如果发现callback回调有问题多半是第一步证书问题导致,可以开启日志trace级别查看cas的日志来排除问题。

cas回调有三种情况:

一个是授权的时候进行回调信息如下

2018-01-19 11:44:28.419 [http-apr-8080-exec-9] TRACE org.jasig.cas.client.session.SingleSignOutHandler - Received a token request
2018-01-19 11:44:28.419 [http-apr-8080-exec-9] DEBUG org.jasig.cas.client.session.SingleSignOutHandler - Recording session for token ST-250-AouhaxqAjvmh5sfaP3Yz-8ec54e266608
2018-01-19 11:44:28.419 [http-apr-8080-exec-9] DEBUG c.j.f.c.s.storage.RedisBackedSessionMappingStorage - Attempting to remove Session=[8F24552DD446F669B7A522B1A8A0C86D]
2018-01-19 11:44:28.419 [http-apr-8080-exec-9] DEBUG c.j.f.c.s.storage.RedisBackedSessionMappingStorage - No mapping for session found.  Ignoring.
2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Placing URL parameters in map.
2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Calling template URL attribute map.

2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Loading custom parameters from configuration.
2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Constructing validation url: https://login.dev.xxx.com.cn/serviceValidate?ticket=ST-250-AouhaxqAjvmh5sfaP3Yz-8ec54e266608&service=https%3A%2F%2Faccount.dev.xxx.com.cn%2Fcas_security_check_
2018-01-19 11:44:28.420 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Retrieving response from server.
2018-01-19 11:44:28.460 [http-apr-8080-exec-9] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Server response: <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationSuccess>
        <cas:user>admin</cas:user>
        </cas:authenticationSuccess>
</cas:serviceResponse>

一个是SLO时清理session的回调信息如下

2018-01-19 11:44:45.484 [http-apr-8080-exec-5] TRACE org.jasig.cas.client.session.SingleSignOutHandler - Received a back channel logout request
2018-01-19 11:44:45.484 [http-apr-8080-exec-5] DEBUG org.jasig.cas.client.util.CommonUtils - safeGetParameter called on a POST HttpServletRequest for Restricted Parameters.  Cannot complete check safely.  Reverting to standard behavior for this Parameter
2018-01-19 11:44:45.485 [http-apr-8080-exec-5] TRACE org.jasig.cas.client.session.SingleSignOutHandler - Logout request:
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-79-M3OyvVsRH7Ft1gRVaBfeuBCAj4K1JEDnndt" Version="2.0" IssueInstant="2018-01-19T11:44:45Z"><saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID><samlp:SessionIndex>ST-250-AouhaxqAjvmh5sfaP3Yz-8ec54e266608</samlp:SessionIndex></samlp:LogoutRequest>
2018-01-19 11:44:45.485 [http-apr-8080-exec-5] DEBUG c.j.f.c.s.storage.RedisBackedSessionMappingStorage - Attempting to remove Session=[8F24552DD446F669B7A522B1A8A0C86D]
2018-01-19 11:44:45.485 [http-apr-8080-exec-5] DEBUG c.j.f.c.s.storage.RedisBackedSessionMappingStorage - Found mapping for session.  Session Removed.
2018-01-19 11:44:45.486 [http-apr-8080-exec-5] DEBUG org.jasig.cas.client.session.SingleSignOutHandler - Invalidating session [8F24552DD446F669B7A522B1A8A0C86D] for token [ST-250-AouhaxqAjvmh5sfaP3Yz-8ec54e266608]

还有一种也是SLO时清理session的回调和上面的有什么区别呢?

上面的SLO是back channel logout方式,还有一种方式:front channel logout,后者是cas新版本提供的新方式,我这里没有使用,具体可以参考官方说明:https://apereo.github.io/cas/5.0.x/installation/Logout-Single-Signout.html#turning-off-single-logout

开启trace日志查看回调是否发生错误来解决回调不生效问题

Cookie问题

当使用单个域名时会出现Cookie清理问题从而导致SLO失效,因为CAS Server生成TGC时如果不设置cookie domain它会写在对接的service所在的域名下,最好的方式是让Cookie写在根域名的根Path(/)下,在CAS server端配置TGC的domain以及其他cookie参数,具体参考:

cas.tgc.path=/
cas.tgc.maxAge=-1
cas.tgc.domain=your.domain.com
#cas.tgc.signingKey=
cas.tgc.name=TGC
#cas.tgc.encryptionKey=
cas.tgc.secure=true
cas.tgc.httpOnly=true
cas.tgc.rememberMeMaxAge=1209600
cas.tgc.cipherEnabled=true

具体说明查看官方文档:https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html#ticket-granting-cookie

举个例子理解一下

我有三个APP域名分别为:

https://account.domain.com
https://login.domain.com
https://app.domain.com

我生成的通配符证书域名为:*.domain.com

我三个APP在部署时jdk下放通配符域名证书

这样修改tgc配置为:

# cookie写的路径 / 为根域名下
cas.tgc.path=/
# cookie有效期,-1 为关闭浏览器自动清空
cas.tgc.maxAge=-1
# cookie写在那个域名下
cas.tgc.domain=domain.com
# cookie的名称
cas.tgc.name=TGC
# cookie开启器安全模式ssl
cas.tgc.secure=true
# cookie禁止js调用
cas.tgc.httpOnly=true
# 这两个采用默认配置即可
cas.tgc.rememberMeMaxAge=1209600
cas.tgc.cipherEnabled=true

用户数据源以及认证问题

CAS在这方面留了很多扩展的地方,而且很方便的配置就可以支持自定义

数据源支持的方式也有很多种(jdbc、mongodb、RestStorage、GIT、等)这里就不一一介绍了 认证方式支持的方式也很多种(Basic、OAuth2.0|1.0、Google Authenticator、LDAP、REST、OpenID、SPNEGO、等)这里就不一一介绍了

具体可以查看官方说明对应的配置:https://apereo.github.io/cas/5.0.x/installation/Configuration-Properties.html

我使用的是jdbc方式

具体可以去github上查看cas-site源码:cas-site

CAS Server Ticket持久化问题

Ticket持久化方式也有很多中(JPA、Couchbase、Hazelcast、Infinispan、InMemory、Ehcache、Ignite、Memcached),默认方式(inMemory基于内存的),下面我给出JAP方式的配置参数:

cas.ticket.registry.jpa.jpaLockingTimeout=3600
cas.ticket.registry.jpa.healthQuery=SELECT 1
cas.ticket.registry.jpa.isolateInternalQueries=false
cas.ticket.registry.jpa.url=jdbc:mysql://127.0.0.1:3306/cas?useUnicode=true&characterEncoding=UTF-8&noAccessToProcedureBodies=true
cas.ticket.registry.jpa.failFast=true
cas.ticket.registry.jpa.dialect=org.hibernate.dialect.MySQL5Dialect
cas.ticket.registry.jpa.leakThreshold=10
cas.ticket.registry.jpa.jpaLockingTgtEnabled=false
cas.ticket.registry.jpa.batchSize=1
#cas.ticket.registry.jpa.defaultCatalog=
cas.ticket.registry.jpa.defaultSchema=cas
cas.ticket.registry.jpa.user=root
cas.ticket.registry.jpa.ddlAuto=validate
cas.ticket.registry.jpa.password=root@123456
cas.ticket.registry.jpa.autocommit=true
cas.ticket.registry.jpa.driverClass=com.mysql.jdbc.Driver
cas.ticket.registry.jpa.idleTimeout=5000

# 下面的参数根据实际情况选择使用
# 连接池
# cas.ticket.registry.jpa.pool.suspension=false
# cas.ticket.registry.jpa.pool.minSize=6
# cas.ticket.registry.jpa.pool.maxSize=18
# cas.ticket.registry.jpa.pool.maxWait=2000
# 签名与数据加解密密钥和算法
# cas.ticket.registry.jpa.crypto.signing.key=
# cas.ticket.registry.jpa.crypto.signing.keySize=512
# cas.ticket.registry.jpa.crypto.encryption.key=
# cas.ticket.registry.jpa.crypto.encryption.keySize=16
# cas.ticket.registry.jpa.crypto.alg=AES

这里需要注意的是,以上给出的配置参数是建议值,ddlauto默认值是create-drop,可选值有(create、create-drop、validate、update),具体含义可以查看官方文档:https://apereo.github.io/cas/5.0.x/installation/JPA-Ticket-Registry.html,建议使用validate的方式,使用validate需要自己创建表,一共四张表下面贴出建表语句:

CREATE TABLE `locks` (
`application_id` varchar(255) NOT NULL,
`expiration_date` datetime DEFAULT NULL,
`unique_id` varchar(255) DEFAULT NULL,
`lockVer` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`application_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8


CREATE TABLE `oauth_tokens` (
`TYPE` varchar(31) NOT NULL,
`ID` varchar(255) NOT NULL,
`NUMBER_OF_TIMES_USED` int(11) DEFAULT NULL,
`CREATION_TIME` datetime DEFAULT NULL,
`EXPIRATION_POLICY` longblob NOT NULL,
`LAST_TIME_USED` datetime DEFAULT NULL,
`PREVIOUS_LAST_TIME_USED` datetime DEFAULT NULL,
`AUTHENTICATION` longblob NOT NULL,
`SERVICE` longblob NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

CREATE TABLE `serviceticket` (
`TYPE` varchar(31) NOT NULL,
`ID` varchar(255) NOT NULL,
`NUMBER_OF_TIMES_USED` int(11) DEFAULT NULL,
`CREATION_TIME` datetime DEFAULT NULL,
`EXPIRATION_POLICY` longblob NOT NULL,
`LAST_TIME_USED` datetime DEFAULT NULL,
`PREVIOUS_LAST_TIME_USED` datetime DEFAULT NULL,
`FROM_NEW_LOGIN` bit(1) NOT NULL,
`TICKET_ALREADY_GRANTED` bit(1) NOT NULL,
`SERVICE` longblob NOT NULL,
`ticketGrantingTicket_ID` varchar(255) DEFAULT NULL,
PRIMARY KEY (`ID`),
KEY `FK60oigifivx01ts3n8vboyqs38` (`ticketGrantingTicket_ID`),
CONSTRAINT `FK60oigifivx01ts3n8vboyqs38` FOREIGN KEY (`ticketGrantingTicket_ID`) REFERENCES `ticketgrantingticket` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

CREATE TABLE `ticketgrantingticket` (
`TYPE` varchar(31) NOT NULL,
`ID` varchar(255) NOT NULL,
`NUMBER_OF_TIMES_USED` int(11) DEFAULT NULL,
`CREATION_TIME` datetime DEFAULT NULL,
`EXPIRATION_POLICY` longblob NOT NULL,
`LAST_TIME_USED` datetime DEFAULT NULL,
`PREVIOUS_LAST_TIME_USED` datetime DEFAULT NULL,
`AUTHENTICATION` longblob NOT NULL,
`EXPIRED` bit(1) NOT NULL,
`PROXIED_BY` longblob,
`SERVICES_GRANTED_ACCESS_TO` longblob NOT NULL,
`ticketGrantingTicket_ID` varchar(255) DEFAULT NULL,
PRIMARY KEY (`ID`),
KEY `FKiqyu3qw2fxf5qaqin02mox8r4` (`ticketGrantingTicket_ID`),
CONSTRAINT `FKiqyu3qw2fxf5qaqin02mox8r4` FOREIGN KEY (`ticketGrantingTicket_ID`) REFERENCES `ticketgrantingticket` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

其他参数含义可以查看官方配置说明:https://apereo.github.io/cas/5.0.x/installation/JPA-Ticket-Registry.html

Client Server集群模式下session问题

当我们CAS Server准备好后,就要处理Client接入的问题,如果我们的Client服务是单机模式那没有任何问题,一旦放到集群环境下就会发生如下有意思的事情。

我前面说了CAS在授权回调时会做几件事,第一TG保存到Cookie,第二个保存ticketid对应的session关系以及session对象。

那么如果我们的Client服务是集群的会发生什么?

举个例子:

我的APP服务部署了2台服务(S1、S2)采用loadbalance映射一个域名出去访问,当CAS授权回调时被loadbalance路由到S1上,SingleSignOutFilter以及SingleSignOutHandler进行了TGC和SessionMappingStorage,默认的持久化方式是hash的方式,也就是说本地map方式,这样在下次访问到APP时被loadbalance路由到S2上就会发生什么有意思的事情呢?我相信做过分布式服务的应该都能猜出来什么问题。

APP:我没找到cas认证信息,跳转到cas login页面

CAS:我找到了你APP已经做过认证了,跳转到APP并且给你上次认证的ticlet

APP:我真没找到你的认证信息,跳转到cas login页面

CAS:你真的已经做过认证了,跳转到APP并且给你上次认证的ticlet

这样就会发生无线跳转死循环问题。

那如何解决上面的问题呢?

在分布式的环境下几乎服务都是集群的,甚至有很多公司会做异地多活等等。那么在集群环境下如何解决cas授权持久化的问题呢?很简单重新实现一个cas-client的SessionMappingStorage,这里可以使用很多方式,比如说:放到db、nosql的存储上(mongodb、redis)、memcache、分布式文件存储都可以。

我这里采用的是redis,而且我们dev和qa环境采用单机模式,stage和prod环境使用集群模式,因此我还做了集群和本地都兼容的方式,话不多说直接贴出实现代码

import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.jasig.cas.client.session.SessionMappingStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xxxxxxxx.framework.redis.client.IRedisClient;

public class RedisBackedSessionMappingStorage implements SessionMappingStorage {
    
    private final Logger logger = LoggerFactory.getLogger(getClass());
    
    /**
     * Maps the ID from the CAS server to the Session.
     */
    private final Map<String, HttpSession> MANAGED_SESSIONS = new HashMap<String, HttpSession>();

    /**
     * Maps the Session ID to the key from the CAS Server.
     */
    private final Map<String, String> ID_TO_SESSION_KEY_MAPPING = new HashMap<String, String>();
    
    private final static String NAME_SPACE = "CAS";
    
    private IRedisClient redisClient;
    
    /**
     * 在dev和qa环境使用单机模式:hash
     * 在stage和prod环境使用集群模式:redis
     */
    private String storageMode = "hash";

    /**
     * 获取 redisClient
     * @return the redisClient
     */
    public IRedisClient getRedisClient() {
        return redisClient;
    }

    /**
     * 设置 redisClient
     * @param redisClient the redisClient to set
     */
    public void setRedisClient(IRedisClient redisClient) {
        this.redisClient = redisClient;
    }

    /**
     * 获取 storageMode
     * @return the storageMode
     */
    public String getStorageMode() {
        return storageMode;
    }

    /**
     * 设置 storageMode
     * @param storageMode the storageMode to set
     */
    public void setStorageMode(String storageMode) {
        this.storageMode = storageMode;
    }

    @Override
    public HttpSession removeSessionByMappingId(String mappingId) {
        HttpSession session = null;
        if (storageMode.equals("hash")) {
            session = MANAGED_SESSIONS.get(mappingId);
        } else {
            session = redisClient.get(mappingId, NAME_SPACE, HttpSession.class, null);
        }

        if (session != null) {
            removeBySessionById(session.getId());
        }

        return session;
    }

    @Override
    public void removeBySessionById(String sessionId) {
        logger.debug("Attempting to remove Session=[{}]", sessionId);
        String key = null;
        if (storageMode.equals("hash")) {
            key = ID_TO_SESSION_KEY_MAPPING.get(sessionId);
        } else {
            key = redisClient.get(sessionId, NAME_SPACE, null);
        }

        if (logger.isDebugEnabled()) {
            if (key != null) {
                logger.debug("Found mapping for session.  Session Removed.");
            } else {
                logger.debug("No mapping for session found.  Ignoring.");
            }
        }
        
        if (storageMode.equals("hash")) {
            MANAGED_SESSIONS.remove(key);
            ID_TO_SESSION_KEY_MAPPING.remove(sessionId);
        } else {
            redisClient.del(key, NAME_SPACE);
            redisClient.del(sessionId, NAME_SPACE);
        }
    }

    @Override
    public void addSessionById(String mappingId, HttpSession session) {
        if (storageMode.equals("hash")) {
            ID_TO_SESSION_KEY_MAPPING.put(session.getId(), mappingId);
            MANAGED_SESSIONS.put(mappingId, session);
        } else {
            redisClient.set(session.getId(), NAME_SPACE, mappingId, -1);
            redisClient.set(mappingId, NAME_SPACE, session, -1);
        }

    }

}

这里使用的redis-client是我自己封装,使用文档在:《RedisClient使用说明》,支持redis集群模式:《RedisClient升级支持Sentinel使用说明》,代码已经放到了github上:

项目地址:redis-client

   

把上面的RedisBackedSessionMappingStorage类注入到org.jasig.cas.client.session.SingleSignOutFilter中即可

    <bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter">
    	<property name="sessionMappingStorage" ref="redisBackedSessionMappingStorage"></property>
    </bean>
    <bean id="redisBackedSessionMappingStorage" class="xxxxxxx.cas.session.storage.RedisBackedSessionMappingStorage">
    	<property name="redisClient" ref="redisClient"></property>
    	<property name="storageMode" value="${cas.session.storage.mode}"></property>
    </bean>

ps.参数cas.session.storage.mode,值:hash(本地map)、redis(集中存储)

WEB服务端session集中存储处理

WEB服务端session集中存储处理方案也有很多种,使用tomcat可以使用TomcatRedisSessionManager来解决session集中存储问题,github地址:https://github.com/ran-jit/tomcat-cluster-redis-session-manager

如果要自己实现也很简单,我这里大致说一下思路,需要包装一个可序列话的session,说白了就是包装一下session实现序列化接口:java.io.Serializable接口生成一个version id,包装一个获取器,在生成session的时候序列化写入集中存储返回id,在用的使用通过id获取,id可以使用jsessionid或者自己生成一个uuid都行。这个id可以放入浏览器cookie,也可以放入url每次带入,在登录成功后将session序列化存储到redis或其他cache、nosql、db等,在登出时清空即可,就看自己喜好来实现了。

到这里基本上对cas的使用经验就总结完了,我相信大家在使用cas时都会遇到上面的问题,希望这篇总结可以帮助到需要的人,感谢看到最后。

最后我的愿望是:世界和平,快乐编程每一天,keep real

Java访问SSL地址,使用证书方式和免验证证书方式

Published on:
Tags: ssl openssl

前文回顾

《Openssl生成自签名证书,简单步骤》中讲述了如何生成自签名证书。

《使用自签名证书,简单步骤》中讲述了如何使用自签名证书。

下面讲述在Java中如何访问SSL地址,使用证书访问和免验证证书访问。

Java安装证书访问SSL地址

使用InstallCert安装证书

《使用自签名证书,简单步骤》这篇文章中介绍的InstallCert生成jssecacerts文件。 将ssecacerts文件放入%JAVA_HOME%\jre\lib\security 下即可。

使用keytool工具导入证书

keytool -import -alias xstore -keystore "cacerts_path" -file a.cer
  • cacerts_path: 你的cacerts文件路径,一般在%JAVA_HOME%jre\lib\security\cacerts
  • a.cer: 你需要导入的cer文件路径,可以是InstallCert生成的文件
  • 密码使用jdk默认密码:changeit,或者在上面命令后增加-storepass changeit设置密码参数

通过上面两种方式可以将证书安装到jdk下,接下来就是java中如何访问ssl地址,不多说直接上代码。

自定义javax.net.ssl.X509TrustManager实现类

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;

public class MyX509TrustManager implements X509TrustManager {

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }

}

包装HttpsDemo类

HttpsDemo类中包装两个方法,sendHttps发起ssl地址请求,sendHttp发起普通地址请求

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HttpsDemo {

    private static final Logger logger = LoggerFactory.getLogger(HttpsDemo.class.getName());

    public static void sendHttps(String path, String outputStr) {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        HttpsURLConnection httpUrlConn = null;
        BufferedReader bufferedReader = null;
        InputStreamReader inputStreamReader = null;
        StringBuffer buffer = new StringBuffer();
        try {
            // 创建SSLContext对象,并使用我们指定的信任管理器初始化
            TrustManager[] tm = { new MyX509TrustManager() };
            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            // 从上述SSLContext对象中得到SSLSocketFactory对象
            SSLSocketFactory ssf = sslContext.getSocketFactory();

            URL url = new URL(path);
            httpUrlConn = (HttpsURLConnection) url.openConnection();
            httpUrlConn.setSSLSocketFactory(ssf);
            httpUrlConn.setDoOutput(true);
            httpUrlConn.setDoInput(true);
            httpUrlConn.setUseCaches(false);
            httpUrlConn.setRequestMethod("GET");
            httpUrlConn.connect();

            // 当有数据需要提交时
            if (null != outputStr) {
                outputStream = httpUrlConn.getOutputStream();
                // 注意编码格式,防止中文乱码
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }

            // 将返回的输入流转换成字符串
            inputStream = httpUrlConn.getInputStream();
            inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            bufferedReader = new BufferedReader(inputStreamReader);

            String str = null;
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            logger.info("地址:{}, success, result:{}", path, buffer.toString());
        } catch (Exception e) {
            logger.error("地址:{}, error, exception:{}", path, e);
        } finally {
            if (bufferedReader != null) {
                IOUtils.closeQuietly(bufferedReader);
            }
            if (inputStreamReader != null) {
                IOUtils.closeQuietly(inputStreamReader);
            }
            if (inputStream != null) {
                IOUtils.closeQuietly(inputStream);
            }
            if (httpUrlConn != null) {
                httpUrlConn.disconnect();
            }
        }
    }

    public static void sendHttp(String path) {
        InputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        HttpURLConnection urlConnection = null;
        try {
            URL url = new URL(path);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setRequestMethod("GET");
            urlConnection.setUseCaches(false);
            inputStream = urlConnection.getInputStream();
            outputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int n = 0;
            while (-1 != (n = inputStream.read(buffer))) {
                outputStream.write(buffer, 0, n);
            }
            logger.info("地址:{}, success, result:{}", path, outputStream.toString());
        } catch (Exception e) {
            logger.error("地址:{}, error, exception:{}", path, e);
        } finally {
            if (outputStream != null) {
                IOUtils.closeQuietly(inputStream);
            }
            if (outputStream != null) {
                IOUtils.closeQuietly(outputStream);
            }
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
    }

	public static void main(String[] args) {
        sendHttps("https://xxx.com", null);
    }
}

上面访问ssl地址如果报错java.security.cert.CertificateException: No name matching localhost found那就是证书没有安装好,检查前面证书安装过程。

Java访问ssl其实是可以绕过证书验证的,可以不需要证书直接发起ssl地址请求,下面介绍一下。

Java绕过证书验证访问SSL地址,达到免验证证书效果

这种方式是采用重写HostnameVerifier的verify方法配合X509TrustManager来处理授信所有host,下面直接上代码

import java.net.HttpURLConnection;
import java.net.URL;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HttpDemo {

    private static final Logger logger = LoggerFactory.getLogger(HttpDemo.class.getName());

    final static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {

        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    };

    public static void httpGet(String path) {
        StringBuffer tempStr = new StringBuffer();
        String responseContent = "";
        HttpURLConnection conn = null;
        try {
            // Create a trust manager that does not validate certificate chains
            trustAllHosts();
            URL url = new URL(path);
            HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
            if (url.getProtocol().toLowerCase().equals("https")) {
                https.setHostnameVerifier(DO_NOT_VERIFY);
                conn = https;
            } else {
                conn = (HttpURLConnection) url.openConnection();
            }
            conn.connect();
            logger.info("地址:{}, success, result:{}", path, conn.getResponseCode() + " " + conn.getResponseMessage());
            // HttpURLConnection conn = (HttpURLConnection)
            // url.openConnection();

            // conn.setConnectTimeout(5000);
            // conn.setReadTimeout(5000);
            // conn.setDoOutput(true);
            //
            // InputStream in = conn.getInputStream();
            // conn.setReadTimeout(10*1000);
            // BufferedReader rd = new BufferedReader(new InputStreamReader(in,
            // "UTF-8"));
            // String tempLine;
            // while ((tempLine = rd.readLine()) != null) {
            // tempStr.append(tempLine);
            // }
            // responseContent = tempStr.toString();
            // System.out.println(responseContent);
            // rd.close();
            // in.close();
        } catch (Exception e) {
            logger.error("地址:{}, is error", e);
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

    /**
     * Trust every server - dont check for any certificate
     */
    private static void trustAllHosts() {

        // Create a trust manager that does not validate certificate chains
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return new java.security.cert.X509Certificate[] {};
            }

            public void checkClientTrusted(X509Certificate[] chain, String authType) {

            }

            public void checkServerTrusted(X509Certificate[] chain, String authType) {

            }
        } };

        // Install the all-trusting trust manager
        // 忽略HTTPS请求的SSL证书,必须在openConnection之前调用
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        } catch (Exception e) {
            logger.error("trustAllHosts is error", e);
        }
    }

	public static void main(String[] args) {
        httpGet("https://xxx.com");
    }
}

以上代码需要注意一点:忽略HTTPS请求的SSL证书,必须在openConnection之前调用。

常见错误

错误一

如果发生如下错误,请添加vm参数:-Dhttps.protocols=TLSv1.1,TLSv1.2 -Djava.net.preferIPv4Stack=true,一般是jdk1.7会发生这个错误,具体原因在《使用自签名证书,简单步骤》这篇文章中已经解释。

javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:946) ~[na:1.7.0_45]
	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) ~[na:1.7.0_45]
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339) ~[na:1.7.0_45]
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323) ~[na:1.7.0_45]
	at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:563) ~[na:1.7.0_45]
	at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185) ~[na:1.7.0_45]
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153) ~[na:1.7.0_45]
	at HttpDemo.httpGet(HttpDemo.java:59) [classes/:na]
	at HttpDemo.main(HttpDemo.java:122) [classes/:na]
Caused by: java.io.EOFException: SSL peer shut down incorrectly
	at sun.security.ssl.InputRecord.read(InputRecord.java:482) ~[na:1.7.0_45]
	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:927) ~[na:1.7.0_45]
	... 8 common frames omitted

错误二

如果发生如下错误,是因为没有找到匹配的证书。 如果使用证书的方式访问,请检查证书安装是否错误。 如果是免验证证书访问,请检查代码没有跳过证书验证。

javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching xxxxxxx.com found
	at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) ~[na:1.7.0_45]
	at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1884) ~[na:1.7.0_45]
	at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:276) ~[na:1.7.0_45]
	at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:270) ~[na:1.7.0_45]
	at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1341) ~[na:1.7.0_45]
	at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153) ~[na:1.7.0_45]
	at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868) ~[na:1.7.0_45]
	at sun.security.ssl.Handshaker.process_record(Handshaker.java:804) ~[na:1.7.0_45]
	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016) ~[na:1.7.0_45]
	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) ~[na:1.7.0_45]
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339) ~[na:1.7.0_45]
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323) ~[na:1.7.0_45]
	at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:563) ~[na:1.7.0_45]
	at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185) ~[na:1.7.0_45]
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153) ~[na:1.7.0_45]
	at HttpsDemo.sendHttps(HttpsDemo.java:62) [classes/:na]
	at HttpsDemo.main(HttpsDemo.java:133) [classes/:na]
Caused by: java.security.cert.CertificateException: No name matching xxxxxxx.com found
	at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:208) ~[na:1.7.0_45]
	at sun.security.util.HostnameChecker.match(HostnameChecker.java:93) ~[na:1.7.0_45]
	at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:347) ~[na:1.7.0_45]
	at sun.security.ssl.AbstractTrustManagerWrapper.checkAdditionalTrust(SSLContextImpl.java:847) ~[na:1.7.0_45]
	at sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:814) ~[na:1.7.0_45]
	at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323) ~[na:1.7.0_45]
	... 12 common frames omitted

使用自签名证书,简单步骤

Published on:

在前文《Openssl生成自签名证书,简单步骤》中讲述了如何生成自签名证书,接下来整理证书使用遇到的问题。

证书使用的方式也有很多中,可以使用keytool生成或导入导出证书,这里对keytool不做过多描述,可以通过–help查看使用方法。

证书文件可以放到应用服务器、负载均衡、jvm中使用,如:IIS、tomcat、nginx或者loadbalance、jdk等等。

这里介绍一个简单的工具:InstallCert安装证书文件到jdk下,这个在本地调试连接ssl服务器代码的时候很有用。

如果我们的服务端使用的是jdk1.8(比如说:cas服务),访问的客户端(业务系统)也是jdk1.8,那么直接使用InstallCert安装即可.

如果我们的服务端使用的是jdk1.8,但是客户端使用jdk1.7会遇到什么问题?

我们都知道jdk1.7默认的TLS版本是1.0但是支持1.1和1.2,如何查看jdk支持的TLS版本呢?

可以使用jdk自带的jcp(java control panel)工具

jcp(java control panel)路径:%JAVA_HOME%\jre\bin

点击高级,勾选TLS1.1 TSL1.2开启支持。

如果使用客户端程序(jdk1.7开发的)访问服务端程序(jdk1.8开发的),在使用InstallCert安装证书时会出现如下错误:

javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:946) ~[na:1.7.0_45]
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312) ~[na:1.7.0_45]

上面错误的意思就是服务器把你拒绝了!把你拒绝了!把你拒绝了!拒绝你的理由就是TLS版本不对。

下面我主要讲在客户端程序(jdk1.7开发的)访问服务端程序(jdk1.8开发的)的场景下安装证书如何解决上面的错误。

通过InstallCert源码安装证书

/*
 * Copyright 2006 Sun Microsystems, Inc.  All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Sun Microsystems nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.io.*;
import java.net.URL;

import java.security.*;
import java.security.cert.*;

import javax.net.ssl.*;

public class InstallCert {

    public static void main(String[] args) throws Exception {
    String host;
    int port;
    char[] passphrase;
    if ((args.length == 1) || (args.length == 2)) {
        String[] c = args[0].split(":");
        host = c[0];
        port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);
        String p = (args.length == 1) ? "changeit" : args[1];
        passphrase = p.toCharArray();
    } else {
        System.out.println("Usage: java InstallCert <host>[:port] [passphrase]");
        return;
    }

    File file = new File("jssecacerts");
    if (file.isFile() == false) {
        char SEP = File.separatorChar;
        File dir = new File(System.getProperty("java.home") + SEP
            + "lib" + SEP + "security");
        file = new File(dir, "jssecacerts");
        if (file.isFile() == false) {
        file = new File(dir, "cacerts");
        }
    }
    System.out.println("Loading KeyStore " + file + "...");
    InputStream in = new FileInputStream(file);
    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
    ks.load(in, passphrase);
    in.close();

    SSLContext context = SSLContext.getInstance("TLSv1.2");
    TrustManagerFactory tmf =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(ks);
    X509TrustManager defaultTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
    SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
    context.init(null, new TrustManager[] {tm}, null);
    SSLSocketFactory factory = context.getSocketFactory();

    System.out.println("Opening connection to " + host + ":" + port + "...");
    SSLSocket socket = (SSLSocket)factory.createSocket(host, port);
    socket.setSoTimeout(10000);
    try {
        System.out.println("Starting SSL handshake...");
        socket.startHandshake();
        socket.close();
        System.out.println();
        System.out.println("No errors, certificate is already trusted");
    } catch (SSLException e) {
        System.out.println();
        e.printStackTrace(System.out);
    }

    X509Certificate[] chain = tm.chain;
    if (chain == null) {
        System.out.println("Could not obtain server certificate chain");
        return;
    }

    BufferedReader reader =
        new BufferedReader(new InputStreamReader(System.in));

    System.out.println();
    System.out.println("Server sent " + chain.length + " certificate(s):");
    System.out.println();
    MessageDigest sha1 = MessageDigest.getInstance("SHA1");
    MessageDigest md5 = MessageDigest.getInstance("MD5");
    for (int i = 0; i < chain.length; i++) {
        X509Certificate cert = chain[i];
        System.out.println
            (" " + (i + 1) + " Subject " + cert.getSubjectDN());
        System.out.println("   Issuer  " + cert.getIssuerDN());
        sha1.update(cert.getEncoded());
        System.out.println("   sha1    " + toHexString(sha1.digest()));
        md5.update(cert.getEncoded());
        System.out.println("   md5     " + toHexString(md5.digest()));
        System.out.println();
    }

    System.out.println("Enter certificate to add to trusted keystore or 'q' to quit: [1]");
    String line = reader.readLine().trim();
    int k;
    try {
        k = (line.length() == 0) ? 0 : Integer.parseInt(line) - 1;
    } catch (NumberFormatException e) {
        System.out.println("KeyStore not changed");
        return;
    }

    X509Certificate cert = chain[k];
    String alias = host + "-" + (k + 1);
    ks.setCertificateEntry(alias, cert);

    OutputStream out = new FileOutputStream("jssecacerts");
    ks.store(out, passphrase);
    out.close();

    System.out.println();
    System.out.println(cert);
    System.out.println();
    System.out.println
        ("Added certificate to keystore 'jssecacerts' using alias '"
        + alias + "'");
    }

    private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();

    private static String toHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder(bytes.length * 3);
    for (int b : bytes) {
        b &= 0xff;
        sb.append(HEXDIGITS[b >> 4]);
        sb.append(HEXDIGITS[b & 15]);
        sb.append(' ');
    }
    return sb.toString();
    }

    private static class SavingTrustManager implements X509TrustManager {

    private final X509TrustManager tm;
    private X509Certificate[] chain;

    SavingTrustManager(X509TrustManager tm) {
        this.tm = tm;
    }

    public X509Certificate[] getAcceptedIssuers() {
//        throw new UnsupportedOperationException();
        return new X509Certificate[0];
    }

    public void checkClientTrusted(X509Certificate[] chain, String authType)
        throws CertificateException {
        throw new UnsupportedOperationException();
    }

    public void checkServerTrusted(X509Certificate[] chain, String authType)
        throws CertificateException {
        this.chain = chain;
        tm.checkServerTrusted(chain, authType);
    }
    }

}

上面源码我修改了SSLContext context = SSLContext.getInstance("TLSv1.2");,原本是TLS,这样在jdk1.7下会报错,尽管加了vm参数:-Dhttps.protocols=TLSv1.1,TLSv1.2 -Djava.net.preferIPv4Stack=true,依然会报错。

修改为TLSv1.2后,直接运行代码,参数为:你需要签名的域名

运行日志会出现如下错误(不用紧张,这个错误没有关系):

Opening connection to login.xxxxx.com.cn:443...
Starting SSL handshake...

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
	at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1884)
	at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:276)
	at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:270)
	at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1341)
	at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153)
	at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868)
	at sun.security.ssl.Handshaker.process_record(Handshaker.java:804)
	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016)
	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323)
	at InstallCert.main(InstallCert.java:99)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:385)
	at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292)
	at sun.security.validator.Validator.validate(Validator.java:260)
	at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:326)
	at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:231)
	at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:107)
	at InstallCert$SavingTrustManager.checkServerTrusted(InstallCert.java:195)
	at sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:813)
	at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323)
	... 8 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:196)
	at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:268)
	at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:380)
	... 16 more

Server sent 1 certificate(s):

 1 Subject EMAILADDRESS=ningyu@xxxxxx.com, CN=login.xxxxxxx.com, OU=JY, O=JY, L=Shanghai, ST=Shanghai, C=CN
   Issuer  EMAILADDRESS=ningyu@xxxxxx.com, CN=login.xxxxxxx.com, OU=JY, O=JY, L=Shanghai, ST=Shanghai, C=CN
   sha1    18 fe a4 26 de 9f ef 9f d0 12 f9 1b da e8 f4 6e 46 a3 ca e2 
   md5     53 02 53 bc 1f 5d e3 0f c2 ce a5 fa 43 7b 53 83 

Enter certificate to add to trusted keystore or 'q' to quit: [1]

出现上面错误没关系,在命令行输入:1,生成文件,会在执行目录下生成:jssecacerts,并且会输出下面的日志:

Enter certificate to add to trusted keystore or 'q' to quit: [1]
1

[
[
  Version: V1
  Subject: EMAILADDRESS=ningyu@xxxxx.com, CN=login.xxxxxxx.com, OU=JY, O=JY, L=Shanghai, ST=Shanghai, C=CN
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11

  Key:  Sun RSA public key, 1024 bits
  modulus: 150111273197244637724411949927732292545940427223472330318676441758610292860528090849280500452765059055376192276098938042951946335160244351904122898746077164287399465663417510841977938344538423662939325238497292924898237072606839002269269847753256718676717424760603548961942760492908854629736493402902120207483
  public exponent: 65537
  Validity: [From: Fri Jan 12 15:15:03 CST 2018,
               To: Mon Jan 10 15:15:03 CST 2028]
  Issuer: EMAILADDRESS=ningyu@xxxxxx.com, CN=login.xxxxxx.com, OU=JY, O=JY, L=Shanghai, ST=Shanghai, C=CN
  SerialNumber: [    b9c6224c 0cf5ee1a]

]
  Algorithm: [SHA256withRSA]
  Signature:
0000: B7 F8 1B FB 3C 7E 46 31   9C 56 31 47 F5 79 2C AA  ....<.F1.V1G.y,.
0010: B0 E3 FB EA CF 6C 15 72   53 8B A9 36 1D 43 E0 AB  .....l.rS..6.C..
0020: 21 3C BD 65 51 11 B3 D6   5B 42 40 DB 07 9C 35 5C  !<.eQ...[B@...5\
0030: 84 9B B7 B8 02 5A E0 96   5D 5F 9E 5D B3 5F 85 A8  .....Z..]_.]._..
0040: 50 64 63 E7 12 B0 DF CA   48 DD 28 B7 B2 8D 42 33  Pdc.....H.(...B3
0050: A5 C1 E8 E1 41 08 F8 39   21 DD 6C BE 6E F1 CD EE  ....A..9!.l.n...
0060: F9 C0 DC 2F 1E 99 D2 DC   A3 2C C7 C2 64 ED 94 5E  .../.....,..d..^
0070: 32 6F CC B4 3D 93 B7 F8   09 8D F9 4E 39 CA 5E 53  2o..=......N9.^S

]

Added certificate to keystore 'jssecacerts' using alias 'login.xxxxxx.com-1'

这个时候再运行一遍InstallCert就不会报错,因为已经有jssecacerts文件,直接copy jssecacerts文件到%JAVA_HOME%\jre\lib\security下,就可以愉快的玩耍了。

这个在我们本地调试连接ssl服务器的代码时很有用,如果不把证书放入jdk下你会被无限的拒绝。