Bài viết gốc có thể đọc ở đây.
1. Tổng quan
Như tiêu đề chúng ta sẽ minh hoạ cách triển khai một trang đăng nhập đơn giản với Spring MVC cho một ứng dụng cần xác thực với Spring Security ở phía backend.
Để biết chi tiết và đầy đủ về cách kiểm soát đăng nhập với Spring Security, đây là bài viết đi sâu về cách cấu hình và triển khai điều này.
2. Trang đăng nhập
Cùng bằng đầu với một trang đăng nhập cực kỳ đơn giản nào:
<html>
<head></head>
<body>
<h1>Login</h1>
<form name='f' action="login" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td><input name="submit" type="submit" value="submit" /></td>
</tr>
</table>
</form>
</body>
</html>
Bây giờ, ở phía người dùng, chúng ta kiểm tra xem người dùng đã điền tên đăng nhập và mật khẩu chưa trước khi gửi biểu mẫu. Ở ví dụ này chúng ta sẽ sử dụng Javascript thuần tuý nhưng JQuery cũng là một lựa chọn tốt:
<script type="text/javascript">
function validate() {
if (document.f.username.value == "" && document.f.password.value == "") {
alert("Username and password are required");
document.f.username.focus();
return false;
}
if (document.f.username.value == "") {
alert("Username is required");
document.f.username.focus();
return false;
}
if (document.f.password.value == "") {
alert("Password is required");
document.f.password.focus();
return false;
}
}
</script>
Bạn thấy đó, chúng ta đơn giản kiểm tra nếu tên đăng nhập và mật khẩu trống thì chúng ta sẽ hiển thị một thông báo Javascript sẽ hiện lên để thông báo.
3. Bản địa hoá thông báo
Tiếp theo, hãy bản địa hoá thông báo chúng ta đang sử dụng ở front end. Có nhiều loại thông báo khác nhau và mỗi loại được bản địa hoá theo một cách khác nhau:
- Thông báo được sinh ra trước khi biểu mẫu được xử lý bởi controller hoặc trình xử lý của Spring. Những thông báo này có thể được tham chiếu đến trang JSP và được bản địa hoá với Jsp/Jslt bản địa hoá (xem thêm ở phần 4.3)
- Thông báo được bản địa hoá ở mỗi trang khi đã được xử lý bởi Spring (sau khi biểu mẫu đăng nhập được gửi đi); các thông báo này được bản địa hoá bởi Spring MVC bản địa hoá (xem thêm ở phần 4.2)
3.1 File message.properties
Trong cả hai trường hợp, chúng ta đều cần tạo một file message.properties cho mỗi ngôn ngữ chúng ta muốn sử dụng; tên của file nên theo thông lệ như sau: messages_[mã ngôn ngữ].properties.
Ví dụ, nếu chúng ta muốn hỗ trợ thông báo lỗi bằng tiếng Anh và tiếng Tây Ban Nha chúng ta sẽ có file sau: messages_en.properties và messages_es_ES.properties. Chú ý rằng, với tiếng Anh - messages.properties là ok.
Chúng ta sẽ đặt hai file này vào trong đường dẫn của dự án (src/main/resources). File đơn giản chỉ chứ mã lỗi và thông báo chúng ta cần hiển thị trong các ngôn ngữ khác nhau, ví dụ:
message.username=Username required
message.password=Password required
message.unauth=Unauthorized access!!
message.badCredentials=Invalid username or password
message.sessionExpired=Session timed out
message.logoutError=Sorry, error login out
message.logoutSucc=You logged out successfully
3.2. Cấu hình bản địa hoá với Spring MVC
Spring MVC cung cấp LocaleResolver, nó sẽ hoạt động khi kết hợp với API LocaleChangeInterceptor để có thể hiển thị thông báo với các ngôn ngữ khác nhau phụ thuộc vào cài đặt ngôn ngữ. Để cấu hình bản địa hoá, chúng ta cần định nghĩa các bean trong cấu hình MVC của chúng ta như sau:
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
registry.addInterceptor(localeChangeInterceptor);
}
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
return cookieLocaleResolver;
}
Mặc định, ứng dụng sẽ lấy ngôn ngữ dựa vào mã ngôn ngữ ở header HTTP. Để thiết lập một ngôn ngữ mặc định, chúng ta cần cài đặt nó trong localeResolver():
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
cookieLocaleResolver.setDefaultLocale(Locale.ENGLISH);
return cookieLocaleResolver;
}
CookieLocaleResolver là trình giải ngôn ngữ, nó sẽ lưu trữ thông tin ngôn ngữ trong cookie của người dùng. Vì vậy, nó sẽ ghi nhớ ngôn ngữ của người dùng mỗi lần họ đăng nhập vào và trong suốt khoảng thời gian họ online.
Nói cách khác, SessionLocaleResolver sẽ ghi nhớ ngôn ngữ của người dùng trong session. Để sử dụng LocaleResolver thay thế, chúng ta cần thay thế phương thức trên bằng phương thức sau:
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
return sessionLocaleResolver;
}
Cuối cùng, hãy chú ý rằng LocaleChangeInterceptor sẽ thay đổi ngôn ngữ mặc định trong giá trị của đối số lang gửi đến trang đăng nhập như sau:
<a href="?lang=en">English</a> |
<a href="?lang=es_ES">Spanish</a>
3.3 Bản địa hoá bằng JSP/JSLT
JSP/JSLT API sẽ hiển thị thông báo bản địa hoá bằng chính trang jsp của nó. Để sử dụng thư viện bản địa hoá jsp chúng ta phải thêm dependency vào pom.xml như sau:
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.2-b01</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
4. Hiển thị thông báo lỗi
4.1. Lỗi xác thực đăng nhập
Để sử dụng JSP/JSTL và hiển thị thông báo bản địa hóa trong login.jsp chúng ta sẽ thay đổi như sau:
- Thêm thẻ thư viện vào login.jsp như sau:
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
- Thêm phần tử jsp/jslt sẽ chỉ đến file messages.properties:
<fmt:setBundle basename="messages" />
- Thêm phần tử <fmt: …> để lưu trữ giá trị thông báo trong biến jsp:
<fmt:message key="message.password" var="noPass" /> <fmt:message key="message.username" var="noUser" />
- Sửa đổi đoạn mã xác thực đăng nhập chúng ta đã viết ở đoạn 3 để bản địa hoá thông báo lỗi:
<script type="text/javascript"> function validate() { if (document.f.username.value == "" && document.f.password.value == "") { alert("${noUser} and ${noPass}"); document.f.username.focus(); return false; } if (document.f.username.value == "") { alert("${noUser}"); document.f.username.focus(); return false; } if (document.f.password.value == "") { alert("${noPass}"); document.f.password.focus(); return false; } } </script>
4.2. Lỗi đăng nhập trước
Đôi khi trang đăng nhập sẽ chuyển đối số lỗi nếu thao tác trước đó không thành công. Ví dụ,
nút gửi biểu mẫu sẽ tải trang đăng nhập. Nếu đăng ký thành công, thì sẽ hiển thị thông báo ok trong biểu mẫu đăng nhập và thông báo lỗi nếu không đăng ký được.
Ví dụ biểu mẫu đăng nhập bên dưới, chúng ta sẽ triển khai điều này bằng cách chặn các tham số regSucc và regError, đồng thời hiển thị một thông báo được bản địa hoá dựa trên giá trị của chúng.
<c:if test="${param.regSucc == true}">
<div id="status">
<spring:message code="message.regSucc">
</spring:message>
</div>
</c:if>
<c:if test="${param.regError == true}">
<div id="error">
<spring:message code="message.regError">
</spring:message>
</div>
</c:if>
4.3. Lỗi bảo mật đăng nhập
Trong trường hợp quá trình đăng nhập bị lỗi vì lý do nào đó, Spring Security sẽ chuyển hướng tới URL lỗi đăng nhập, chúng ta sẽ định nghĩa như sau /login.html?error=true.
Vì vậy, tương tự cách chúng ta hiển thị trạng thái đăng ký trong trang, chúng ta cần làm tương tự trong trường hợp đăng nhập có vấn đề:
<c:if test="${param.error != null}">
<div id="error">
<spring:message code="message.badCredentials">
</spring:message>
</div>
</c:if>
Chú ý rằng chúng ta đang sử dụng <spring:message …>. Có nghĩa là thông báo lỗi sẽ được tự động sinh ra trong quá trình Spring MVC xử lý.
Trang đăng nhập đầy đủ bao gồm cả JS và thông báo trạng thái có thể xem thêm ở đây .
4.4. Lỗi đăng xuất
Trong ví dụ sau, code jsp <c:if test=”${not empty SPRING_SECURITY_LAST_EXCEPTION}”> trong logout.html sẽ kiểm tra nếu có lỗi trong quá trình đăng xuất.
Ví dụ nếu đây là một ngoại lệ persistence khi trình xử lý đăng xuất tuỳ chỉnh cố gắng lưu trữ dữ liệu người sử dụng trước khi chuyển hướng đến trang đăng xuất. Mặc dù những lỗi này rất hiếm, chúng ta cũng nên xử lý chúng gọn gàng nhất có thể.
Chúng ta cùng xem logout.jsp hoàn chỉnh:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="sec"
uri="http://www.springframework.org/security/tags"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<c:if test="${not empty SPRING_SECURITY_LAST_EXCEPTION}">
<div id="error">
<spring:message code="message.logoutError">
</spring:message>
</div>
</c:if>
<c:if test="${param.logSucc == true}">
<div id="success">
<spring:message code="message.logoutSucc">
</spring:message>
</div>
</c:if>
<html>
<head>
<title>Logged Out</title>
</head>
<body>
<a href="login.html">Login</a>
</body>
</html>
Chú ý rằng trang đăng xuất này cũng đọc chuỗi query logSucc, và nếu giá trị của nó bằng true, thông báo thành công bản địa hoá sẽ được hiển thị.
5. Cấu hình Spring Security
Trọng tâm của bài viết này là phần frontend của quá trình đăng nhập chứ không phải là backend vì vậy chúng ta sẽ chỉ lướt qua một vài vấn đề chính trong cấu hình bảo mật. Để cấu hình đầy đủ, bạn nên đọc ở bài viết này.
5.1. Chuyển hướng đến URL lỗi đăng nhập
Lệnh sau trong <form-login.../> sẽ chuyển hướng ứng dụng tới URL xử lý lỗi đăng nhập:
authentication-failure-url="/login.html?error=true"
5.2. Chuyển hướng đăng xuất thành công
<logout
invalidate-session="false"
logout-success-url="/logout.html?logSucc=true"
delete-cookies="JSESSIONID" />
Thuộc tính logout-success-url đơn giản sẽ chuyển hướng đến trang đăng xuất với đối số xác nhận rằng đăng xuất đã thành công.
6. Tổng kết
Ở bài viết này chúng ta đã được giới thiệu cách triển khai trang đăng nhập cho ứng dụng sử dụng backend là Spring Security để xử lý xác thực đăng nhận, hiển thị lỗi đăng nhập và thông báo được bản địa hoá.
Bình luận