Version 4

    原文:开发基于JBoss AS 7.2.0的Java EE程序 - 05.如何配置安全域以及使用JAAS进行身份认证和权限控制

    英文:JBoss AS 7.2.0 - Java EE application development - 05.How to configure Security domain and JAAS

     

     

    概述

    在JAAS里面,只有 Identity和Role这两种实体概念,它们之间是N:N的关系,也就是说是:一个Identity可以具有多个Roles,一个Role可以属于多个Identities。比如:

    User1 具有 Role1,Role2,Role3,Role4。

    User2 具有 Role1,Role2,Role3,Role5

    User3 具有 Role3,Role4,Role5

    User4 具有 Role3,Role4

    User5 具有 Role5

    ...

    优点:直接;缺点:Roles很多的时候,管理会变得混乱。

     

     

    因此我们在现实生活中是这样为用户分配权限的:首先创建用户组 UserGroup1, UserGroup2, UserGroup3;然后为各用户组分配操作权限 Role1,Role2,Role3;最后为为单个用户分配用户组。比如:

    UserGroup1具有权限Role1,Role2,Role3,
    UserGroup2具有权限Role3,Role4,
    UserGroup3具有权限Role5
    User1 属于 UserGroup1和UserGroup2,那么他具有 Role1,Role2,Role3,Role4
    User2 属于 UserGroup1和UserGroup3,那么他具有 Role1,Role2,Role3,Role5
    User3 属于 UserGroup2和UserGroup3,那么他具有 Role3,Role4,Role5
    User4 属于 UserGroup2,那么他具有 Role3,Role4
    User5 属于 UserGroup3,那么他仅有 Role5

    简单点:

    User1 具有 Role1,Role2,Role3,Role4。
    User2 具有 Role1,Role2,Role3,Role5
    User3 具有 Role3,Role4,Role5
    User4 具有 Role3,Role4
    User5 具有 Role5

    效果和上面的做法一样。但是,在Roles很多的情况下,这样管理会更容易一些。

     

     

    我们把JAAS信息存储到DB中,这样便于共享和查询。因此,我们需要

    (a) 创建EJB 实体Bean来描述用户,用户组和角色。

    (b) 配置standalone.xml来告诉JBoss AS 如何在DB里面读取JAAS信息。

    (c) 在代码中进行身份认证和权限控制

     

     

     

    1. Entities

     

    1.1 JAAS涉及的各Entities之间的关系

    我们用下图展示:

            

     

     

     

    1.2 EJB Entity Beans

    (a) JaasRole.java

    import java.io.Serializable;

     

    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.TableGenerator;
    import javax.persistence.Transient;

     

    @Entity
    public class JaasRole implements Serializable {
        //*********************************[static fields]*********************************//
        private static final long serialVersionUID = 1L;
        //JPA
        public static final String JPA_GEN_NAME = "GEN_JAASROLE";
        public static final String JPA_PK_COL_VAL = "NEXT_PK_JAASROLE";
       
       
        //*********************************[member fields]*********************************//
        private Integer id;
        private String name;//供Java classes/methods 使用的JAAS角色名字
        private String displayName = "";//显示给Customer的角色名字

     


        //*********************************[getter/setter]*********************************//
        @Id
        @TableGenerator(name = JPA_GEN_NAME,
            table=JPATableGeneratorHelper.TABLE_JPA_GENERATOR,
            pkColumnName=JPATableGeneratorHelper.PK_COL_NAME,
            valueColumnName=JPATableGeneratorHelper.VALUE_COL_NAME,
            allocationSize=1,
            pkColumnValue=JPA_PK_COL_VAL
        )
        @GeneratedValue(strategy = GenerationType.TABLE, generator=JPA_GEN_NAME)
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }   
       
        @Column(unique=true)
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
       
        @Column(nullable=false)
        public String getDisplayName() {
            return displayName;
        }
        public void setDisplayName(String displayName) {
            this.displayName = displayName;
        }
       
       
        //*********************************[Transient]*********************************//
        @Override
        @Transient
        public String toString(){
            //不要用"/"
            return "{显示名:"+displayName+";Jaas Role:"+name+"}";
        }
    }

     

     

    (b) UserGroup.java

     

    @Entity

    public class UserGroup implements Serializable {

        //*********************************[static fields]*********************************//

        private static final long serialVersionUID = 1L;

        //JPA

        public static final String JPA_GEN_NAME = "GEN_USERGROUP";

        public static final String JPA_PK_COL_VAL = "NEXT_PK_USERGROUP";

        //JOIN TABLE

        public static final String JOIN_TABLE_UserGroup_JaasRole = "UserGroup_JaasRole";

        public static final String JOIN_TABLE_UserGroup_JaasRole_JoinColumn = "usergroup_id";

        public static final String JOIN_TABLE_UserGroup_JaasRole_InverseJoinColumn = "jaasrole_id";

        //*********************************[member fields]*********************************//

        private Integer id;

        private String name;//用户组的名字,仅仅用于为操作GUI的用户分配 JaasRole时方便查看使用。Jaas的RoleGroup名字目前为固定的"Roles"。

        private Set<JaasRole> jaasRoles;//java.util.List被替换成java.util.Set,参见warning.txt

       

       

        //*********************************[getter/setter]*********************************//

        @Id

        @TableGenerator(name = JPA_GEN_NAME,

            table=JPATableGeneratorHelper.TABLE_JPA_GENERATOR,

            pkColumnName=JPATableGeneratorHelper.PK_COL_NAME,

            valueColumnName=JPATableGeneratorHelper.VALUE_COL_NAME,

            allocationSize=1,

            pkColumnValue=JPA_PK_COL_VAL

        )

        @GeneratedValue(strategy = GenerationType.TABLE, generator=JPA_GEN_NAME)

        public Integer getId() {

            return id;

        }

        public void setId(Integer id) {

            this.id = id;

        }   

       

        public String getName() {

            return name;

        }

        @Column(unique=true)

        public void setName(String name) {

            this.name = name;

        }

     

     

        /**

         * 多对多:一个角色可以有多个权限,一个权限可是设置给多个角色

         * 不需要反向映射:不需要知道一个权限对应了哪些角色,

         * 否则 Permission里面也要做同类似的设置

         * @return

         */

        @ManyToMany(targetEntity=JaasRole.class,fetch = FetchType.LAZY)

        @JoinTable(name               = JOIN_TABLE_UserGroup_JaasRole,

                   joinColumns        = @JoinColumn(name=JOIN_TABLE_UserGroup_JaasRole_JoinColumn),

                   inverseJoinColumns = @JoinColumn(name=JOIN_TABLE_UserGroup_JaasRole_InverseJoinColumn)

            )

        public Set<JaasRole> getJaasRoles() {

            return jaasRoles;

        }

        public void setJaasRoles(Set<JaasRole> jaasRoles) {

            this.jaasRoles = jaasRoles;

        }

       

       

        //*********************************[Transient]*********************************//

        @Override

        @Transient

        public String toString(){

            //不要用"/"

            return "{用户组名:"+name+"}";

        }

    }

     

     

     

    (c) User.java

     

    @Entity

    public class User implements Serializable{

        //*********************************[static fields]*********************************//

        private static final long serialVersionUID = 1L;

        //JPA

        public static final String JPA_GEN_NAME = "GEN_USER";

        public static final String JPA_PK_COL_VAL = "NEXT_PK_USER";

        //JOIN TABLE

        public static final String JOIN_TABLE_User_UserGroup = "User_UserGroup";

        public static final String JOIN_TABLE_User_UserGroup_JoinColumn = "user_id";

        public static final String JOIN_TABLE_User_UserGroup_InverseJoinColumn = "usergroup_id";

     

        //*********************************[member fields]*********************************//

        private Long id;

        private Date createDate = new Date();

        private String username;//登录名

        private String password;//加了Transient注解:仅仅供用户从客户端传递到服务器端以便新增或者修改时使用。

        private String hashedPassword;

        private String firstname = "";//真实姓

        private String lastname = "";//真实名       

        private String mobilePhone = "";//手机

        private String email ="";//电子邮件。

        private Date birthday = null;//生日

        private Gender gender = Gender.UNKNOWN;//性别

        private Integer maxConcurrentSessions = 1;//EJB客户端的最大并发会话数量。

        @Deprecated

        private String imageLink = "unknown.jpg";//作废!portraitDir取代!头像<img src="#{request.contextPath}/file/people/#{user.portraitDir}/48.png" />

        private Integer score = 100;//论坛积分。

        private Boolean activated = false;

        private Boolean locked = false;//用户是否因为谩骂、发广告等不良行为被锁定

        private Date lockReleaseDate = new Date();//解锁日期

        private Date lastChangePasswordDate = new Date();

        private Long portraitDir = 0L;//肖像存储的子目录地址,如果为0,表明用户没用设置过肖像,那么采用默认的肖像目录:0。如果用户设置了肖像,那么更新这个数字为ID值!

        private Set<UserGroup> userGroups;//java.util.List被替换成java.util.Set,参见warning.txt

       

       

        //*********************************[getter/setter]*********************************//

        @Id

        @TableGenerator(name = JPA_GEN_NAME,

            table=JPATableGeneratorHelper.TABLE_JPA_GENERATOR,

            pkColumnName=JPATableGeneratorHelper.PK_COL_NAME,

            valueColumnName=JPATableGeneratorHelper.VALUE_COL_NAME,

            allocationSize=1,

            pkColumnValue=JPA_PK_COL_VAL

        )

        @GeneratedValue(strategy = GenerationType.TABLE, generator=JPA_GEN_NAME)

        public Long getId() {

            return id;

        }

        public void setId(Long id) {

            this.id = id;

        }

     

        @Temporal(TemporalType.TIMESTAMP)

        @Column(nullable=false,updatable=false)

        public Date getCreateDate() {

            return createDate;

        }

        public void setCreateDate(Date createDate) {

            this.createDate = createDate;

        }

       

        @Column(unique=true,nullable=false,updatable=false,length=USERNAME_MAX_LENGTH)

        public String getUsername() {

            return username;

        }

        public void setUsername(String username) {

            this.username = username;

        }

     

        /**

         * 16 bytes = 128 bit

         * 64 bytes = 512 bit

         * 128 bytes = 1024 bit

         */   

        @Column(nullable=false,length=256)
        public String getHashedPassword() {
            return hashedPassword;
        }
        public void setHashedPassword(String hashedPassword) {
            this.hashedPassword = hashedPassword;
        }

     

        //其它Getter/Setter


        /**

         * 多对多:一个User可以有多个UserGroup,一个UserGroup可是设置给多个User

         * 不需要反向映射:不需要知道一个UserGroup对应了哪些User,

         */   

        @ManyToMany(targetEntity=UserGroup.class,fetch = FetchType.LAZY)

        @JoinTable(name               = JOIN_TABLE_User_UserGroup,

                   joinColumns        = @JoinColumn(name=JOIN_TABLE_User_UserGroup_JoinColumn),

                   inverseJoinColumns = @JoinColumn(name=JOIN_TABLE_User_UserGroup_InverseJoinColumn)

            )

        public Set<UserGroup> getUserGroups() {

            return userGroups;

        }

        public void setUserGroups(Set<UserGroup> userGroups) {

            this.userGroups = userGroups;

        }

       

       

        //*********************************[Transient]*********************************//

        @Transient

        public String getPassword() {

            return password;

        }

       

        public void setPassword(String password) {

            this.password = password;

        }

       

    }

     

     

     

    1.3 DB中各数据表

     

            

     

            

     

            

     

            

     

            

     

     

     

    2. standalone.xml中配置security-domain

    <management>
        <security-realms>
            <security-realm name="ManagementRealm">
                ...
            </security-realm>
            <security-realm name="ApplicationRealm">
                <server-identities>
                    <ssl>
                        <keystore path="server.keystore" relative-to="jboss.server.config.dir" keystore-password="ybxiang_keystore_password"/>
                    </ssl>

                </server-identities>
                <authentication>
                    <jaas name="ybxiang-forum-jaas-security-domain"/>
                </authentication>
            </security-realm>
        </security-realms>
        ...
    </management>

    ...

    <subsystem xmlns="urn:jboss:domain:security:1.2">

        <security-domains>

            <security-domain name="ybxiang-forum-jaas-security-domain" cache-type="default">

                <authentication>

                    <login-module code="Remoting" flag="optional">

                        <module-option name="password-stacking" value="useFirstPass"/>

                    </login-module>

                    <login-module code="org.jboss.security.auth.spi.DatabaseServerLoginModule" flag="required">

                        <module-option name="password-stacking" value="useFirstPass"/>

                        <module-option name="dsJndiName" value="java:jboss/datasources/ybxiangForumMySqlDataSource"/>

                        <module-option name="principalsQuery" value="SELECT hashedPassword FROM User WHERE username=? and activated=true and locked=false"/>

                        <module-option name="rolesQuery" value="SELECT DISTINCT r.name, 'Roles' FROM User u, User_UserGroup ug, UserGroup_JaasRole gr, JaasRole r WHERE u.id=ug.user_id AND ug.usergroup_id=gr.usergroup_id AND gr.jaasrole_id=r.id AND u.username=?"/>

                        <module-option name="hashAlgorithm" value="SHA-256"/>

                        <module-option name="hashEncoding" value="Base64"/>

                        <module-option name="hashCharset" value="UTF-8"/>

                        <module-option name="unauthenticatedIdentity" value="guest"/>

                    </login-module>

                </authentication>

            </security-domain>

            <security-domain name="other" cache-type="default">

                ...

            </security-domain>

            <security-domain name="jboss-web-policy" cache-type="default">

                ...

            </security-domain>

            <security-domain name="jboss-ejb-policy" cache-type="default">

                ...

            </security-domain>

        </security-domains>

    </subsystem>

     

     

    3. JavaEE 各子项目中使用security domain

    我们需要配置下面的3个文件,让我们的Java EE各子项目使用我们在上面配置好的security domain:

    • jboss-as-7.2.0.Final\standalone\deployments\ybxiang-forum.ear\META-INF\jboss-app.xml
    • jboss-as-7.2.0.Final\standalone\deployments\ybxiang-forum.ear\ybxiang-forum-ejb.jar\META-INF\jboss-ejb3.xml
    • jboss-as-7.2.0.Final\standalone\deployments\ybxiang-forum.ear\ybxiang-forum-war.war\WEB-INF\jboss-web.xml

     

    3.1 jboss-app.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <p:jboss-app xmlns:p="http://www.jboss.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee ../../xsd/jboss-app_7_0.xsd ">
        <security-domain>ybxiang-forum-jaas-security-domain</security-domain>
    </p:jboss-app>

     

    3.2 jboss-ejb3.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <jboss:ejb-jar xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:security="urn:security"
        xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-2_0.xsd http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd"
        version="3.1"
        impl-version="2.0">
       
        <assembly-descriptor xmlns="http://java.sun.com/xml/ns/javaee">
            <security:security xmlns:security="urn:security">
                <security:security-domain>ybxiang-forum-jaas-security-domain</security:security-domain>
                <ejb-name>*</ejb-name>

            </security:security>
        </assembly-descriptor>
    </jboss:ejb-jar>

     

    3.3 jboss-web.xml

    <?xml version="1.0" encoding="UTF-8"?>

    <jboss-web>

        <security-domain>java:/jaas/ybxiang-forum-jaas-security-domain</security-domain>

        <!-- encoding for login servlet 'j_security_check' -->

        <valve>

            <class-name>org.apache.catalina.authenticator.FormAuthenticator</class-name>

            <param>

                <param-name>characterEncoding</param-name>

                <param-value>UTF-8</param-value>

            </param>

        </valve>

    </jboss-web>