Tomcat 7 JSP 页面:在 Tomcat 中使用 Shibboleth 身份验证会话进行身份验证时避免 500 错误

Tomcat 7 JSP page: Avoid 500 error when authenticating in Tomcat using Shibboleth authentication session

Tomcat 7 异常:

Apache Tomcat/7.0.63 - HTTP Status 500 - An exception occurred processing JSP page

参考:

我将此代码与我们的 i2b2 应用程序集成在一起,以便我们可以针对我们的 Shibboleth IdP 进行身份验证。

https://github.com/HSSC/i2b2-web-integration/blob/master/doc/INSTALL.md

申请(i2b2):

http://www.i2b2.org

testshib:

(我们按照此构建了 Shibboleth 服务提供商)

http://www.testshib.org/

Shibboleth IdP:

http://shibboleth.net/

问题:

页面在 JSP 页面上失败,状态代码为 500,在设置 "id" 的行上,因为它上面的行有一个 "auth" 变量(类型List<string> in Java) 为空。请注意,此页面包含 JSP 标签和 Javascript.

login.jsp 页面上的代码片段失败:

function initI2B2()
{

    // alert('initI2B2');
    <%
        shibboleth.ConnectDatabase auth = (shibboleth.ConnectDatabase) session.getAttribute("connectDatabase");
        String id = org.apache.commons.lang.StringEscapeUtils.escapeHtml(auth.getUser().identifier);  // fails here because the List<string> object is null
        String sessionId = org.apache.commons.lang.StringEscapeUtils.escapeHtml(auth.getUser().session);
        Calendar cal = Calendar.getInstance();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateTime = sdf.format(cal.getTime());
        String token = DigestUtils.sha512Hex(dateTime + sessionId);
    %>

    var passedInUserKey = "<%= org.apache.commons.lang.StringEscapeUtils.escapeHtml(auth.getUser().identifier)%>";
    var passedInPassKey = "<%= org.apache.commons.lang.StringEscapeUtils.escapeHtml(auth.getUser().session)%>";
    var passedInDomainK = "demo";
    var passedInInstitutionid = "1";
    var tokenV = "<%= token%>";

导致此问题的工作流程?

如果我们有一个好的会话,代码通常会很好地工作。我们现在的解决方法是让会话在 24 小时内超时。

这是导致错误的工作流程...

点击 i2b2 页面 > 自动重定向到 Shibboleth IdP > 使用 IdP 凭据登录 > 重定向回 i2b2 页面 > 此代码将用户和会话 ID(密码)从 Shibboleth 会话中取出并使用它自动授权进入 i2b2 应用程序。

login.jsp 页面被黑客入侵以从 Shibboleth 会话中获取数据,因此用户无需输入凭据。

问题是当 shibboleth session 超时,或者当 Tomcat session 过期或者浏览器中的缓存被清除时,就会出现这个错误,因为整个页面都需要来自 session 的那两个变量(ID 和密码)。

清除缓存是出现此错误的最快方法。另一种方法是将 Tomcat 默认会话超时从 30 分钟更改为 1 分钟。

<session-config>
    <session-timeout>1</session-timeout>
</session-config>

可能给出此问题可能答案的想法:

  1. 在ASP.NET中,如果会话不存在,您可以告诉它自动重定向到另一个页面(使用配置)。 Tomcat 是否通过给它一些条件(使用代码)来提供该功能?

  2. 我们如何强制终止 Tomcat 会话(不处理此页面),并像我们有一个全新的请求一样重定向到 i2b2?我知道在 ASP.NET 中,如果您收到应用程序错误,可以调用代码。 Tomcat 有什么优惠吗?

  3. 如果我们更想hack这个页面,如何简单的用一个条件来避免错误,但仍然有页面处理,然后重定向?

还有其他想法吗?

package edu.tmc.uth.i2b2shibboleth;

import java.security.*;
import java.sql.*;
import java.util.*;
import javax.faces.bean.*;
import javax.faces.context.*;
import javax.servlet.ServletContext;
import javax.servlet.http.*;

/**Manages the retrieval of Shibboleth attributes and the connection to the
 * i2b2 hive database.
 *
 * @author JRussell
 */
@ManagedBean(name = "connectDatabase")
@SessionScoped
public class ConnectDatabase implements HttpSessionBindingListener {

    private User user;
    private Connection connection;
    private List<String> test = new ArrayList<String>();
    String propertyPath = null;
    private ResourceBundle properties;

    public User getUser() {
        return user;
    }

    /*Main function for retrieving Shibboleth information and updating 
     * database records.  This is called from the Facelets page (index.xhtml).
     */
    public List<String> getUserInfo() {
        try {
            connection = connectToDatabase();
            setShibbolethAttributes();
            updateI2b2UserDatabase();

            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return test;
    }

    /*Returns a connection to the i2b2 hive database.
     * Uses the properties in the database.properties file.
     */
    public Connection connectToDatabase() {
        try {            
            properties = ResourceBundle.getBundle("edu/tmc/uth/i2b2shibboleth/database");
            String url = properties.getString("I2B2_PM.connectionURL");
            String userName = properties.getString("I2B2_PM.userName");
            String password = properties.getString("I2B2_PM.password");

            Class.forName(properties.getString("I2B2_PM.databaseClass"));
            connection = DriverManager.getConnection(url, userName, password);
            if (connection != null) {
                System.out.println("connected to DB");
                test.add("Connected to i2b2 hive database.");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return connection;
    }

    /*Populates the Shibboleth attributes into the User object.
     * The request header values are based on the attribute-map.xml configuration
     * file on the Shibboleth Service Provider.  The attribute identifiers and 
     * the attributes released are configured on an institutional basis.
     */
    public void setShibbolethAttributes() {
        HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();        
        String firstName = request.getHeader("Shib-InetOrgPerson-givenName");
        String lastName = request.getHeader("Shib-Person-surname");
        String email = request.getHeader("Shib-InetOrgPerson-mail");
        String identifier = request.getHeader("Shib-iamPerson-subjectUniqueId");
        String session = request.getHeader("Shib-Session-ID"); 
        user = new User(firstName, lastName, email, identifier, session);
        test.add("Basic User attributes");
        test.add(user.toString());
        test.add("All Shibboleth headers");
        //Outputs all of the headers from the Shibboleth request
        Enumeration enumer = request.getHeaderNames();
        while(enumer.hasMoreElements()){
            String headerName = enumer.nextElement().toString();
            if(headerName.startsWith("Shib")){
            test.add(headerName+" - "+request.getHeader(headerName));
            }
        }
    }

    /*Add a new user to the i2b2 hive database if they don't have an existing record. 
     * Update the password and enable the user if the i2b2 user already exists. 
     */
    private void updateI2b2UserDatabase() throws SQLException {
        //Try to find current user in database
        String stmt = "SELECT user_id FROM pm_user_data WHERE user_id=?";
        PreparedStatement pst = connection.prepareStatement(stmt);
        pst.setString(1, user.identifier);
        System.out.println(stmt+" "+user.identifier);
        ResultSet result = pst.executeQuery();
        //user record found in database so update password
        if (result.next()) {
            System.out.println("user record found");
            test.add("Subject identifier already in database -  " + result.getString("user_id"));
            stmt = "UPDATE pm_user_data SET password=?, status_cd=? WHERE user_id=?";
            pst = connection.prepareStatement(stmt);
            pst.setString(1, encryptMD5(user.session));
            pst.setString(2, "A");
            pst.setString(3, user.identifier);
            pst.executeUpdate();
            connection.commit();
        } 
        else { //new user so add record to database
            test.add("New user - " + user.identifier + " " + user.first + " " + user.last);
            stmt = "INSERT INTO pm_user_data (user_id, full_name, password, email, status_cd) \n"
                    + "VALUES (?,?,?,?,?)";
            pst = connection.prepareStatement(stmt);
            pst.setString(1, user.identifier);
            pst.setString(2, user.first + " " + user.last);
            pst.setString(3, encryptMD5(user.session));
            pst.setString(4, user.email);
            pst.setString(5, "A");
            pst.executeUpdate();

            //assign user roles to i2b2 project
            String project = properties.getString("I2B2_PM.projectName");
            pst = connection.prepareStatement("INSERT INTO pm_project_user_roles (project_id, user_id, user_role_cd, status_cd) \n"
                    + "VALUES (?,?,?,?)");
            pst.setString(1, project);
            pst.setString(2, user.identifier);
            pst.setString(3, "DATA_OBFSC");
            pst.setString(4, "A");
            pst.executeUpdate();
            pst.setString(3, "DATA_AGG");
            pst.executeUpdate();
            pst.setString(3, "USER");
            pst.executeUpdate();
            connection.commit();
        }
    }

    /* The i2b2 applications expect passwords to be encrypted.
    *This function encrypts passwords with MD5 before inserting them
    *into the database.
     */
    private String encryptMD5(String text) {
        String encrypted = "";

        byte[] defaultBytes = text.getBytes();
        try {
            MessageDigest algorithm = MessageDigest.getInstance("MD5");
            algorithm.reset();
            algorithm.update(defaultBytes);
            byte messageDigest[] = algorithm.digest();

            StringBuilder hexString = new StringBuilder();
            for (int i = 0; i < messageDigest.length; i++) {
                hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
            }
            encrypted = hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return encrypted;
    }

    /*Function needed to implement HttpSessionBindingListener.
     * Don't need to modify the function itself. 
     */
    public void valueBound(HttpSessionBindingEvent event) {
        //do nothing
    }

    //This function deactivates a user in the i2b2 hive database when 
    //the user session times out.  Session timeout is set in web.xml.
    public void valueUnbound(HttpSessionBindingEvent event) {
        connection = connectToDatabase();
        try {
            //Try to find current user in database
            String stmt = "SELECT user_id FROM pm_user_data WHERE user_id=?";
            PreparedStatement pst = connection.prepareStatement(stmt);
            pst.setString(1, user.identifier);
            ResultSet result = pst.executeQuery();
            //user record found in database so deactivate them since they have logged off
            if (result.next()) {
                System.out.println("in results.next");
                stmt = "UPDATE pm_user_data SET status_cd=? WHERE user_id=?";
                pst = connection.prepareStatement(stmt);
                pst.setString(1, "D");
                pst.setString(2, user.identifier);
                pst.executeUpdate();
                connection.commit();
            }
            connection.close();
            ServletContext context = event.getSession().getServletContext();
            FacesContext.getCurrentInstance().getExternalContext().redirect("logout.xhtml");
        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }
}

Tomcat 实际上有一种方法可以捕获未处理的应用程序异常。它有点等同于 ASP.NET Application_Error 事件。这可能不是一个优雅的解决方案,但它现在有效。每当我们遇到任何 JSP 异常时,我们应该被重定向到这个 error.jsp 页面,然后通过在 error.jsp 页面代码中强制重定向到 Shibboleth IdP 登录页面。

在部署 web.xml 文件中进行设置。这是相对于 i2b2 部署文件夹的。

<error-page>
        <exception-type>java.lang.Throwable</exception-type>
        <location>/error/error.jsp</location>
</error-page>

然后我把这个放到error.jsp页面:

<%@ page isErrorPage="true" import="java.io.*" contentType="text/plain"%>

Message:
<%=exception.getMessage()%>

StackTrace:
<%
    // redirect to Shibboleth IdP login page
    String redirectURL = "https://redirecturl/";
    response.sendRedirect(redirectURL);
%>

然后重新启动 Linux 服务。必须按此顺序完成,因为存在与 shibd 和 tomcat 服务的依赖关系。

service httpd stop
service tomcat stop
service jboss stop
service shibd stop
service shibd start
service jboss start
service tomcat start
service httpd start