Thứ Năm, 2 tháng 5, 2013

Java : Ví dụ JSP chứng thực (authentication)

Trong thực hành khi viết một ứng dụng web bằng JSP ta thường sử dụng servlet cho phần xử lý và trang JSP cho phần trình bày (giao diện). Ví dụ dưới đây chỉ mang tính chất giới thiệu JSP, vì tất cả phần trình bày và xử lý đều được viết bằng JSP tuy có phân biệt trang JSP cho trình bày và trang JSP cho xử lý, vì suy cho cùng JSP cũng là servlet và mọi việc ta có thể làm với servlet thì ta đều có thể làm với JSP. Như đã nói, cách viết phần xử lý dùng JSP không nên áp dụng trong thực tiễn khi viết ứng dụng web.

Dưới đây ta sẽ xây dựng một ứng dụng web cho phép chứng thực người dùng. Ứng dụng này chỉ đơn giản cho phép người dùng đăng nhập và đăng xuất. Ngoài ra nó còn có thêm phần quản lý truy nhập tuỳ theo vai trò người dùng.

Ta xây dựng một thư mục dự án như sau :


Người dùng đầu tiên sẽ truy cập trang logon để đăng nhập. Tập tin logonProcess sẽ xử lý nếu đăng nhập thành công sẽ chuyển tới trang welcome, ngược lại sẽ hiển thị thông điệp lỗi tại trang đăng nhập. Trang welcome cho phép người dùng đăng xuất và chuyển tới trang logoff. Tập tin logoffProcess xử lý phần đăng xuất.

Nội dung logon.jsp
<%@ page import="java.util.*" language="java" %>

<%!ResourceBundle rb;

 public void jspInit() {
  rb = ResourceBundle.getBundle("messages");
 }%>
<html>
<head>
<title><%=rb.getString("logon.title")%></title>
</head>

<body bgcolor="white">
<%
  String path = request.getContextPath() + "/logonProcess";
%>
 <form name="logonForm" method="post" action="<%=path %>">
  <table>
   <tr>
    <th class="right">User name</th>
    <td class="left"><input type="text" name="username" size="30" /></td>
   </tr>
   <tr>
    <th class="right">Password</th>
    <td class="left"><input type="password" name="password"
     size="30" /></td>
   </tr>
   <tr>
    <th class="right">
     <input type="submit" name="logon"
      value=<%=rb.getString("button.submit")%> />
    </th>
    <td class="right"><input type="reset" name="reset"
     value=<%=rb.getString("button.reset")%> /></td>
   </tr>
  </table>
  <p style="color: red"><jsp:expression>session.getAttribute("msgErreur") == null ? "" : session
     .getAttribute("msgErreur")</jsp:expression></p>
  <script type="text/javascript">
  <!--
   var focusControl = document.forms["logonForm"].elements["username"];

   if (focusControl != null && focusControl.type != "hidden"
     && !focusControl.disabled
     && focusControl.style.display != "none") {
    focusControl.focus();
   }
  // -->
  </script>
 </form>
</body>
</html>
Giải thích:
- Ta ghi đè hàm jspInit() để khởi tạo tài nguyên ResourceBundle rb : dùng để lấy các nhãn lưu trong tập tin messages.properties
- Người dùng nhập tài khoản và mật khẩu lần lượt vào hai chỗ nhập "User name" và "Password"
- Nhấn nút Submit để gởi truy vấn Post hai giá trị username password đến trang /logonProcess nhằm xử lý.
- Nếu muốn xoá các thông tin để nhập lại thì nhấn nút Reset.
- Nếu đăng nhập sai thì thông tin lỗi lấy từ thuộc tính msgErreur trong phiên giao dịch này và hiển thị nó ở cuối trang.
- Phần mã nằm trong thẻ script là phần văn lệnh javascript cho phép đặt con trỏ vào ô username khi trang này hiển thị.

Nội dung tập tin messages.properties chứa thông tin các nhãn, các thông báo và đường dẫn ...
logon.title=Log on
prompt.username=User name
prompt.password=Password
button.submit=Submit
button.reset=Reset
logon.failed=User name or password is incorrect

welcome.title=Welcome
welcome.heading=Welcome
welcome.message=Log in success

logoff.title=Log off
logoff.heading=Log off
logoff.message=Log off success

process.logon.success=/welcome
process.logon.failure=/logon
process.logoff=/logoff


Nội dung trang xử lý đăng nhập logonProcess.jsp
<%@ page import="java.util.*, com.openspace.*" language="java"%>

<%!ResourceBundle rb;

 public void jspInit() {
  rb = ResourceBundle.getBundle("messages");
 }%>
<jsp:useBean id="userBean" class="com.openspace.UserService"
 scope="session" />
<jsp:setProperty property="*" name="userBean" />

<%
 User user = userBean.authenticate();
 if (user != null) {
  session.setAttribute("user", user);
  String sucessPath = rb.getString("process.logon.success");
  session.setAttribute("msgErreur", "");
%>
<jsp:forward page="<%=sucessPath%>" />
<%
 } else {
  String failurePath = rb.getString("process.logon.failure");
  String msgErreur = rb.getString("logon.failed");
  session.setAttribute("msgErreur", msgErreur);
%>
<jsp:forward page="<%=failurePath%>" />
<%
 }
%>
Giải thích:
- Khởi tạo hàm jspInit() như đã nói ở trên, rồi sau đó thẻ jsp:useBean khai báo một đối tượng java bean có tên userBean thuộc lớp com.openspace.UserService và đối tượng này có phạm vi sử dụng trong toàn phiên kết nối (session).
- Sau đó gán các giá trị được gởi đến cho đối tượng này qua việc dùng thẻ jsp:setProperty. Ở đây hai giá trị username password sẽ được gán cho hai thành phần (member) cùng tên của đối tượng này.
- Một khi đối tượng này được khởi tạo, ta dùng nó để thực hiện việc chứng thực người dùng bằng cách gọi hàm authenticate(). Nếu thành công, tức thông tin đăng nhập chính xác và người dùng được tìm thấy, thì xoá thông báo lỗi msgErreur và chuyển tới trang /welcome. Đồng thời gán thông tin người dùng vào thuộc tính "user" của phiên kết nối. Thông tin này được dùng trong filter để lọc phần truy xuất tài nguyên nhằm ngăn việc truy xuất chúng khi chưa đăng nhập. Ngược lại sẽ chuyển trở về trang /logon cùng thông báo lỗi.

Nội dung lớp User chứa thông tin về người dùng cùng với vai trò (role) của người này
package com.openspace;

public class User {
 private String username;
 private String[] roles;

 public User(String username, String[] roles) {
  this.username = username;
  this.roles = roles;
 }

 public String getUsername() {
  return username;
 }

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

 public String[] getRoles() {
  return roles;
 }

 public void setRoles(String[] roles) {
  this.roles = roles;
 }

 public boolean hasRole(String role) {
  if (roles.length > 0) {
   for (String r : roles) {
    if (r.equals(role))
     return true;
   }
  }
  return false;
 }
}

Nội dung lớp UserService chứa thông tin về người dùng ta dùng trong form (mẫu) trong trang jsp. Ở đây thông tin người dùng tôi viết hẳn trong mã vì lý do đơn giản để minh hoạ. Trong thực hành, đúng ra ta phải có hai lớp : một lớp truy xuất dữ liệu (DAO) và một lớp đại diện người dùng trong form. Ta có một người dùng username "toan", mật khẩu "jsp", có vai trò (role) "user".
package com.openspace;

import java.util.HashMap;
import java.util.Map;

public class UserService {
 private static final String USER_ROLE = "user";
 private Map users;
 private Map userPassword;
 private String username;
 private String password;

 public UserService() {
  users = new HashMap<>();
  userPassword = new HashMap<>();
  users.put("toan", new User("toan", new String[] { USER_ROLE }));
  userPassword.put("toan", "jsp");
 }

 public User authenticate() {
  String pw = userPassword.get(username);
  if (pw == null || !pw.equals(password))
   return null;

  User user = users.get(username);
  return user;
 }

 public Map getUserPassword() {
  return userPassword;
 }

 public void setUserPassword(Map userPassword) {
  this.userPassword = userPassword;
 }

 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;
 }

}

Tập tin miêu tả ứng dụng web web.xml có nội dung sau :
<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
 <display-name>JSP Example</display-name>

 <!-- Action Servlet Configuration -->
  <servlet>
  <servlet-name>logonJSP</servlet-name>
  <jsp-file>/pages/logon.jsp</jsp-file>
 </servlet>
  <servlet>
  <servlet-name>logonProcessJSP</servlet-name>
  <jsp-file>/pages/logonProcess.jsp</jsp-file>
 </servlet>
  <servlet>
  <servlet-name>logoffJSP</servlet-name>
  <jsp-file>/pages/logoff.jsp</jsp-file>
 </servlet>
  <servlet>
  <servlet-name>logoffProcessJSP</servlet-name>
  <jsp-file>/pages/logoffProcess.jsp</jsp-file>
 </servlet>
  <servlet>
  <servlet-name>welcomeJSP</servlet-name>
  <jsp-file>/pages/welcome.jsp</jsp-file>
 </servlet>


 <!-- Action Servlet Mapping -->
 <servlet-mapping>
  <servlet-name>logonJSP</servlet-name>
  <url-pattern>/logon</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
  <servlet-name>logonProcessJSP</servlet-name>
  <url-pattern>/logonProcess</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
  <servlet-name>logoffJSP</servlet-name>
  <url-pattern>/logoff</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
  <servlet-name>logoffProcessJSP</servlet-name>
  <url-pattern>/logoffProcess</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
  <servlet-name>welcomeJSP</servlet-name>
  <url-pattern>/welcome</url-pattern>
 </servlet-mapping>


 <!-- The Welcome File List -->
 <welcome-file-list>
  <welcome-file>logon</welcome-file>
 </welcome-file-list>

  <!-- <jsp-config>
  <jsp-property-group>
   <url-pattern>*.jsp</url-pattern>
  </jsp-property-group>
 </jsp-config> -->

 <filter>
  <filter-name>userAccessFilter</filter-name>
  <filter-class>com.openspace.AuthorizationFilter</filter-class>
  <init-param>
   <param-name>roles</param-name>
   <param-value>user</param-value>
  </init-param>
 </filter>
 
  <filter-mapping>
  <filter-name>userAccessFilter</filter-name>
  <url-pattern>/welcome</url-pattern>
  <url-pattern>/logoffProcess</url-pattern>
 </filter-mapping>

  <security-constraint>
  <web-resource-collection>
   <web-resource-name>Restricted folders</web-resource-name>
   <url-pattern>/pages/*</url-pattern>
  </web-resource-collection>
  <auth-constraint/>
 </security-constraint>
</web-app>
Giải thích:
- Ánh xạ các trang jsp vào các đường dẫn tương ứng thông qua các thẻ servlet servlet-mapping. Chú ý các thẻ servlet luôn luôn được định nghĩa trước các thẻ servlet-mapping tương ứng.
- Thẻ welcome-file-list định nghĩa các đường dẫn mặc định khi người dùng không xác định đường dẫn cụ thể. Chẳng hạn khi người dùng chỉ nhập "http://localhost:8080/JSP/" trong trình duyệt web thì máy chủ web sẽ tự động dẫn người dùng đến địa chỉ "http://localhost:8080/JSP/logon". Trong thẻ này ta có thể có nhiều thẻ welcome-file, khi đó máy chủ web sẽ lần lượt nếu không tìm thấy đường dẫn đầu tiên, sẽ tìm kiếm đường dẫn tiếp theo và cứ thế cho đến khi tìm ra một đường dẫn trong danh sách thực sự hiện hữu ở máy chủ.
- Thẻ jsp-property-group trong thẻ jsp-config, mà ở đây ta để trong chú thích, cho phép ta cấu hình một nhóm các tập tin jsp đặc thù nào đó nhằm thêm chẳng hạn phần đầu (dùng thẻ include-prelude) hoặc phần đuôi (dùng thẻ include-coda) vào mỗi trang jsp trong nhóm này. Đặc điểm đường dẫn của các trang trong nhóm này được định nghĩa bằng thẻ url-pattern.
- Thẻ filter cho phép định nghĩa một bộ lọc, trong đó : filter-name định nghĩa tên bộ lọc, filter-class định nghĩa lớp bộ lọc sử dụng, lớp này phải hiện dựng (implement) mặt giao javax.servlet.Filter. Ta có thể chuyển các tham số khi khởi tạo bộ lọc này bằng cách dùng thẻ init-param, trong đó tên của tham số định nghĩa bằng thẻ param-name và giá trị của tham số này bằng thẻ param-value.
- Thẻ filter-mapping dùng để ánh xạ việc dùng bộ lọc này cho các đường dẫn được định nghĩa bằng thẻ url-pattern. Ở đây bộ lọc được áp dụng cho hai trang /welcome/logoffProcess. Tương tự như các thẻ servlet servlet-mapping, thẻ filter phải luôn được định nghĩa trước thẻ filter-mapping tương ứng.
- Thẻ security-constraint dùng để giới hạn sự truy xuất của người dùng đã đăng nhập (khi dùng  Security Realms) vào những khu vực tài nguyên khác nhau. Ở đây tuy không dùng Realms, nhưng với cấu hình như thế, ta sẽ chặn mọi sự truy xuất trực tiếp của mọi người dùng vào mọi tập tin trong thư mục "/pages"

Nội dung lớp bộ lọc AuthorizationFilter
package com.openspace;

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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class AuthorizationFilter implements Filter {

 private String[] roleNames;

 @Override
 public void destroy() {
 }

 @Override
 public void doFilter(ServletRequest request, ServletResponse response,
   FilterChain chain) throws IOException, ServletException {
  HttpServletRequest req = (HttpServletRequest) request;
  HttpServletResponse res = (HttpServletResponse) response;
  HttpSession session = req.getSession();
  User user = (User) session.getAttribute("user");
  boolean hasRole = false;
  if (user != null) {
   for (String role : roleNames) {
    if (user.hasRole(role)) {
     hasRole = true;
    }
   }
  }
  if (hasRole) {
   chain.doFilter(request, response);
  } else {
   // if don't have the user role, redirect to /logon
   req.getRequestDispatcher("/logon").forward(req, res);
  }
 }

 @Override
 public void init(FilterConfig filterConfig) throws ServletException {
  String roles = filterConfig.getInitParameter("roles");
  if (roles == null || "".equals(roles)) {
   roleNames = new String[0];
  } else {
   roles.trim();
   // split the comma-separated string into an array and trim around
   // spaces of each splited element
   roleNames = roles.split("\\s*,\\s*");
  }
 }

}
Giải thích:
- Hàm khởi tạo init sẽ lấy giá trị của tham số "roles" được định nghĩa trong tập tin miêu tả web.xml và khởi tạo giá trị cho biến roleNames.
- Khi máy chủ web nhận được truy vấn đến đường dẫn định nghĩa trong thẻ filter-mapping, cụ thể ở đây là hai đường dẫn "/welcome" và "/logoffProcess", nó sẽ chuyển truy vấn này cho bộ lọc xử lý.
- Hàm xử lý của bộ lọc là doFilter. Ở đây hàm xử lý này xem xem người đăng nhập có vai trò được định nghĩa trong tập tin miêu tả hay không, với thông tin người dùng được lấy từ thuộc tính user mà ta đã gán giá trị trong phần xử lý logonProcess ở trên. Nếu người đăng nhập có vai trò được chấp nhận ta sẽ chuyển truy vấn cho trang tương ứng xử lý, ngược lại ta sẽ chuyển về trang đăng nhập (/logon)

Nội dung trang welcome.jsp người dùng được chuyển đến khi đăng nhập thành công đồng thời có vai trò được chấp nhận trong tập tin miêu tả web.xml
<%@ page language="java" import="java.util.*"%>

<%!ResourceBundle rb;
 public void jspInit() {
  rb = ResourceBundle.getBundle("messages");
 }%>
<jsp:useBean id="userBean" class="com.openspace.UserService"
 scope="session" />

<html>
<head>
<title><%=rb.getString("welcome.title")%></title>
</head>
<body bgcolor="white">


 <h3><%=rb.getString("welcome.heading")%>
  <jsp:getProperty property="username" name="userBean" />
 </h3>
 <p><%=rb.getString("welcome.message")%></p>
<%
  String path = request.getContextPath() + "/logoffProcess";
%>
 <a href="<%=path %>" ><%=rb.getString("logoff.title")%></a>

</body>
</html>
Tập tin này chứa thông tin chào mừng (hiển thị tên người dùng) và một đường dẫn (link) cho phép người dùng đăng xuất

Nội dung tập tin xử lý đăng xuất logoffProcess.jsp
<%@ page import="java.util.*, com.openspace.*" language="java"%>

<%!ResourceBundle rb;

 public void jspInit() {
  rb = ResourceBundle.getBundle("messages");
 }%>

<%
 session.setAttribute("user", null);
 session.invalidate();
 String logoffPath = rb.getString("process.logoff");
%>
<jsp:forward page="<%=logoffPath%>" />
Trong đây ta sẽ xoá nội dung thuộc tính "user" trong phiên kết nối, kết thúc phiên kết nối này và chuyển đến trang thoát.

Nội dung trang thoát logoff.jsp
<%@ page language="java" import="java.util.*"%>

<%!ResourceBundle rb;
 public void jspInit() {
  rb = ResourceBundle.getBundle("messages");
 }%>

<html>
<head>
<title><%=rb.getString("logoff.title")%></title>
</head>
<body bgcolor="white">

 <h3><%=rb.getString("logoff.heading")%></h3>
 <p><%=rb.getString("logoff.message")%></p>

</body>
</html>
Trang này hiển thị thông điệp cho biết người dùng đã đăng xuất thành công.

Không có nhận xét nào:

Đăng nhận xét