###### 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> ``` ```