JAASRealm 是由 JAAS(Java Authentication and Authorization Service,Java 验证与授权服务)验证用户的一种领域实现。JAASRealm 实现支持如下 Realm 属性:
className 此领域实现的 Java 类名,对于 JAASRealm 必须是 org.apache.catalina.realm.JASSRealm
appName 传给 JAAS LoginContext 构造函数(并基于 JAAS 配置挑选适当的登录方法)的应用程序名。默认值是“Tomcat”,不过可以在 -Djava.security.auth.login.config=**/jaas.config 文件中更改对应名,即可设置成任何所要的值。
userClassNames 代表个别用户的 javax.security.Principal 类清单,以逗号分隔。对于 UnixLoginModule 设定值,应当包括 UnixPrincipal 类
roleClassNames 代表安全角色的 javax.security.Principal 类清单,以逗号分隔。对于 UnixLoginModule,设定值应该包括 UnixNumericGroupPrincipal 类
useContextClassLoader 告知 JAASRealm,是否使用上下文类加载器加载类。如果设置为 true,则使用上下文类加载器加载类;否则,使用 Tomcat 自身的类加载器加载类,默认值为 true。
(1)实现自己的用户和角色类,需要实现 javax.security.Principal 接口,实现 getName() 方法
(2)实现自己的 LoginModule,需要实现 javax.security.auth.spi.LoginModule 接口中的 initialize()、login()、commit()、abort() 和 logout() 方法,这些方法的触发顺序如下图:
(3)将上面的实现编译,然后打包成一个 jar 包。
(4)将编译后打的 jar 包拷贝到 %CATALINA_HOME%/lib 目录下面
(5)在 %CATALINA_HOME%/config 目录下面创建一个 jaas.config 目录,配置我们自定义的 LoginModule
(6)在 %CATALINA_HOME%/bin/catalina.bat 脚本中配置 JAVA_OPTS 环境变量,指定我们创建的 jaas.config 配置
(7)配置 server.xml 文件,开启 JAASReal 领域验证
(8)通过浏览器访问 http://localhost:8080/manager/html 地址验证
javax.security.Principal 接口表示主体的抽象概念,它可以用来表示任何实体,例如,个人、公司或登录ID。该接口仅仅提供了一个方法,即 getName() 用来返回此主体的名称。注意:实现此接口一般也需要重写 equals() 和 hashCode() 方法。
javax.security.auth.spi.LoginModule 接口是一个 SPI 接口(服务提供接口 Service Provider Interface),该接口由验证技术提供者实现,即由我们实现该接口去验证用户名和密码是否匹配。该接口提供了如下几个方法:
initialize() 用来初始化 LoginModule
login() 用户登录时触发,可以拿到用户名、密码,然后进行用户和密码匹配验证
commit() 登录(login)成功时触发,用来注册用户和角色
abort() 登录(login)失败是触发,清理残留数据
logout() 退出登录时被触发,清理用户、权限等信息
上面这些方法均是由 JAAS 自动调用,我们只需要在对应的方法中实现对应的功能即可。这里有个疑问?我们怎样将我们实现的 LoginModule 类告知 JAAS 呢!当然是通过 jaas.conf 配置文件告知。
上图是该示例的项目结构,项目实现了自己的用户(User)、角色(Role)和 LoginModule 类。
(1)实现用户对象,代码如下:
package com.hxstrive.tomcat.demo; import java.security.Principal; import java.util.Objects; /** * @author hxstrive.com 2022/6/8 */ public class User implements Principal { private String username; public User(String username) { this.username = username; } @Override public String getName() { return this.username; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User user = (User) o; return username.equals(user.username); } @Override public int hashCode() { return Objects.hash(username); } }
(2)实现角色对象,代码如下:
package com.hxstrive.tomcat.demo; import java.security.Principal; import java.util.Objects; /** * @author hxstrive.com 2022/6/8 */ public class Role implements Principal { private String roleName; public Role(String roleName) { this.roleName = roleName; } @Override public String getName() { return this.roleName; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Role role = (Role) o; return roleName.equals(role.roleName); } @Override public int hashCode() { return Objects.hash(roleName); } }
(3)自定义 LoginModule 实现类,代码如下:
package com.hxstrive.tomcat.demo; import javax.security.auth.Subject; import javax.security.auth.callback.*; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; import java.util.Arrays; import java.util.Map; /** * 自定义登录验证 * @author hxstrive.com 2022/6/8 */ public class MyLoginModule implements LoginModule { /** 用户名 */ private String username; /** 密码 */ private char[] password; /** 标记用户是否 login() 登录成功(true-登录成功;false-登录失败) */ private boolean succeeded = false; /** 标记 commit() 是否成功(true-成功;false-失败) */ private boolean commitSucceeded = false; /** 用户对象 */ private User user; /** 角色对象 */ private Role role; private Subject subject; private CallbackHandler callbackHandler; private Map<String, ?> sharedState; private Map<String, ?> options; /** * 初始化此 LoginModule * @param subject 要进行验证的 Subject * @param callbackHandler 用来与最终用户通信的 CallbackHandler * @param sharedState 与其他已配置的 LoginModule 共享的状态 * @param options 在登录 Configuration 中为此特定的 LoginModule 指定的选项 */ @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { System.out.println("MyLoginModule.initialize()"); this.subject = subject; this.callbackHandler = callbackHandler; this.sharedState = sharedState; this.options = options; } /** * 对 Subject 进行验证的方法 * @return * @throws LoginException */ @Override public boolean login() throws LoginException { System.out.println("MyLoginModule.login()"); // 提示输入用户名和密码 if (this.callbackHandler == null) { throw new LoginException("No CallBackHandler!"); } Callback[] callbacks = new Callback[2]; callbacks[0] = new NameCallback("输入用户名"); callbacks[1] = new PasswordCallback("输入密码", false); // 获取用户名和密码 try { this.callbackHandler.handle(callbacks); this.username = ((NameCallback) callbacks[0]).getName(); this.password = ((PasswordCallback) callbacks[1]).getPassword(); } catch (Exception e) { e.printStackTrace(); } // 这里就可以使用 JDBC 将用户输入的用户名/密码和数据库中的用户进行对比 if("tomcat".equals(this.username)) { if("aaaaaa".equals(String.valueOf(this.password))) { this.succeeded = true; System.out.println("登录成功!username=" + this.username + ", password=" + String.valueOf(this.password)); return this.succeeded; } else { throw new FailedLoginException("登录失败,密码错误"); } } else { throw new FailedLoginException("登录失败,用户名不存在"); } } /** * 提交验证过程的方法 * @return * @throws LoginException */ @Override public boolean commit() throws LoginException { System.out.println("MyLoginModule.commit()"); if(this.succeeded) { this.commitSucceeded = true; // 创建和注册用户信息 this.user = new User(this.username); if (!subject.getPrincipals().contains(user)) { subject.getPrincipals().add(user); } // 创建角色和注册角色信息 this.role = new Role("manager-gui"); if (!subject.getPrincipals().contains(role)) { subject.getPrincipals().add(role); } System.out.println("添加 Subject 成功!"); // 将临时用户名和密码清空 this.username = ""; Arrays.fill(this.password, ' '); return this.commitSucceeded; } else { return false; } } /** * 中止验证过程触发该方法 * @return * @throws LoginException */ @Override public boolean abort() throws LoginException { System.out.println("MyLoginModule.abort()"); if (!this.succeeded) { // 登录失败 return false; } else if (!this.commitSucceeded) { // 登录成功,提交角色权限信息失败,即整体身份验证失败 this.succeeded = false; this.username = null; Arrays.fill(this.password, ' '); this.user = null; this.role = null; } else { // 成功登录且提交角色权限信息 logout(); } return true; } /** * 注销 Subject 触发该方法 * @return * @throws LoginException */ @Override public boolean logout() throws LoginException { System.out.println("MyLoginModule.logout()"); // 将用户和角色信息从 JAAS 中移除 this.subject.getPrincipals().remove(user); this.subject.getPrincipals().remove(role); // 清空数据 this.succeeded = false; this.commitSucceeded = false; this.username = null; Arrays.fill(this.password, ' '); this.user = null; this.role = null; return true; } }
(4)在 %CATALINA_HOME%/conf 目录下创建 jaas.conf 配置文件,内容如下:
MyRealm{ com.hxstrive.tomcat.demo.MyLoginModule required debug=true; };
上面文件将配置自定义的 LoginModule。
(5)修改 %CATALINA_HOME%/bin/catalina.bat 脚本,修改 JAVA_OPTS 环境变量。如下:
set JAVA_OPTS=-Djava.security.auth.login.config==%CATALINA_HOME%/conf/jaas.config
(6)修改 %CATALINA_HOME%/conf/server.xml 配置文件,添加如下内容:
<!-- Use the LockOutRealm to prevent attempts to guess user passwords via a brute-force attack --> <Realm className="org.apache.catalina.realm.LockOutRealm"> <Realm className="org.apache.catalina.realm.JAASRealm" appName="MyRealm" userClassNames="com.hxstrive.tomcat.demo.User" roleClassNames="com.hxstrive.tomcat.demo.Role" debug="99"/> </Realm>
上面配置文件中的 appName 属性取值来自创建的 jaas.config 中配置的 MyRealm。
(7)使用浏览器访问 http://localhost:8080/manager/html,如下图:
如果我们输入用户名/密码(tomcat/aaaaaa),控制台输出信息如下:
MyLoginModule.initialize() MyLoginModule.login() 登录成功!username=tomcat, password=aaaaaa MyLoginModule.commit() 添加 Subject 成功!
如果我们用户名/密码输入错误,控制台输出信息如下:
MyLoginModule.initialize() MyLoginModule.login() MyLoginModule.abort()