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

1.認證和受權概述

(1)認證:對用戶的身份進行驗證。html

.NET基於的RBS(參考1)的認證和受權相關的核心是2個接口System.Security.Principal.IPrincipal和System.Security.Principal.IIdentity。咱們本身實現認證過程,經過Thread.CurrentPrincipal來設置和讀取認證結果。認證成功後設置認證狀態和標識。java

Java內置了的JAAS(參考2),核心是javax.security.auth.Subject類和javax.security.Principal接口。java對認證過程也提供了2個類型,javax.security.auth.login.LoginContext類和javax.security.auth.spi.LoginModule接口。咱們本身實現認證過程,但只要實現了LoginModule就能夠經過LoginContext使用一致的語法。web

(2)受權:對用戶的權限進行驗證,一般使用Role(角色)管理權限。apache

.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。tomcat

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實現的。cookie

# 成員 .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。oracle

ASP.NET的forms驗證經過FormsAuthentication發送和註銷用於認證的token,經過配置web.config可讓不一樣的Web服務器以相同的方式對token加密和解密以適應Web服務器負載均衡。不適用cookie承載token時,能夠自定義認證邏輯,好比經過url參數方式承載token配合ssl用於app客戶端驗證等。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等第三方提供的一些實現能有更高的可用性。

相關文章
相關標籤/搜索