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>
用於JAAS的LoginModule的配置:/main/java/resources/jaas.config
MyLogin{
MyLoginModule Required debug=true;
};
用於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>
用於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; } }
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; } }
用於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 自動生成的方法存根 } }
用於登入的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>
MyLoginModule實現:其中的三個name屬性必須是固定值:j_security_check、j_username和j_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; } }
在.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等第三方提供的一些實現能有更高的可用性。