Java Web系列:JAAS認證和授權基礎

剛哥521發表於2015-12-21

1.認證和授權概述

(1)認證:對使用者的身份進行驗證。

.NET基於的RBS(參考1)的認證和授權相關的核心是2個介面System.Security.Principal.IPrincipal和System.Security.Principal.IIdentity。我們自己實現認證過程,通過Thread.CurrentPrincipal來設定和讀取認證結果。認證成功後設定認證狀態和標識。

Java內建了的JAAS(參考2),核心是javax.security.auth.Subject類和javax.security.Principal介面。java對認證過程也提供了2個型別,javax.security.auth.login.LoginContext類和javax.security.auth.spi.LoginModule介面。我們自己實現認證過程,但只要實現了LoginModule就可以通過LoginContext使用一致的語法。

(2)授權:對使用者的許可權進行驗證,通常使用Role(角色)管理許可權。

.NET的支援基於角色的授權。.NET的IPrincipal介面的IsInRole方法是授權的核心。有兩種方式使用:1.使用內建的IPrincipal物件(如GenericIdentity),在認證的同時載入使用者的角色roles。2.自定義IPrincipal實現,實現自己的IsInRole邏輯。ASP.NET中實現的的RolePrincipal就通過將邏輯轉發給System.Web.Security.Roles靜態類。Roles依賴System.Web.Security.RoleProvider介面實現角色的查詢,我們可以通過web.config的相關節點來配置自定義的RoleProvider。

JAAS的IPrincipal介面沒有提供IsInRole方法,我們有2個選擇,要麼通過多個IPrincipal表示角色,要麼自定義實現IPrincipal新增角色支援。Tomcat容器實現的org.apache.catalina.realm.GenericPrincipal就和.NET中的System.Security.Principal.GenericIdentity十分類似的角色實現。

Java的Subject類和IPrincipal介面與.NET的IPrincipal介面和IIdentity的介面不容易對應。為了便於統一理解.NET和Java的核心型別,我們可以從成員的理解,可以認為Java的Principal型別相當於.NET中的IPrincipa和IIdentity兩個型別的作用。Subject只是作為Principal的聚合根。之前提到的Tomcat容器中的GenericPrincipal就即提供了hasRole和getName成員。Tomcat實現的HttpServletRequest對應的成員就是通過GenericPrincipal實現的。

# 成員 .NET Java
認證型別 AuthenticationType System.Security.Principal.IIdentity.AuthenticationType javax.servlet.http.HttpServletRequest.getAuthType()
標識名稱 Name System.Security.Principal.IIdentity.Name java.security.Principal.getName()
角色驗證 IsInRole System.Security.Principal.IPrincipal.IsInRole javax.servlet.http.HttpServletRequest.isUserInRole()

 

2..NET Web認證和授權

ASP.NET Forms認證主採用RolePrincipal主體,未認證使用者設定為GenericIdentity標識,已認證使用者設定為FormsIdentity。RolePrincipal在驗證角色時將使用Roles靜態類通過RoleProvider進行角色驗證。通過HttpContext.User可以直接呼叫主體。

實現一個最簡單的自定義RoleProvider只需要繼承並實現GetRolesForUser和IsUserInRole兩個方法,通常可以使用委託在Application_Start中注入的方式實現通用的RoleProvider。

ASP.NET的forms驗證通過FormsAuthentication傳送和登出用於認證的token,通過配置web.config可以讓不同的Web伺服器以相同的方式對token加密和解密以適應Web伺服器負載均衡。不適用cookie承載token時,可以自定義認證邏輯,比如通過url引數方式承載token配合ssl用於app客戶端驗證等。

.NET的認證和授權示意圖:

自定義RoleProvider的示例,省略了不需要實現的部分程式碼,GetRolesForUserDelegate和IsUserInRoleDelegate在Application_Start中注入即可徹底實現RoleProvider和應用服務程式碼的解耦:

    public class SimpleRoleProvider : RoleProvider
    {
        public static Func<string, string[]> GetRolesForUserDelegate;

        public static Func<string, string, bool> IsUserInRoleDelegate;

        public override string[] GetRolesForUser(string username)
        {
            return GetRolesForUserDelegate(username);
        }

        public override bool IsUserInRole(string username, string roleName)
        {
            return IsUserInRoleDelegate(username, roleName);
        }
    }

Forms身份驗證和RoleProvider的分別定義在web.config配置檔案中。ASP.NET的配置檔案示例(省略了其他配置):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <authentication mode="Forms">
      <forms loginUrl="~/Home/Login" cookieless="UseCookies" slidingExpiration="true" />
    </authentication>
    <roleManager defaultProvider="SimpleRoleProvider" enabled="true">
      <providers>
        <clear />
        <add name="SimpleRoleProvider" type="Onion.Web.SimpleRoleProvider" />
      </providers>
    </roleManager>
  </system.web>
</configuration>

.NET中還有用於配置Pricipal的兩個方法System.AppDomain.SetThreadPrincipal和System.AppDomain.SetPrincipalPolicy以及控制訪問的兩個型別System.Security.Permissions.PrincipalPermission和System.Security.Permissions.PrincipalPermissionAttribute

3.JAAS

HttpServletRequest介面定義了6個驗證和授權相關的方法getAuthType()、login()、logout()、getRemoteUser()、isUserInRole()、getUserPrincipal()。類似ASP.NET,Forms身份驗證也在配置檔案中進行配置。但由於Java熱衷於定義一堆介面將實現推遲到容器級別,LoginContext依賴的具體的LoginModule的配置也必須在容器中進行配置。因此除了web.xml,還需要配置在容器中配置JAAS的配置檔案。JAAS的示意圖:

(1)JAAS 內建的登入模組使用:NTLoginModule:

配置:

first{
    com.sun.security.auth.module.NTLoginModule Required debug=true;
};

程式碼

package com.test.jaas1;

import java.security.Principal;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

public class App {
    public static void main(String[] args) {
        System.setProperty("java.security.auth.login.config",
                Thread.currentThread().getContextClassLoader().getResource("jaas.config").getPath());
        try {
            LoginContext lc = new LoginContext("first");
            lc.login();
            System.out.println(lc.getSubject().getPrincipals().size());
            for (Principal item : lc.getSubject().getPrincipals()) {
                System.out.println(String.format("%s principal:%s", item.getClass().getTypeName(), item.getName()));
            }
            lc.logout();
            System.out.println(lc.getSubject().getPrincipals().size());
        } catch (LoginException e) {
            e.printStackTrace();
        }
    }
}

 (2)JAAS Tomcat容器下的登入模組使用(參考3):

用於配置Forms認證:web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1">
    <display-name>Archetype Created Web Application</display-name>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Admin</web-resource-name>
            <url-pattern>/admin/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>

    <security-role>
        <role-name>admin</role-name>
    </security-role>
    <login-config>
        <auth-method>FORM</auth-method>
        <form-login-config>
            <form-login-page>/login.html</form-login-page>
            <form-error-page>/error.html</form-error-page>
        </form-login-config>
    </login-config>
</web-app>
View Code

用於JAAS的LoginModule的配置:/main/java/resources/jaas.config

MyLogin{
    MyLoginModule Required debug=true;
};
View Code

用於Tomcat的配置:/main/webapp/META-INF/context.xml配置(這個依賴至少還在專案內,不需要修改tomcat):

<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <Realm className="org.apache.catalina.realm.JAASRealm" 
    appName="MyLogin"
    userClassNames="UserPrincipal"
    roleClassNames="RolePrincipal" />
</Context>
View Code

用於Tomcat的UserClass和RoleClass程式碼:

import java.security.Principal;

public class UserPrincipal implements Principal {

    private String _name;

    public UserPrincipal(String name) {
        this._name = name;
    }

    @Override
    public String getName() {

        return this._name;
    }

}
View Code

RoleClass:

import java.security.Principal;

public class RolePrincipal implements Principal {

    private String _name;

    public RolePrincipal(String name) {
        this._name = name;
    }

    @Override
    public String getName() {

        return this._name;
    }

}
View Code

用於JAAS配置檔案初始化的程式碼(為了依賴tomcat的配置,在Filter中設定配置檔案):

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

@WebFilter("/*")
public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // TODO 自動生成的方法存根
        System.setProperty("java.security.auth.login.config",
                Thread.currentThread().getContextClassLoader().getResource("jaas.config").getPath());
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        chain.doFilter(new MyRequest((HttpServletRequest) request), response);

    }

    @Override
    public void destroy() {
        // TODO 自動生成的方法存根

    }

}
View Code

用於登入的login.html:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<form method="post" action="j_security_check">
<table>
<tr><td><label>UserName</label></td><td><input type="text" name="j_username"></td></tr>
<tr><td><label>Password</label></td><td><input type="password" name="j_password"></td></tr>
<tr><td></td><td><input type="submit" value="Login"></td></tr>
</table>
</form>
</body>
</html>
View Code

MyLoginModule實現:其中的三個name屬性必須是固定值:j_security_checkj_usernamej_password

import java.io.IOException;
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;

public class MyLoginModule implements LoginModule {

    private CallbackHandler handler;
    private Subject subject;

    @Override
    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
            Map<String, ?> options) {
        handler = callbackHandler;
        this.subject = subject;
    }

    @Override
    public boolean login() throws LoginException {

        Callback[] callbacks = new Callback[2];
        callbacks[0] = new NameCallback("login");
        callbacks[1] = new PasswordCallback("password", true);

        try {
            handler.handle(callbacks);
            String name = ((NameCallback) callbacks[0]).getName();
            String password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword());

            if (name != null && name.equals("user123") && password != null && password.equals("pass123")) {
                return true;
            }

            // If credentials are NOT OK we throw a LoginException
            throw new LoginException("Authentication failed");

        } catch (IOException e) {
            throw new LoginException(e.getMessage());
        } catch (UnsupportedCallbackException e) {
            throw new LoginException(e.getMessage());
        }
    }

    @Override
    public boolean commit() throws LoginException {
        subject.getPrincipals().add(new UserPrincipal("user123"));
        subject.getPrincipals().add(new RolePrincipal("admin"));
        return true;
    }

    @Override
    public boolean abort() throws LoginException {
        // TODO 自動生成的方法存根
        return false;
    }

    @Override
    public boolean logout() throws LoginException {
        subject.getPrincipals().clear();
        return true;
    }

}
View Code

在.NET的RBS基礎上實現RBAC(參考4)是可行的,但JAAS....。JAAS只要用過的人都對其印象深刻

4.參考

(1)https://msdn.microsoft.com/en-us/library/52kd59t0(v=vs.90).aspx

(2)http://docs.oracle.com/javase/8/docs/technotes/guides/security/jaas/JAASRefGuide.html

(3)http://www.byteslounge.com/tutorials/jaas-form-based-authentication-in-tomcat-example

(4)http://csrc.nist.gov/groups/SNS/rbac/

5.小結:

JAAS抽象的不切實際,實現又全靠容器,不同容器的實現還不一致,IPrincipal又不能直接支援Servlet認證和授權相關的方法。至少應該像.NET一樣提供資料結構級別的角色認證型別,而不是跑偏成現在這樣。容器要麼自己擴充套件IPrincipal支援角色,要麼通過配置傳入指定型別的IPrincipal子類來區分角色和使用者。只能希望Apache Shiro和Spring等第三方提供的一些實現能有更高的可用性。

相關文章