---
# System prepended metadata

title: 訓練課程 17 (t17) -  使用容器進行身份鑑別與授權檢查
tags: [106級專題, JSF_教學]

---

###### tags: `JSF_教學` `106級專題`
# 訓練課程 17 (t17) -  使用容器進行身份鑑別與授權檢查

## Lab 檔案下載

[Download from https://github.com/hychen39/container_auth_lab.git](https://github.com/hychen39/container_auth_lab.git)

## 設定概覽

![](https://i.imgur.com/9BDeHbx.png)

設定步驟:
1. 建立 user 及 user_group tables
2. 建 Container 中建立 JDBC Realm (安全領域)
3. 設定 App 使用所建立的 JDBC Realm; 對應 App 中的 Security Role 到 JDBC Realm 中的 Principal
4. 設定 App 中的身份鑑別相關關表單
5. 設定 App 中不同安全角色下要保護的資源 url


## 建立 user 及 user_group tables

需在 Payara 5 在安裝 Oracle JDBC Driver. 參考 [訓練課程 12 (t12) - Payara Server 連線 Oracle database](/vAIjE4CQRR6rOMnrZLZeGw)。

建立使用者 `USER` 及其所屬群組 `USER_GROUP`。
User `john` 的群組為 `mis`, User `mary` 的群組為 `admin`。

```sql=
CREATE TABLE USER_TEMP (
  USERID VARCHAR(255) NOT NULL,
  PASSWORD VARCHAR(255) NOT NULL,
  PRIMARY KEY (USERID)
);

CREATE TABLE USER_GROUP (
  GROUPID VARCHAR(20) NOT NULL,
  USERID VARCHAR(255) NOT NULL,
	  PRIMARY KEY (GROUPID, USERID)
	);
	
INSERT INTO USER_TEMP VALUES('john', '1234');
INSERT INTO USER_TEMP VALUES('mary', '1234');
INSERT INTO USER_GROUP VALUES('mis', 'john');
INSERT INTO USER_GROUP VALUES('admin', 'mary');
	
COMMIT;
```

## 建 Container 中建立 JDBC Realm (安全領域)


安全領域的設定不能隨應用程式部署一併設定, 需要手動設定。

起動 Payara Server, 開啟 Domain Admin Console 頁面。

### 建立 Connection Pool

建立一個 Connection Pool, 路徑: Resources > JDBC > JDBC Connection Pools。

建立一個 app_user 的 Connect Pools, 設定屬性如下:

Step 1/2: General Settings
![](https://i.imgur.com/CLktOyy.png)

Step 2/2: Additional properties

輸入以下額外特性:
- username: app106test
- password: ******
- URL: jdbc:oracle:thin:@host:1521/pdb1

![](https://i.imgur.com/0r3B4nK.png)

### 建立 Data Source

建立 Data Source 供應用程式使用。

路徑 Resource > JDBC > JDBC Resources

為新的 Data Source 設定一個 JNDI name, 以便應用程式向 Container 以此名稱取得 Data Source。

![](https://i.imgur.com/xeYgIEG.png)

Pool Name 選擇方才建立的 Connection Pool:
![](https://i.imgur.com/JO4C2mp.png)

### 建立 JDBC Realm (JDBC 身份鑑別領域)

What is the Realm:
- A Realm is the place that stores the Principals (合法的使用者名稱).
- The principal is the authenticated user in the container.


路徑: Configurations > server-config > Security > Realms > (B)New

![](https://i.imgur.com/IHu5omS.png)

#### 設定 Realm 特性 1/3
- Name = app_user_1_realm
- Class Name = `com.sun.enterprise.security.ee.auth.realm.jdbc.JDBCRealm`
![](https://i.imgur.com/GoOWg01.png)

JAAS Context 是設定使用的 login module。Login module 定義在 `[Glassfish_Domain]/config/login.conf 中`。JAAS Context 的欄位大小寫有差別，且字串要和 `[Glassfish_Domain]/config/login.conf ` 中設定字串要相同。


####  設定 Realm 特性 2/3

接續設定使用 JdbcRealm 時使用者帳號、密碼及群組等資料庫表格資訊。告訴 Glassfish 儲放使用者帳號及密碼的表格、帳號欄位、密碼欄位名稱。另外，告訴 Glassfish 使用者群組及群組欄位名稱。User table 及 Group table 填入先前建立的資料庫表格名稱.

Glassfish 會使用 JNDI 欄位中的 JDBC DataSource 去取得資料庫連線。

![](https://i.imgur.com/GgIc1XW.png)






#### 設定 Realm 特性 3/3

接續設定 Password Encryption 及 Digest Algorithm。如果資料庫中的密碼使用 plain text，則 Password Encryption及 Digest Algorithm 皆設為 None。

![](https://i.imgur.com/Ph2XdBV.png)



## 對應 App 中的 Security Role 到 JDBC Realm 中的 Principal

完成 Payara 上的 JDBCRealm 上的設定後，

在 web.xml 中
- 設定要使用的 JDBC Realm 
- 建立 Security Role

之後, 設定的 Security Role 和 Container 内的 principal 或者 User Group 對應。這個對應關係放在 `glassfish-web.xml`。


### 設定要使用的 JDBC Realm

開啟 `web.xml`, 設定 Realm Name

![](https://i.imgur.com/3xyH0WC.png)

### 加入 Security Roles 到 `web.xml`

![](https://i.imgur.com/ZOEmMBX.png)

### 設定 Security Role 與 Principle 或者 Group 的對應關係

開啟 `glassfish-web.xml`, 

![](https://i.imgur.com/ZWLzef7.png)


## 設定 App 中的身份鑑別相關關表單

需要建立以下的頁面:
- Login: 登入頁面
- authentication failed (Status code 401): 身份鑑別失敗頁面
- not authorized (Status code 403): 未授權頁面


### 建立 Login 頁面

建立 Web folder `/auth`, 在內建立  `authmethodform.xhtml` JSF page.

```xml=
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
    <h:head>
        <title> JSF Login form </title>
    </h:head>
    <h:body>
        <ui:composition template="/templates/masterLayout.xhtml">
            <ui:define name="contentArea">
                Form based authentication page
                <h:form>
                    <div class="form-group">
                        <h:outputLabel for="inputName"> User Name </h:outputLabel>
                        <h:inputText id="inputName" class="form-control" value="#{securityBean.username}"/>
                    </div>
                    <div class="form-group">
                        <h:outputLabel for="inputPasswd"> Password </h:outputLabel>   
                        <h:inputSecret id="inputPasswd" class="form-control" value="#{securityBean.password}"/>
                        <h:commandButton class="btn btn-primary" value="Login" action="#{securityBean.login()}"/>
                    </div>
                </h:form>
                This page only works if you were redirected here by the server.
            </ui:define>
        </ui:composition>
    </h:body>
</html>

```

建立需要的 CDI Bean `SecurityBean`, 放在 `auth` 套件下。

We inject `HttpServletRequest` instance to the bean. 

The `httpServletRequest.login()` method uses web container login mechanism to validate the user against the security realm configured in the `web.xml`.


```java=
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package auth;

import java.security.Principal;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Named;
import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

/**
 *
 * @author user
 */
@Named(value = "securityBean")
@RequestScoped
public class SecurityBean {

    @Inject
    private Principal principal;
    @Inject
    private HttpServletRequest request;
    
    private String username;
    private String password;

    /**
     * Creates a new instance of SecurityBean
     */
    public SecurityBean() {
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    
    public String getPrincipalName() {
        return principal.getName();
    }

    public boolean isLoginStatus() {
        String remoteUser = request.getRemoteUser();
        return (remoteUser != null);
    }

    public String logout() {
        try {
            request.logout();
        } catch (ServletException ex) {
            Logger.getLogger(SecurityBean.class.getName()).log(Level.SEVERE, null, ex);
        }
        return "/auth/authmethodform?faces-redirect=true";
    }

    public boolean isAdmin() {
        //ExternalContext context = FacesContext.getCurrentInstance().getExternalContext();
        //return context.isUserInRole("admin");
        return request.isUserInRole("admin")
    }
    
    public String login(){
        try {
            request.login(username, password);
        } catch (ServletException ex) {
            Logger.getLogger(SecurityBean.class.getName()).log(Level.SEVERE, null, ex);
            FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Login failed."));
            return "notAllowed";
        }
        return "/index?faces-redirect=true";
    }
}

```

### 建立身份鑑別失敗頁面

在 Web `/auth` 目錄新增 `badLogin.xhtml`:

```xml=
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html">
    <h:head>
        <title>Bad Login</title>
    </h:head>
    <h:body>
        Incorrect user name or password (Status 401).
    </h:body>
</html>
```

### 建立授權失敗頁面

在 Web `/auth` 目錄新增 `notAllowed.xhtml`:

```xml=
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
    <h:head>
        <title>Not Allowed</title>
    </h:head>
    <h:body>
        <ui:composition template="/templates/masterLayout.xhtml">
            <ui:define name="contentArea">
            You entered a good user name and password but are not allowed to view the requested resource.
            (Status 403)
            <h:link class="btn btn-primary" outcome="/index">Home </h:link>
            </ui:define>
        </ui:composition>
    </h:body>
</html>
```

### 在 `web.xml` 中設定登入及失敗時的頁面

#### 登入及登入失敗

- Form Login Page: `/faces/auth/authmethodform.xhtml`
- Form Error Page: `/faces/auth/badLogin.xhtml`

注意, url 中要加 `/faces`, 如此請求才能送給 JSF Servlet 處理, 方能正確 render JSF 頁面。

![](https://i.imgur.com/mQMpnsJ.png)

#### 資源未授權

點選 (T)Pages > Error Pages

輸入:
- Error Code: 403
- Error Page Location: /faces/auth/notAllowed.xhtml

![](https://i.imgur.com/LYYtday.png)

Error Page Location 欄位無法在 GUI 下輸入, 點選 (T) Source 直接修改原始碼:

![](https://i.imgur.com/tSTIbNU.png)




## 設定 App 中不同安全角色下要保護的資源 url

在 `web.xml` 中建立 Security Constraints, 設定某個資源的 url pattern 下, 能夠存取的 Security Roles。

資源限制規則如下:

資源 url | Security Role
---|---
`/faces/member/* `| mis, admin
`/faces/admin/*` | admin


路徑 (T)Security > Security Constraints 加入上述的資源限制:

Member Area:

![](https://i.imgur.com/1NFIwBv.png)


Admin Area
![](https://i.imgur.com/9A4WxQv.png)


## 測試登入

## 製作登出連結並在導覽列顯示目前的用戶名稱

### 取得使用者名稱

請容器注入 `Principal` 實體到 CDI bean 的成員變數中, `Principal` 實體代表目前登入的使用者。

方法 `principal.getName()` 可取的登入的名稱。

方法 [`httpServletRequest.getRemoteUser():String`](https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html#getRemoteUser--) 也可傳回登入的使用者名稱。

### 使用程式執行登入及登出

請容器注入 `HttpServletRequest` 實體, 用來執行登入及登出。

方法 [`httpServletRequest.logout():void`](https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html#logout--) 執行等出

方法 [`httpServletRequest.login(String username, String password) throws ServletException:void`](https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html#login-java.lang.String-java.lang.String-) 執行登入, 使用容器的身份鑑別機制。

### 判斷使用者是否具備某個 Security Role

方法 [`httpServletRequest.isUserInRole(String role):String`](https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html#isUserInRole-java.lang.String-)


### SecurityBean 中的 action methods

SecurityBean 有以下 action methods 供 JSF Page 使用:

```java=

 public boolean isLoginStatus() {
        String remoteUser = request.getRemoteUser();
        return (remoteUser != null);
    }

    public String logout() {
        try {
            request.logout();
        } catch (ServletException ex) {
            Logger.getLogger(SecurityBean.class.getName()).log(Level.SEVERE, null, ex);
        }
        return "/auth/authmethodform?faces-redirect=true";
    }

    public boolean isAdmin() {
        //ExternalContext context = FacesContext.getCurrentInstance().getExternalContext();
        //return context.isUserInRole("admin");
        return request.isUserInRole("admin")
    }
    
    public String login(){
        try {
            request.login(username, password);
        } catch (ServletException ex) {
            Logger.getLogger(SecurityBean.class.getName()).log(Level.SEVERE, null, ex);
            FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Login failed."));
            return "notAllowed";
        }
        return "/index?faces-redirect=true";
    }
```

### 使得頁面的某些部份在特定角色才顯示

開啟 Web `/templates/navbar.xhtml` 全域導覽頁面, 使用 `<ui:fragment>` 使得 `Customer List` 只有在 `admin` 角色時才顯示:

```xml=
<ui:fragment rendered="#{securityBean.admin}">
    <li id="customerItem" class="active">
        <h:link outcome="/admin/customerList">Customer List</h:link> 
    </li>
</ui:fragment>
```

開啟 Web `/template/sideBarLeft.xhtml`, 使用 `<ui:fragment>` 使得 `Customer List` 只有在 `admin` 角色時才顯示:

```xml=
<ul class="nav nav-stacked nav-pills">
<!-- Render the fragment only for the admin role. -->
    <ui:fragment rendered="#{securityBean.admin}" >
        <li><h:link class="btn btn-primary" 
                    outcome="/admin/customerList"> 
                Customer List (Admin Only)
            </h:link> </li>
    </ui:fragment>
    <li><h:link class="btn btn-primary" 
                value="Show Emails (Member)" 
                outcome="/member/emailList" /></li>
</ul>
```

### 顯示使用者名稱及提供登出按鈕

開啟 Web `/templates/navbar.xhtml` 全域導覽頁面

```xml=
<ul class="nav navbar-nav navbar-right">
<!-- Show the logged username -->
    <li><a> <h:outputText value="#{securityBean.principalName}"  /> </a> </li>
    <!-- Show the logout action link -->
    <ui:fragment rendered="#{securityBean.loginStatus}" >
        <li id="logoutAction"> 
            <h:commandLink action="#{securityBean.logout}" >
                <span class="glyphicon glyphicon-log-out"></span> Logout 
            </h:commandLink>
        </li>
    </ui:fragment>
</ul>
```
```
