###### tags: `JSF_教學` `106級專題`
# 訓練課程 17 (t17) - 使用容器進行身份鑑別與授權檢查
## Lab 檔案下載
[Download from https://github.com/hychen39/container_auth_lab.git](https://github.com/hychen39/container_auth_lab.git)
## 設定概覽

設定步驟:
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

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

### 建立 Data Source
建立 Data Source 供應用程式使用。
路徑 Resource > JDBC > JDBC Resources
為新的 Data Source 設定一個 JNDI name, 以便應用程式向 Container 以此名稱取得 Data Source。

Pool Name 選擇方才建立的 Connection Pool:

### 建立 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

#### 設定 Realm 特性 1/3
- Name = app_user_1_realm
- Class Name = `com.sun.enterprise.security.ee.auth.realm.jdbc.JDBCRealm`

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 去取得資料庫連線。

#### 設定 Realm 特性 3/3
接續設定 Password Encryption 及 Digest Algorithm。如果資料庫中的密碼使用 plain text,則 Password Encryption及 Digest Algorithm 皆設為 None。

## 對應 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

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

### 設定 Security Role 與 Principle 或者 Group 的對應關係
開啟 `glassfish-web.xml`,

## 設定 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 頁面。

#### 資源未授權
點選 (T)Pages > Error Pages
輸入:
- Error Code: 403
- Error Page Location: /faces/auth/notAllowed.xhtml

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

## 設定 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:

Admin Area

## 測試登入
## 製作登出連結並在導覽列顯示目前的用戶名稱
### 取得使用者名稱
請容器注入 `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>
```
```