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()