Java 高级特性——安全,jaas登陆框架(二)
Java 高级特性——安全,jaas登陆框架(二)
java 提供了 jaas 框架来对用户进行鉴权,本文主要从实战方面讲述如何使用 jaas 框架,最终完成登陆模块。
阅读本文的前置知识:Java 安全框架(策略文件的使用)
注:如果使用了 Java 模块,则需将登陆用到的自定义包导出
一、Jaas 框架介绍
注:LoginModule 和 CallbackHandler 可以不是一一对应的关系,LoginModule 可以对应多个 CallbackHandler
最简单的例子就是使用 jdk 提供的类来完成登陆请求,但是一般无法满足我们的需求。
要使用 jaas 框架首先我们需要打开安全管理器,然后调用相关的登陆代码来完成登陆,在这之前,先让我们来熟悉几个用到的类或接口。
1.LoginContext 类
LoginContext
公有方法代码如下:
public void login();
public void logout();
public Subject getSubject();
函数解释:
login()
:登陆logout()
:登出getSubject()
:获取当前登陆用户摘要信息
2.Principal类
Principal
接口,代码如下:
public interface Principal {
public boolean equals(Object another);
public String toString();
public int hashCode();
public String getName();
/*
* 蕴含关系,subject 是否蕴含当前的 Principal 主体
*/
public default boolean implies(Subject subject) {
if (subject == null)
return false;
return subject.getPrincipals().contains(this);
}
该接口表示了登陆主体,代表了登陆者的身份,通过 getName()
方法返回身份名。该接口在后续重点说明。
Principal
接口 jdk 提供了下面几种实现:
UserPrincipal
:使用用户名来表述当前登陆者的身份NtUserPrincipal
:也是使用用户名,不过用于 Windows 系统UnixPrincipal
:同上,不过用于 Unix 系统
可以通过在 policy 文件中配置 Principal
主体所拥有的权限,一个 Subject
可以有多个 Principal
。
LoginModule
的实现需要与 Principal
一一对应。
3.LoginModule 类
LoginModule
接口,代码如下:
public interface LoginModule {
void initialize(Subject subject, CallbackHandler callbackHandler,
Map<String,?> sharedState,
Map<String,?> options);
boolean login() throws LoginException;
boolean commit() throws LoginException;
boolean abort() throws LoginException;
boolean logout() throws LoginException;
}
函数解释:
initialize()
:当新建一个LoginContext
时,Jaas会自动创建对应的LoginModule
并自动调用initialize()
,可以在此函数中进行初始化操作。login()
:当调用LoginContext.login()
时,jass自动调用该函数,完成登陆,如果该函数返回为true
并且没有异常抛出,则登陆成功。commit()
:登陆成功后,调用此函数。abort()
:如果LoginModule
整体验证失败,则调用该方法。logout()
:当调用LoginContext.logout()
时,自动调用该方法。
4.主要流程
Jaas 框架,主要的流程如下:
- 1.新建一个
LoginContext
,jaas通过传入的name
找到对应配置的用户名,然后利用配置信息找到对应的LoginModule
并实例化。 - 2.调用
LoginContext.login()
方法,jaas 会回调LoginModule.login()
方法进行登陆,如果该方法返回为true
并且没有抛出异常,则登陆成功。 - 3.如果用户登陆成功,jaas会回调
LoginModule.commit()
函数进行提交,可以在这里对用户进行授权。 - 4.如果在授权阶段发生异常,jaas会调用
LoginModule.abort()
函数。 - 5.调用
LoginContext.logout()
函数退出登陆。
5.相关的配置文件
jaas 有两个相关的配置文件,分别是 policy 文件和 config 文件。
policy 文件
- 命名:xxx.policy
- 使用方式:通过添加程序启动参数(前提,开启了securityManager),
java -Djava.security.policy==xxx.policy
policy 文件可以对代码和主体(Principal)进行授权配置,主要结构如下:
grant {
## 示例:permission xxx.Permission "name", "action";
permission javax.security.auth.AuthPermission "doAsPrivileged";
permission javax.security.auth.AuthPermission "doAs";
permission javax.security.auth.AuthPermission "createLoginContext";
permission java.util.PropertyPermission "os.name", "read";
permission javax.security.auth.AuthPermission "modifyPrincipals";
};
grant principal com.fy.login.internal.SimplePrincipal "user" {
permission java.util.PropertyPermission "user.*", "read";
};
有几个需要注意的点:
- 1.policy文件有严格的格式限制,每个授权块后面必须有分号,每行后面也必须有分号
- 2.如果没有 action,可省略 action 和 逗号,如果有,那么 name 后面必须加逗号。
- 3.授权主体(Principal)的配置如上所述,语法结构为
grant principal xxx.Principal "name"{ 权限列表 };
,这里的principal
表示当前授权对象是个Principal
类,xxx.Principal
表示自定义的Principal
实现类,"user"
表示当前授权主体的名称,该主体包含了一组权限。 - 4.关于
Principal
需要特别说明的是:我们最好实现它的equals()
和hashCode()
方法,并且在implies()
方法中利用这两个方法来判断该subject
是否蕴含该主体。
config 文件
- 命名:xxx.config
- 使用方式:通过添加程序启动参数(前提,开启了securityManager),
java -Djava.security.auth.login.config=xxx.config
config 文件主要功能是指定通过new LoginContext(name)
所对应name
要使用的LoginModule
类。例如,对ADMIN
用户,我们可以配置AdminLoginModule
类,进行更加严密的鉴权;而对USER
用户,使用相对简单的鉴权。
config 文件示例:
user
{
com.fy.login.internal.SimpleLoginModule required;
};
admin
{
com.fy.login.internal.SimpleLoginModule required;
};
注:该有的分号不能省略。
二、实现自定义登陆模块
IUserSeviece
、LoginManager
、CallbackHandler
、EventListeners
对外提供功能。- 通过 SPI 机制提供
IUserService
来实现密码获取和加载IValidateChain
来进行更严格的鉴权。 EventListeners
提供了静态的添加IEventListener
的方法来添加登陆登出监听。BaseMetadata
和NetMetadata
用来保存用户信息。- 通过
LoginManager.login(callback)
传入的CallbackHandler
来获取对应类型的Metadata
信息。
注:该框架仅是用来熟悉jaas的。
参考地址:https://gitee.com/zolmk/x-login
三、数字签名
常用命令:
- 生成证书:
keytool -genkeypair -keystore <密钥库名 | acmesoft.certs> -alias <别名 | acmeroot>
- 导出证书(公钥):
keytool -exportcert -keystore <密钥库名 | acmesoft.certs> -alias <别名 | acmeroot> -file acmeroot.cer
- 导入证书(将公钥加入到密钥库):
keytool -importcert -keystore <密钥库名 | client.certs> -alias <别名 | acmeroot> -file <证书名 | acmeroot.cer>
- 查看证书:
keytool -printcert -file <证书名 | acmeroot.cer>
- 验证证书:
jarsigner -verify -verbose -keystore <密钥库名 | client.certs> test.jar
- 列出证书:
keytool -list -v -keystore <密钥库名 | acmeroot.crets>
- 签名:
jarsigner -keystore <密钥库名 | acmeroot.crets> test.jar <别名 | acmeroot>
1.消息摘要
消息摘要即消息的指纹,消息摘要的两个基本属性:1.如果消息的1位或者几位改变了,那么消息摘要也要改变。2.拥有给定消息的伪造者无法创建与原消息具有相同摘要的假消息。
常见计算消息摘要的算法:MD5、SHA1、SHA2、SHA3
注:计算消息摘要的算法是公开的,确定的。
2.消息签名
消息签名使用了数字签名技术,分别有两套密钥,公钥和私钥,私钥需要消息发送方严密保存,公钥人人可以拥有。公钥和私钥之间存在着数学关系,在编程中无需在意。当消息发送方发送消息时,先计算出消息摘要,然后利用私钥对消息摘要进行签名,然后将签名后的数据发送到接收方;接收方收到消息摘要后,使用公钥进行校验,如果验证无误,那么证明消息摘要没有被篡改,然后就可以利用此消息摘要来判断消息是否正确。
只对消息摘要进行签名是因为对消息进行签名代价相对应计算消息摘要来说,代价大太多,因此用签名的方式来保证消息摘要没被篡改,用消息摘要来保证消息没有被篡改。
签名的命令:jarsigner -keystore <密钥库名 | acmeroot.crets> test.jar <别名 | acmeroot>
注:进行消息签名的密钥库必须有私钥,一般为根密钥库。
3.校验签名
校验签名之前需要先得到消息发送方的公钥,然后利用公钥来校验签名。
首先要保证公钥没有被篡改(一般可省略这一步),可以通过命令输出证书的内容,然后向证书拥有者询问并核对证书指纹。
使用命令来进行校验:jarsigner -verify -verbose -keystore <密钥库名 | client.certs> test.jar
4.代码签名
代码签名主要是对jar包或应用程序进行签名,在这里主要讲如何对jar包进行签名,并且利用策略文件通过签名进行授权。
应用场景:应用开发者想通过对 jar 包授权来进行安全控制,即通过 jar 包签名来授予各个 jar 包不同的权限。
第一步:生成根证书,keytool -genkeypair -keystore acmesoft.certs -alias acmeroot
。
- 应用开发者可以利用根证书对 jar 包进行签名,但是根证书中包含私钥,因此不能分享给其他人。
- 会有提示输入密码,此密码就是新建的 acmesoft.certs 密钥库的密码
- 按提示输入相应的信息(可以乱填)
第二步:建立第二个密钥库,并将公共的 acmeroot 证书添加进去,先keytool -exportcert -keystore acmesoft.certs -alias acmeroot -file acmeroot.cer
生成 cer 证书,然后keytool -importcert -keystore client.certs -alias acmeroot -file acmeroot.cer
将 acmeroot.cer 证书导入 client.certs 密钥库中。
- 第一个命令:按提示输入 acmesoft.certs 密钥库的密码。
- 第二个命令:按提示输入新建的 client.certs 密钥库的密码
第三步:用根证书给 jar 包签名,jarsigner -keystore acmesoft.certs test.jar acmeroot
,签名后的 jar 包会在 jar包内的 META_INF 文件夹下生成签名文件。可以使用 jarsigner -verify -verbose -keystore client.certs test.jar
来验证签名。
第四步:通过代码签名授权,需要对策略文件进行配置,格式如下:
keystore "client.certs", "JKS";
keystorePasswordURL "secret";
grant signedBy "acmeroot"{
permission java.util.PropertyPermission "user.dir", "read";
};
- 首先配置 keystore 密钥库,这里需要注意路径,如果不写路径,则默认从当前路径开始。
- 第二行需配置 keystore 的密码文件所在位置,需注意,这里配置的是密码文件所在位置,不是密码值。
- 第三行开始进行授权配置,格式是:
grant signedBy "aliasName"{ 权限列表 }
,aliasName 即证书别名。 - 可以将证书授权和其他授权方式结合起来,如下。
grant codeBase "xxx.jar", principal xxx.Principal "name", signedBy "aliasName"{
permission ...;
};
grant signedBy "aliasName"{
permission java.util.PropertyPermission "user.dir", "read",
}
注:需开启安全管理器和启动时需配置policy文件,可以通过添加-Djava.security.debug=all
参数来查看安全日志。
注:实测配置无误时,开启debug日志,反而程序无法运行,关闭后正常,原因未找到。
四、加密
加密四部曲:
- 1.获取指定加解密机,
Cipher cipher = Cipher.getInstance("DES")
。 - 2.获取
Key
KeyGenerator keyGenerator = KeyGenerator.getInstance("DES");
keyGenerator.init(new SecureRandom());
Key key = keyGenerator.generateKey();
- 3.初始化加密解机并指定类型,
cipher.init(Cipher.ENCRYPT_MODE, key)
- 4.加解密,
byte[] data = cipher.update(info)
orbyte[] data = cipher.doFinal(info, 0, info.lenght)
需要注意:
-
如果数据长度超过了
cipher.getBlockSize()
,则需要分批调用cipher.update(info)
。 -
如果要使用 RSA 算法等双密钥算法,可以用如下方法产生密钥:
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
Key privateKey = keyPair.getPrivate();
Key publicKey = keyPair.getPublic();
- 如果要使用指定的密钥,可以使用下面的代码:
Key spec = new SecretKeySpec(keyBytes, "RSA");