基于Java JAAS表单的身份验证

本文详述了如何使用 Java Authentication and Authorization Service (JAAS) 实现表单认证,包括创建登录模块类、配置 JAAS 和 Tomcat 的步骤,以及如何在 web.xml 中配置安全约束。
使用JAAS实现登录模块是一个高级主题,而且大多数开发人员也很少有机会参与这种开发。 但是JAAS登录模块的基本实现并不是那么困难,这是因为我打算将其发布。
在这里,我正在解释如何实现tomcat管理的身份验证模块。 此实现与容器无关。 我们可以将其与稍有更改的任何容器一起使用。
第一步,我们需要创建一个实现javax.security.auth.spi.LoginModule接口的登录模块类。 该接口提供了必须由我们的登录模块类实现的5种方法。 这些是initialize(),login(),commit(),abort(),logout()
initialize()方法传递了四个参数。
“主题”是什么,我们需要进行身份验证。 主题可以代表单个登录用户的相关信息。 它可以表示“用户名”,“密码”等身份。此外,它还可以表示分配给用户的角色。 所有这些身份都应表示为java.security.Principal。 因此,我们应该通过实现java.security.Principal创建单独的类来区分这些实体。 在本教程中,我为用户名,密码和角色创建了单独的类,分别为JAASUserPrincipal,JAASPasswordPrincipal和JAASRolePrincipal。 主题的getPrincipals()方法返回一组与主题关联的java.security.Principal。 为了区别这些,为每个身份创建单独的类很重要。
CallbackHandler ”用于与用户通信。 通过登录模块对用户进行身份验证时,登录模块将调用CallbackHandler实例的handle()方法以获取用户名和密码。 我们还不想担心CallbackHandler实例。 因为容器设法提供所需的callbakcs。 Tomcat为此提供了JAASCallbackHandler 。 但是,如果我们要显式调用身份验证,则需要通过实现javax.security.auth.callback.CallbackHandler来创建自己的回调处理程序类。 我将在本教程的结尾进行解释。
initialize()方法的下一个重要参数是'options'。 这些选项我们在' jass.config '文件中声明。 通过初始化登录模块,提供了在“ jass.config”文件中声明的选项的映射。 稍后我将解释本教程的“ jaas.config”文件。
接下来,我将显示我们的主体类的完整源代码。

JAASUserPrincipal.java

package com.rainyday.server.login;

 import java.io.Serializable;
 import java.security.Principal;

/**
 * @author semika
 *
 */
 public class JAASUserPrincipal implements Principal, Serializable {

 private String name;
 
 /**
  * @param name
  */
 public JAASUserPrincipal(String name) {
  
 if (name == null) {
     throw new NullPointerException("NULL user name");
 }
     this.name = name;
 }
 
 @Override
 public String getName() {
     return name;
 }

 @Override
 public String toString() {
     return "UserPrincipal [name=" + name + "]";
 }

 @Override
 public int hashCode() {
     final int prime = 31;
     int result = 1;
     result = prime * result + ((name == null) ? 0 : name.hashCode());
     return result;
 }

 @Override
 public boolean equals(Object obj) {
     if (this == obj)
        return true;
     if (obj == null)
        return false;
     if (getClass() != obj.getClass())
        return false;
     JAASUserPrincipal other = (JAASUserPrincipal) obj;
     if (name == null) {
        if (other.name != null)
           return false;
     } else if (!name.equals(other.name))
        return false;
  
     return true;
 }
}

JAASRolePrincipal.java

package com.rainyday.server.login;

 import java.io.Serializable;
 import java.security.Principal;

/**
 * @author semika
 *
 */
 public class JAASRolePrincipal implements Principal, Serializable {

 private String name;
 
 /**
  * @param name
  */
 public JAASRolePrincipal(String name) {
     if (name == null) {
        throw new NullPointerException("NULL role name");
     }
     this.name = name;
 }

 @Override
 public String getName() {
    return name;
 }

 @Override
 public String toString() {
     return "JASSRolePrincipal [name=" + name + "]";
 }

 @Override
 public int hashCode() {
     final int prime = 31;
     int result = 1;
     result = prime * result + ((name == null) ? 0 : name.hashCode());
     return result;
 }

 @Override
 public boolean equals(Object obj) {
     if (this == obj)
        return true;
     if (obj == null)
        return false;
     if (getClass() != obj.getClass())
        return false;
     JAASRolePrincipal other = (JAASRolePrincipal) obj;
     if (name == null) {
        if (other.name != null)
           return false;
     } else if (!name.equals(other.name))
        return false;
     
     return true;
 }
}

JAASPasswordPrincipal.java

package com.rainyday.server.login;

 import java.io.Serializable;
 import java.security.Principal;

/**
 * @author semika
 *
 */
 public class JAASPasswordPrincipal implements Principal, Serializable {

 private String name;
 
 /**
  * @param name
  */
 public JAASPasswordPrincipal(String name) {
     if (name == null) {
        throw new NullPointerException("NULL password.");
     }
     this.name = name;
 }

 @Override
 public String getName() {
     return name;
 }

 @Override
 public int hashCode() {
     final int prime = 31;
     int result = 1;
     result = prime * result + ((name == null) ? 0 : name.hashCode());
     return result;
 }

 @Override
 public boolean equals(Object obj) {
     if (this == obj)
         return true;
     if (obj == null)
         return false;
     if (getClass() != obj.getClass())
         return false;
     JAASPasswordPrincipal other = (JAASPasswordPrincipal) obj;
     if (name == null) {
        if (other.name != null)
           return false;
     } else if (!name.equals(other.name))
        return false;
     
     return true;
 }

 @Override
 public String toString() {
     return "JAASPasswordPrincipal [name=" + name + "]";
 }

}
以上三个类是完全相似的。 但是,我们需要为每个主体创建单独的类以区分它们。
登录模块的login()方法执行身份验证。 这将验证用户输入的数据库用户名和密码。 现在,您可能会遇到问题,用户输入的登录详细信息如何进入login()方法。 如前所述,回调处理程序将登录标识带入login()方法。 从login()方法中,login模块通过将所需的回调传递到其中来调用回调处理程序的handle()方法。 然后,handle()方法将使用所需的信息填充这些回调,并使其可用于login()方法。
成功认证后,登录模块将调用commit()方法。 可以使用相关的主体填充主题。 例如,我们可以从数据库中检索用户分配的角色,并将这些角色附加到主题中。
接下来,我将解释运行此登录模块所需的配置。
我们应该创建“ jass.config”文件,并将其放置在“ $ CATALINA_HOME / conf ”下。
本教程的“ jass.config”文件如下。
rainyDay {
   com.rainyday.server.login.JAASLoginModule required
   dbDriver="com.mysql.jdbc.Driver"
   dbURL="jdbc:mysql://localhost/rainyday"
   dbUser="root"
   dbPassword="abc123"
   userQuery="select username from secu_user where secu_user.username=? and secu_user.password=?"
   roleQuery="select secu_user_role.rolename from secu_user, secu_user_role "
             + "where secu_user.username=secu_user_role.username and secu_user.username=?"
   debug=true;
};

“ jass.config”文件应具有类似的格式。 除了登录模块声明之外,您还可以根据需要声明选项。 登录模块可以在initialize()方法参数中使用“选项”映射来使用这些选项。

另外,我们应该通过添加JAVA_OPTS环境变量的路径来告诉tomcat,“ jaas.config”文件的位置。 我将其添加到$ CATALINA_HOME / bin下的'catalina.sh'文件中,如下所示。

JAVA_OPTS =” $ JAVA_OPTS -Djava.security.auth.login.config == .. / conf / jaas.config”

接下来,您需要声明JAASRealm配置。 您可以在$ CATALINA_HOME / conf下的server.xml文件中添加一个新的“ Realm”条目。 在我们的教程中,“领域”条目如下。

<Realm className="org.apache.catalina.realm.JAASRealm"
 appName="rainyDay"
 userClassNames="com.rainyday.server.login.JASSUserPrincipal,com.rainyday.server.login.JAASPasswordPrincipal"
 roleClassNames="com.rainyday.server.login.JASSRolePrincipal"/>

有关apache tomcat的领域配置,您可以查看此文档 。 jaas登录模块的完整源代码。

JAASLoginModule.java

package com.rainyday.server.login;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import org.apache.log4j.Logger;

/**
 * @author semika
 *
 */
public class JAASLoginModule implements LoginModule { 
 
    private static Logger LOGGER = Logger.getLogger(JAASLoginModule.class); 
 
    // initial state
    private Subject subject;
    private CallbackHandler callbackHandler;
    private Map sharedState;
    private Map options;

    // configurable option
    private boolean debug = false;
    
    // the authentication status
    private boolean succeeded = false;
    private boolean commitSucceeded = false;
    
    //user credentials
    private String username = null;
    private char[] password = null;
    
    //user principle
    private JAASUserPrincipal userPrincipal = null;
    private JAASPasswordPrincipal passwordPrincipal = null;
    
    public JAASLoginModule() {
         super();
    }

    @Override
    public void initialize(Subject subject, CallbackHandler callbackHandler,
                Map<string, ?=""> sharedState, Map<string, ?=""> options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.sharedState = sharedState;
        this.options = options;
  
        debug = "true".equalsIgnoreCase((String)options.get("debug")); 
    }

    @Override
    public boolean login() throws LoginException {
  
        if (callbackHandler == null){
            throw new LoginException("Error: no CallbackHandler available " +
            "to garner authentication information from the user");
        }
        Callback[] callbacks = new Callback[2];
        callbacks[0] = new NameCallback("username");
        callbacks[1] = new PasswordCallback("password: ", false);
  
        try {
   
            callbackHandler.handle(callbacks);
            username = ((NameCallback)callbacks[0]).getName();
            password = ((PasswordCallback)callbacks[1]).getPassword();
   
            if (debug) {
                LOGGER.debug("Username :" + username);
                LOGGER.debug("Password : " + password);
            }
   
            if (username == null || password == null) {
                LOGGER.error("Callback handler does not return login data properly");
                throw new LoginException("Callback handler does not return login data properly"); 
            }
   
            if (isValidUser()) { //validate user.
                succeeded = true;
                return true;
            } 
   
        } catch (IOException e) { 
             e.printStackTrace();
        } catch (UnsupportedCallbackException e) {
             e.printStackTrace();
        }
  
        return false;
    }

    @Override
    public boolean commit() throws LoginException {
        if (succeeded == false) {
            return false;
        } else { 
            userPrincipal = new JAASUserPrincipal(username);
            if (!subject.getPrincipals().contains(userPrincipal)) {
                subject.getPrincipals().add(userPrincipal);
                LOGGER.debug("User principal added:" + userPrincipal);
            }
            passwordPrincipal = new JAASPasswordPrincipal(new String(password)); 
            if (!subject.getPrincipals().contains(passwordPrincipal)) {
                subject.getPrincipals().add(passwordPrincipal);
                LOGGER.debug("Password principal added: " + passwordPrincipal);
            }
      
            //populate subject with roles.
            List<string> roles = getRoles();
            for (String role: roles) {
                JAASRolePrincipal rolePrincipal = new JAASRolePrincipal(role);
                if (!subject.getPrincipals().contains(rolePrincipal)) {
                    subject.getPrincipals().add(rolePrincipal); 
                    LOGGER.debug("Role principal added: " + rolePrincipal);
                }
            }
      
            commitSucceeded = true;
      
            LOGGER.info("Login subject were successfully populated with principals and roles"); 
      
            return true;
       }
   }

   @Override
   public boolean abort() throws LoginException {
      if (succeeded == false) {
          return false;
      } else if (succeeded == true && commitSucceeded == false) {
          succeeded = false;
          username = null;
          if (password != null) {
              password = null;
          }
          userPrincipal = null;    
      } else {
          logout();
      }
      return true;
   }

    @Override
    public boolean logout() throws LoginException {
        subject.getPrincipals().remove(userPrincipal);
        succeeded = false;
        succeeded = commitSucceeded;
        username = null;
        if (password != null) {
            for (int i = 0; i < password.length; i++){
                password[i] = ' ';
                password = null;
            }
        }
        userPrincipal = null;
        return true;
   }
 
   private boolean isValidUser() throws LoginException {

      String sql = (String)options.get("userQuery");
      Connection con = null;
      ResultSet rs = null;
      PreparedStatement stmt = null;
  
      try {
          con = getConnection();
          stmt = con.prepareStatement(sql);
          stmt.setString(1, username);
          stmt.setString(2, new String(password));
   
          rs = stmt.executeQuery();
   
          if (rs.next()) { //User exist with the given user name and password.
              return true;
          }
       } catch (Exception e) {
           LOGGER.error("Error when loading user from the database " + e);
           e.printStackTrace();
       } finally {
           try {
               rs.close();
           } catch (SQLException e) {
               LOGGER.error("Error when closing result set." + e);
           }
           try {
               stmt.close();
           } catch (SQLException e) {
               LOGGER.error("Error when closing statement." + e);
           }
           try {
               con.close();
           } catch (SQLException e) {
               LOGGER.error("Error when closing connection." + e);
           }
       }
       return false;
   }

 /**
  * Returns list of roles assigned to authenticated user.
  * @return
  */
  private List<string> getRoles() { 
  
      Connection con = null;
      ResultSet rs = null;
      PreparedStatement stmt = null;
  
      List<string> roleList = new ArrayList<string>(); 
  
      try {
          con = getConnection();
          String sql = (String)options.get("roleQuery");
          stmt = con.prepareStatement(sql);
          stmt.setString(1, username);
   
          rs = stmt.executeQuery();
   
          if (rs.next()) { 
              roleList.add(rs.getString("rolename")); 
          }
      } catch (Exception e) {
          LOGGER.error("Error when loading user from the database " + e);
          e.printStackTrace();
      } finally {
           try {
               rs.close();
           } catch (SQLException e) {
               LOGGER.error("Error when closing result set." + e);
           }
           try {
               stmt.close();
           } catch (SQLException e) {
               LOGGER.error("Error when closing statement." + e);
           }
           try {
               con.close();
           } catch (SQLException e) {
               LOGGER.error("Error when closing connection." + e);
           }
       }
       return roleList;
 }
 
 /**
  * Returns JDBC connection
  * @return
  * @throws LoginException
  */
  private Connection getConnection() throws LoginException {
  
      String dBUser = (String)options.get("dbUser");
      String dBPassword = (String)options.get("dbPassword");
      String dBUrl = (String)options.get("dbURL");
      String dBDriver = (String)options.get("dbDriver");

      Connection con = null;
      try {
         //loading driver
         Class.forName (dBDriver).newInstance();
         con = DriverManager.getConnection (dBUrl, dBUser, dBPassword);
      } 
      catch (Exception e) {
         LOGGER.error("Error when creating database connection" + e);
         e.printStackTrace();
      } finally {
      }
      return con;
   }
}
如果login()或commit()方法执行中出现问题,将调用login模块的abort()方法。 在这种情况下,如果身份验证过程失败,我们不能说身份验证过程已成功完成,并且可以在abort()方法内完成所需的清理操作。
我们可以利用在登录模块的initialize()方法中初始化的“选项”映射来获取在“ jass.config”文件中声明的配置信息。 您可以想出一种好的技术来获取JDBC连接对象。 在本教程中,我并没有专心于此,而只是想向您展示机制,即如何完成事情。
至此,我们已经完成了基本JAAS身份验证模块所需的工作。 接下来,我们应该在web.xml文件中配置安全约束。
<login-config>
     <auth-method>FORM</auth-method>
     <realm-name>rainyDay</realm-name>
     <form-login-config>
          <form-login-page>/login.jsp</form-login-page>
          <form-error-page>/error.jsp</form-error-page>
     </form-login-config>
</login-config>
<security-role>
    <role-name>*</role-name>
</security-role>
<security-constraint>
    <web-resource-collection>
         <web-resource-name>Rainy day</web-resource-name>
         <url-pattern>/</url-pattern>
         <http-method>POST</http-method>
         <http-method>GET</http-method>
    </web-resource-collection>
    <auth-constraint>
         <role-name>*</role-name>
    </auth-constraint>
</security-constraint>

由于上述安全限制,如果在没有身份验证的情况下对应用程序受保护区域中的特定资源提出了某些请求,则该请求将被重定向到“登录”页面。 接下来,我将向您展示简单HTML表单,该表单将在表单提交后调用我们的登录模块。

<form id="loginForm" name="loginForm" method="post" action="j_security_check">
        User Name : <input id="username" type="text" name="j_username" class="textbox"></input>
        Password : <input id="password" type="password" name="j_password" class="textbox"></input>
        <input name="login" type="submit" value="LOGIN" id="submit" class="button blue">
 </form>
到此,我们完成了。这是JAAS的非常基本的实现。 这种JAAS模块的优点在于,我们只需更改一次配置即可更改为其他登录模块实现,而无需对现有代码进行任何修改。 而且这是与容器无关的。 如果要使用jBoss服务器而不是“ jass.config”来部署它,则可以使用jboss conf文件夹中的“ login-config.xml”文件。 正如我答应过的,这里就是如何显式调用这种登录模块。 在某些情况下,我们需要以实用的方式对特定用户进行身份验证,但仍然应该使用已实现的登录模块。 在这种情况下,最大的问题是向我们的登录模块提供用户身份(用户名,密码等)。 在上述情况下,我们使用了一个“ CallbackHandler”类,该类是apache catalina提供的“ JAASCallbackHander”。 但是,在这里我们不能使用和我们必须实现我们自己的回调处理程序类。

JAASCallbackHandler.java

package com.rainyday.server.login;

import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.log4j.Logger;

/**
 * @author semika
 *
 */
 public class JAASCallbackHandler implements CallbackHandler {

 private static final Logger LOGGER = Logger.getLogger(JAASCallbackHandler.class);
 
 private String username = null;
 private String password = null;
 
 /**
  * @param username
  * @param password
  */
 public JAASCallbackHandler(String username, String password) {
     this.username = username;
     this.password = password;
 }


 @Override
 public void handle(Callback[] callbacks) throws IOException,
   UnsupportedCallbackException {
  
     LOGGER.info("Callback Handler invoked ");
  
     for (int i = 0; i < callbacks.length; i++) {
        if (callbacks[i] instanceof NameCallback) {
           NameCallback nameCallback = (NameCallback) callbacks[i];
           nameCallback.setName(username);
        } else if (callbacks[i] instanceof PasswordCallback) {
           PasswordCallback passwordCallback = (PasswordCallback) callbacks[i];
           passwordCallback.setPassword(password.toCharArray());
        } else {
           throw new UnsupportedCallbackException(callbacks[i], "The submitted Callback is unsupported");
        }
     }
 }
}

接下来,我们必须创建一个' LoginContext'实例来显式调用身份验证。

LoginContext lc = null;
    try {
        lc = new LoginContext("rainyDay", new JAASCallbackHandler(username, password));
        lc.login();
        //get the subject.
        Subject subject = lc.getSubject();
        //get principals
        subject.getPrincipals();
        LOGGER.info("established new logincontext");
    } catch (LoginException e) {
        LOGGER.error("Authentication failed " + e);
    }
如果我们最终无一例外地执行了以上代码,则表明身份验证已成功。 如果遇到异常,则认证失败。
这就是本教程中的全部内容。 http://docs.oracle.com/javaee/1.4/tutorial/doc/Security5.html是我了解登录身份验证的很好的教程。

参考:来自我们JCG合作伙伴 Semika loku kaluge的基于Java表单的身份验证 ,位于Code Box博客上。


翻译自: https://www.javacodegeeks.com/2012/06/java-jaas-form-based-authentication.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值