Tomcat JAASRealm 领域

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。

使用 JAAS 领域步骤

(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  地址验证

JAAS 预备知识

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 配置文件告知。

JAAS 简单示例

上图是该示例的项目结构,项目实现了自己的用户(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()
说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号