Tạo Một Ứng Dụng Login Đơn Giản Và Bảo Mật Với Java Servlet Filter
Bảo mật (Security) là môt khía cạnh quan trọng của một ứng dụng có sự vận chuyển các dữ liệu quan trọng trên .
Authentication (Xác thực)
Xác thực là quá trình mà quyền truy cập (access privileges) của người dùng được xác minh trước khi họ vào khu vực được bảo vệ của . Có hai cách tiếp cận xác thực chính: xác thực cơ bản và xác thực dựa trên biểu mẫu (Form-based authentication).
Basic Authentication (Xác thực cơ bản).
Với xác thực cơ bản, người dùng có thể truy cập mọi trang (page) hoàn toàn bình thường, với các trang yêu cầu bảo mật, một cửa sổ sẽ hiển thị để người dùng nhập vào của họ. Thông tin sẽ được gói lại gửi kèm theo yêu cầu (request) đến .
Khi người dùng nhập một đường dẫn trên trình duyệt, và nhấn để yêu cầu một trang (page). Một thông tin được tạo ra và được gửi kèm theo yêu cầu. Thông thường thông tin này bao gồm thông tin trình duyệt của người dùng, thông tin hệ điều hành. Trong trường hợp xác thực cơ bản (basic authentication) thông tin được gói bên trong .
Trong bài học này tôi không đề cập chi tiết về xác thực cơ bản.
Form-based Authentication (Xác thực dựa trên biểu mẫu)
Hầu hết các Website sử dụng hình thức xác thực dựa trên biểu mẫu (Form-based Authentication). Website cho phép người dùng truy cập mọi trang thông thường mà không yêu cầu mật khẩu. Tuy nhiên nếu người dùng truy cập vào một trang được bảo vệ, nó sẽ chuyển hướng tới một trang đăng nhập.
Trong bài học này, tôi sẽ đề cập chi tiết về cách sử dụng một để bảo mật ứng dụng .
2- Khái niệm về Role và Principal
Trong bảo mật, có 2 khái niệm quan trọng là và .
(Vai trò) là một tập hợp các quyền (permission) đối với một ứng dụng.
Để đơn giản tôi đưa ra một ví dụ, ứng dụng có 2 vai trò (nhân viên) và (Người quản lý).
- Vai trò được phép sử dụng các chức năng bán hàng, và chức năng tạo mới thông tin khách hàng.
- Vai trò được phép sử dụng các chức năng quản lý nhân viên, và xem các báo cáo doanh thu.
có thể tạm hiểu là một "Chủ thể" sau khi đã đăng nhập vào một hệ thống, họ có quyền làm điều gì đó trong hệ thống. Một "Chủ thể" có thể có một hoặc nhiều vai trò. Điều này phụ thuộc vào sự phân quyền của ứng dụng cho mỗi tài khoản người dùng khác nhau.
Trong ứng dụng , một đặc biệt được sử dụng để xử lý bảo mật, nó thường được gọi là .
Khi người dùng truy cập vào một trang (page) được bảo vệ, sẽ kiểm tra, nếu người dùng chưa đăng nhập, yêu cầu của người dùng sẽ bị chuyển hướng (redirect) sang trang đăng nhập.
Nếu người dùng đã đăng nhập thành công, một đối tượng được tạo ra, nó mang các thông tin của người dùng, bao gồm cả các vai trò.
Nếu người dùng đã đăng nhập thành công trước đó, và truy cập vào một trang (page) được bảo vệ. sẽ kiểm tra các vai trò của người dùng có phù hợp để truy cập vào trang này hay không. Nếu không hợp lệ, nó sẽ hiển thị cho người dùng một trang thông báo truy cập bị cấm (Access Denied).
Đây là cấu trúc của ứng dụng mà chúng ta sẽ thực hiện:
Ứng dụng bao gồm 2 vai trò (Role) là và .
- Vai trò cho phép truy cập 2 trang và
- Vai trò cho phép truy cập 2 trang và .
Có 2 người dùng (user) là và .
- Người dùng được gán vai trò
- Người dùng được gán cả 2 vai trò và .
Người dùng có thể truy cập tất cả các trang không được bảo hộ một cách bình thường. Tuy nhiên nếu người dùng truy cập vào một trang được bảo hộ, nó sẽ chuyển hướng (redirect) tới trang đăng nhập. Người dùng đăng nhập thành công, lúc này sẽ có 2 tình huống xẩy ra:
- Ứng dụng sẽ chuyển hướng về trang sau khi đăng nhập thành công, nếu có vai trò phù hợp.
- Ứng dụng sẽ hiển thị thông báo truy cập bị từ chối (Access Denied) nếu có vai trò không phù hợp.
Lớp đại diện cho người dùng của ứng dụng.
package org.o7planning.securitywebapp.bean; import java.util.ArrayList; import java.util.List; public class UserAccount { public static final String GENDER_MALE = "M"; public static final String GENDER_FEMALE = "F"; private String userName; private String gender; private String password; public UserAccount() { } public UserAccount(String userName, String password, String gender, String... roles) { this.userName = userName; this.password = password; this.gender = gender; if (roles != null) { for (String r : roles) { this.roles.add(r); } } } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } return roles; } this.roles = roles; } }
Lớp được sử dụng để truy vấn dữ liệu trong Database (Mô phỏng).
package org.o7planning.securitywebapp.utils; import java.util.HashMap; import java.util.Map; import org.o7planning.securitywebapp.bean.UserAccount; import org.o7planning.securitywebapp.config.SecurityConfig; public class DataDAO { static { initUsers(); } private static void initUsers() { // User này có 1 vai trò là EMPLOYEE. UserAccount emp = new UserAccount("employee1", "123", UserAccount.GENDER_MALE, // SecurityConfig.ROLE_EMPLOYEE); // User này có 2 vai trò EMPLOYEE và MANAGER. UserAccount mng = new UserAccount("manager1", "123", UserAccount.GENDER_MALE, // SecurityConfig.ROLE_EMPLOYEE, SecurityConfig.ROLE_MANAGER); mapUsers.put(emp.getUserName(), emp); mapUsers.put(mng.getUserName(), mng); } // Tìm kiếm người dùng theo userName và password. public static UserAccount findUser(String userName, String password) { UserAccount u = mapUsers.get(userName); if (u != null && u.getPassword().equals(password)) { return u; } return null; } }
7- SecurityConfig & SecurityUtils
Lớp giúp cấu hình các vai trò và các chức năng (các trang) được phép truy cập ứng với vai trò đó.
package org.o7planning.securitywebapp.config; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; public class SecurityConfig { public static final String ROLE_MANAGER = "MANAGER"; public static final String ROLE_EMPLOYEE = "EMPLOYEE"; // String: Role static { init(); } private static void init() { // Cấu hình cho vai trò "EMPLOYEE". urlPatterns1.add("/userInfo"); urlPatterns1.add("/employeeTask"); mapConfig.put(ROLE_EMPLOYEE, urlPatterns1); // Cấu hình cho vai trò "MANAGER". urlPatterns2.add("/userInfo"); urlPatterns2.add("/managerTask"); mapConfig.put(ROLE_MANAGER, urlPatterns2); } return mapConfig.keySet(); } return mapConfig.get(role); } }
Lớp là một lớp tiện ích, nó có các phương thức giúp kiểm tra một (yêu cầu) có bắt buộc phải đăng nhập hay không, và đó có phù hợp với vai trò của người dùng đã đăng nhập hay không.
package org.o7planning.securitywebapp.utils; import java.util.List; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.o7planning.securitywebapp.config.SecurityConfig; public class SecurityUtils { // Kiểm tra 'request' này có bắt buộc phải đăng nhập hay không. public static boolean isSecurityPage(HttpServletRequest request) { String urlPattern = UrlPatternUtils.getUrlPattern(request); for (String role : roles) { if (urlPatterns != null && urlPatterns.contains(urlPattern)) { return true; } } return false; } // Kiểm tra 'request' này có vai trò phù hợp hay không? public static boolean hasPermission(HttpServletRequest request) { String urlPattern = UrlPatternUtils.getUrlPattern(request); for (String role : allRoles) { if (!request.isUserInRole(role)) { continue; } if (urlPatterns != null && urlPatterns.contains(urlPattern)) { return true; } } return false; } }
package org.o7planning.securitywebapp.utils; import java.util.Collection; import java.util.Map; import javax.servlet.ServletContext; import javax.servlet.ServletRegistration; import javax.servlet.http.HttpServletRequest; public class UrlPatternUtils { private static boolean hasUrlPattern(ServletContext servletContext, String urlPattern) { for (String servletName : map.keySet()) { ServletRegistration sr = map.get(servletName); if (mappings.contains(urlPattern)) { return true; } } return false; } // servletPath: public static String getUrlPattern(HttpServletRequest request) { ServletContext servletContext = request.getServletContext(); String servletPath = request.getServletPath(); String pathInfo = request.getPathInfo(); String urlPattern = null; if (pathInfo != null) { urlPattern = servletPath + "/*"; return urlPattern; } urlPattern = servletPath; boolean has = hasUrlPattern(servletContext, urlPattern); if (has) { return urlPattern; } int i = servletPath.lastIndexOf('.'); if (i != -1) { String ext = servletPath.substring(i + 1); urlPattern = "*." + ext; has = hasUrlPattern(servletContext, urlPattern); if (has) { return urlPattern; } } return "/"; } }
là một , nó làm nhiệm vụ kiểm tra các trước khi cho phép truy cập vào các trang (page) được bảo hộ.
đọc đã được khai báo trong lớp .
package org.o7planning.securitywebapp.filter; import java.io.IOException; import java.util.List; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.o7planning.securitywebapp.bean.UserAccount; import org.o7planning.securitywebapp.request.UserRoleRequestWrapper; import org.o7planning.securitywebapp.utils.AppUtils; import org.o7planning.securitywebapp.utils.SecurityUtils; @WebFilter("/*") public class SecurityFilter implements Filter { public SecurityFilter() { } @Override public void destroy() { } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String servletPath = request.getServletPath(); // Thông tin người dùng đã được lưu trong Session // (Sau khi đăng nhập thành công). UserAccount loginedUser = AppUtils.getLoginedUser(request.getSession()); if (servletPath.equals("/login")) { chain.doFilter(request, response); return; } HttpServletRequest wrapRequest = request; if (loginedUser != null) { // User Name String userName = loginedUser.getUserName(); // Các vai trò (Role). // Gói request cũ bởi một Request mới với các thông tin userName và Roles. wrapRequest = new UserRoleRequestWrapper(userName, roles, request); } // Các trang bắt buộc phải đăng nhập. if (SecurityUtils.isSecurityPage(request)) { // Nếu người dùng chưa đăng nhập, // Redirect (chuyển hướng) tới trang đăng nhập. if (loginedUser == null) { String requestUri = request.getRequestURI(); // Lưu trữ trang hiện tại để redirect đến sau khi đăng nhập thành công. int redirectId = AppUtils.storeRedirectAfterLoginUrl(request.getSession(), requestUri); response.sendRedirect(wrapRequest.getContextPath() + "/login?redirectId=" + redirectId); return; } // Kiểm tra người dùng có vai trò hợp lệ hay không? boolean hasPermission = SecurityUtils.hasPermission(wrapRequest); if (!hasPermission) { RequestDispatcher dispatcher // = request.getServletContext().getRequestDispatcher("/WEB-INF/views/accessDeniedView.jsp"); dispatcher.forward(request, response); return; } } chain.doFilter(wrapRequest, response); } @Override public void init(FilterConfig fConfig) throws ServletException { } }
UserRoleRequestWrapper.java
package org.o7planning.securitywebapp.request; import java.security.Principal; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; /** * An extension for the HTTPServletRequest that overrides the getUserPrincipal() * and isUserInRole(). We supply these implementations here, where they are not * normally populated unless we are going through the facility provided by the * container. * If he user or roles are null on this wrapper, the parent request is consulted * to try to fetch what ever the container has set for us. This is intended to * be created and used by the UserRoleFilter. * * @author thein * */ public class UserRoleRequestWrapper extends HttpServletRequestWrapper { private String user; private HttpServletRequest realRequest; super(request); this.user = user; this.roles = roles; this.realRequest = request; } @Override public boolean isUserInRole(String role) { if (roles == null) { return this.realRequest.isUserInRole(role); } return roles.contains(role); } @Override public Principal getUserPrincipal() { if (this.user == null) { return realRequest.getUserPrincipal(); } // Make an anonymous implementation to just return our user return new Principal() { @Override public String getName() { return user; } }; } }
9- Trang chủ, Login, Logout
package org.o7planning.securitywebapp.servlet; import java.io.IOException; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet({ "/", "/index" }) public class HomeServlet extends HttpServlet { private static final long serialVersionUID = 1L; public HomeServlet() { super(); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher // = this.getServletContext().getRequestDispatcher("/WEB-INF/views/homeView.jsp"); dispatcher.forward(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
package org.o7planning.securitywebapp.servlet; import java.io.IOException; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.o7planning.securitywebapp.bean.UserAccount; import org.o7planning.securitywebapp.utils.AppUtils; import org.o7planning.securitywebapp.utils.DataDAO; @WebServlet("/login") public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; public LoginServlet() { super(); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher // = this.getServletContext().getRequestDispatcher("/WEB-INF/views/loginView.jsp"); dispatcher.forward(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userName = request.getParameter("userName"); String password = request.getParameter("password"); UserAccount userAccount = DataDAO.findUser(userName, password); if (userAccount == null) { String errorMessage = "Invalid userName or Password"; request.setAttribute("errorMessage", errorMessage); RequestDispatcher dispatcher // = this.getServletContext().getRequestDispatcher("/WEB-INF/views/loginView.jsp"); dispatcher.forward(request, response); return; } AppUtils.storeLoginedUser(request.getSession(), userAccount); // int redirectId = -1; try { redirectId = Integer.parseInt(request.getParameter("redirectId")); } catch (Exception e) { } String requestUri = AppUtils.getRedirectAfterLoginUrl(request.getSession(), redirectId); if (requestUri != null) { response.sendRedirect(requestUri); } else { // Mặc định sau khi đăng nhập thành công // chuyển hướng về trang /userInfo response.sendRedirect(request.getContextPath() + "/userInfo"); } } }
package org.o7planning.securitywebapp.utils; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpSession; import org.o7planning.securitywebapp.bean.UserAccount; public class AppUtils { private static int REDIRECT_ID = 0; // Lưu trữ thông tin người dùng vào Session. public static void storeLoginedUser(HttpSession session, UserAccount loginedUser) { // Trên JSP có thể truy cập thông qua ${loginedUser} session.setAttribute("loginedUser", loginedUser); } // Lấy thông tin người dùng lưu trữ trong Session. public static UserAccount getLoginedUser(HttpSession session) { UserAccount loginedUser = (UserAccount) session.getAttribute("loginedUser"); return loginedUser; } public static int storeRedirectAfterLoginUrl(HttpSession session, String requestUri) { Integer id = uri_id_map.get(requestUri); if (id == null) { id = REDIRECT_ID++; uri_id_map.put(requestUri, id); id_uri_map.put(id, requestUri); return id; } return id; } public static String getRedirectAfterLoginUrl(HttpSession session, int redirectId) { String url = id_uri_map.get(redirectId); if (url != null) { return url; } return null; } }
package org.o7planning.securitywebapp.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/logout") public class LogoutServlet extends HttpServlet { private static final long serialVersionUID = 1L; public LogoutServlet() { super(); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.getSession().invalidate(); // Redrect to Home Page. response.sendRedirect(request.getContextPath() + "/"); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet(request, response); } }
package org.o7planning.securitywebapp.servlet; import java.io.IOException; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/userInfo") public class UserInfoServlet extends HttpServlet { private static final long serialVersionUID = 1L; public UserInfoServlet() { super(); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher // = this.getServletContext().getRequestDispatcher("/WEB-INF/views/userInfoView.jsp"); dispatcher.forward(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
<web-app xmlns:xsi="w3.org/2001/XMLSchema-instance" xmlns="java.sun.com/xml/ns/javaee" xsi:schemaLocation="java.sun.com/xml/ns/javaee java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
Employee Task || Manager Task || User Info || Login || Logout
/WEB-INF/views/homeView.jsp
/WEB-INF/views/loginView.jsp
manager1/123
/WEB-INF/views/userInfoView.jsp
Chạy ứng dụng:
package org.o7planning.securitywebapp.servlet; import java.io.IOException; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/employeeTask") public class EmployeeTaskServlet extends HttpServlet { private static final long serialVersionUID = 1L; public EmployeeTaskServlet() { super(); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher // = this.getServletContext()// .getRequestDispatcher("/WEB-INF/views/employeeTaskView.jsp"); dispatcher.forward(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
package org.o7planning.securitywebapp.servlet; import java.io.IOException; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/managerTask") public class ManagerTaskServlet extends HttpServlet { private static final long serialVersionUID = 1L; public ManagerTaskServlet() { super(); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher // = this.getServletContext()// .getRequestDispatcher("/WEB-INF/views/managerTaskView.jsp"); dispatcher.forward(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
/WEB-INF/views/employeeTaskView.jsp
Hello, This is a protected page!
/WEB-INF/views/managerTaskView.jsp
Hello, This is a protected page!
/WEB-INF/views/accessDenied.jsp
Chạy ứng dụng:
Chạy ứng dụng và đăng nhập với , đây là người dùng có vai trò .
Chạy ứng dụng và đăng nhập với , đây là người dùng có 2 vai trò và .