• Java Web系列:JAAS认证和授权基础


    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等第三方提供的一些实现能有更高的可用性。

  • 相关阅读:
    WPF Get jiayuan outbox list(send mail box)
    Python中列表的各种方法
    Python中字符串拼接的三种方式
    Python2中input()、raw_input()和Python3中input()
    Python 中for...esle和while...else语法
    第 20 章 设置应用程序的样式并对其进行部署
    第 19 章 用户帐号
    第 18 章 Django 入门
    第 17 章 使用API
    安装requests
  • 原文地址:https://www.cnblogs.com/easygame/p/5063911.html
Copyright © 2020-2023  润新知