Spring Security cung cấp các cách để thực hiện xác thực (authentication) và phân quyền (authorization) trong ứng dụng web. Chúng ta có thể sử dụng Spring Security trong bất kỳ ứng dụng web nào dựa trên servlet.
Spring Security
Một số lợi ích của việc sử dụng Spring Security:
- Công nghệ đã được kiểm chứng – tốt hơn là nên sử dụng Spring Security thay vì tự viết lại từ đầu. Bảo mật là một phần cần đặc biệt cẩn trọng, nếu không ứng dụng sẽ dễ bị tấn công.
- Ngăn chặn một số kiểu tấn công phổ biến như CSRF, session fixation.
- Dễ dàng tích hợp vào bất kỳ ứng dụng web nào – chúng ta không cần phải chỉnh sửa nhiều trong cấu hình ứng dụng web vì Spring tự động thêm các bộ lọc bảo mật vào ứng dụng.
- Hỗ trợ xác thực theo nhiều cách khác nhau – in-memory, DAO, JDBC, LDAP và nhiều hơn nữa.
- Cho phép loại trừ các mẫu URL cụ thể – hữu ích khi muốn bỏ qua bảo mật cho các file HTML tĩnh, hình ảnh,…
- Hỗ trợ phân quyền theo nhóm và vai trò.
Ví dụ Spring Security
Chúng ta sẽ tạo một ứng dụng web và tích hợp nó với Spring Security. Hãy tạo ứng dụng web bằng tùy chọn “Dynamic Web Project” trong Eclipse để tạo sẵn cấu trúc ứng dụng cơ bản. Đảm bảo chuyển dự án này thành dự án Maven, vì chúng ta sẽ sử dụng Maven để build và triển khai.
Sau khi thêm bảo mật, cấu trúc cuối cùng của dự án sẽ giống như hình minh họa trong hướng dẫn.
Chúng ta sẽ tìm hiểu ba phương pháp xác thực với Spring Security:
- in-memory
- DAO
- JDBC
Với JDBC, mình sẽ sử dụng cơ sở dữ liệu MySQL và chạy đoạn script SQL sau để tạo bảng lưu thông tin người dùng.
CREATE TABLE `Employees` (
`username` varchar(20) NOT NULL DEFAULT '',
`password` varchar(20) NOT NULL DEFAULT '',
`enabled` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `Roles` (
`username` varchar(20) NOT NULL DEFAULT '',
`role` varchar(20) NOT NULL DEFAULT '',
PRIMARY KEY (`username`,`role`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `Employees` (`username`, `password`, `enabled`)
VALUES
('pankaj', 'pankaj123', 1);
INSERT INTO `Roles` (`username`, `role`)
VALUES
('pankaj', 'Admin'),
('pankaj', 'CEO');
commit;
Chúng ta cũng cần cấu hình JDBC DataSource dưới dạng JNDI trong servlet container.
Các phụ thuộc Maven cho Spring Security
Dưới đây là file pom.xml cuối cùng của chúng ta.
<project xmlns="<https://maven.apache.org/POM/4.0.0>" xmlns:xsi="<https://www.w3.org/2001/XMLSchema-instance>"
xsi:schemaLocation="<https://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>">
<modelVersion>4.0.0</modelVersion>
<groupId>WebappSpringSecurity</groupId>
<artifactId>WebappSpringSecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- Spring Security Artifacts - START -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>3.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>3.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<!-- Spring Security Artifacts - END -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.0.2.RELEASE</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<warSourceDirectory>WebContent</warSourceDirectory>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
Chúng ta có các phụ thuộc sau liên quan đến Spring Framework:
- spring-jdbc: Được sử dụng cho các thao tác JDBC trong phương thức xác thực JDBC. Nó yêu cầu cấu hình DataSource dưới dạng JNDI.
- spring-security-taglibs: Thư viện thẻ (tag library) của Spring Security. Mình đã sử dụng nó để hiển thị vai trò người dùng trên trang JSP. Tuy nhiên, trong hầu hết các trường hợp, bạn sẽ không cần dùng đến.
- spring-security-config: Được dùng để cấu hình các nhà cung cấp xác thực (authentication providers), ví dụ như JDBC, DAO, LDAP, v.v.
- spring-security-web: Thành phần này tích hợp Spring Security với Servlet API. Chúng ta cần nó để gắn cấu hình bảo mật vào ứng dụng web.
Lưu ý thêm rằng chúng ta sẽ sử dụng tính năng của Servlet API 3.0 để thêm listener và filter bằng cách lập trình (programmatically), do đó phiên bản servlet-api trong phần dependencies nên là 3.0 hoặc cao hơn.
Trang giao diện (View Pages) trong ví dụ Spring Security
Ứng dụng của chúng ta sẽ có các trang JSP và HTML. Mục tiêu là áp dụng xác thực đối với tất cả các trang, trừ các trang HTML.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Health Check</title>
</head>
<body>
<h3>Service is up and running!!</h3>
</body>
</html>
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="<https://java.sun.com/jsp/jstl/core>" prefix="c" %>
<%@ taglib uri="<https://www.springframework.org/security/tags>" prefix="sec" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "<https://www.w3.org/TR/html4/loose.dtd>">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Home Page</title>
</head>
<body>
<h3>Home Page</h3>
<p>
Hello <b><c:out value="${pageContext.request.remoteUser}"/></b><br>
Roles: <b><sec:authentication property="principal.authorities" /></b>
</p>
<form action="logout" method="post">
<input type="submit" value="Logout" />
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
</body>
</html>
Tôi đã thêm index.jsp
làm welcome-file
trong tệp deployment descriptor của ứng dụng. Spring Security sẽ xử lý các cuộc tấn công CSRF, vì vậy khi chúng ta gửi form để đăng xuất, chúng ta cần gửi CSRF token trở lại máy chủ để xóa nó. Đối tượng CSRF được Spring Security thiết lập là _csrf, và chúng ta sử dụng thuộc tính name và token của nó để gửi kèm trong yêu cầu đăng xuất. Giờ chúng ta hãy xem phần cấu hình Spring Security.
Ví dụ Spring Security: Cài đặt DAO cho UserDetailsService
Vì chúng ta sẽ sử dụng xác thực theo kiểu DAO, nên cần phải triển khai giao diện UserDetailsService
và cung cấp phần cài đặt cho phương thức loadUserByUsername()
. Tốt nhất chúng ta nên sử dụng một nguồn dữ liệu để xác thực người dùng, nhưng để đơn giản hóa, tôi chỉ làm một kiểm tra cơ bản. File này có tên là AppUserDetailsServiceDAO.java
.
package com.journaldev.webapp.spring.dao;
import java.util.Collection;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class AppUserDetailsServiceDAO implements UserDetailsService {
protected final Log logger = LogFactory.getLog(getClass());
@Override
public UserDetails loadUserByUsername(final String username)
throws UsernameNotFoundException {
logger.info("loadUserByUsername username="+username);
if(!username.equals("pankaj")){
throw new UsernameNotFoundException(username + " not found");
}
//creating dummy user details, should do JDBC operations
return new UserDetails() {
private static final long serialVersionUID = 2059202961588104658L;
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return "pankaj123";
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> auths = new java.util.ArrayList<SimpleGrantedAuthority>();
auths.add(new SimpleGrantedAuthority("admin"));
return auths;
}
};
}
}
Lưu ý rằng tôi đang tạo một lớp ẩn danh (anonymous inner class) của UserDetails
và trả về nó. Bạn cũng có thể tạo một lớp cài đặt riêng cho UserDetails, sau đó khởi tạo và trả về đối tượng đó. Thông thường, đây là cách tiếp cận phổ biến trong các ứng dụng thực tế.
Ví dụ Spring Security: Cài đặt WebSecurityConfigurer
Chúng ta có thể cài đặt giao diện WebSecurityConfigurer
, hoặc mở rộng lớp cơ sở WebSecurityConfigurerAdapter
và ghi đè các phương thức cần thiết.
File cấu hình bảo mật sẽ là: SecurityConfig.java
.
package com.journaldev.webapp.spring.security;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import com.journaldev.webapp.spring.dao.AppUserDetailsServiceDAO;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(AuthenticationManagerBuilder auth)
throws Exception {
// in-memory authentication
// auth.inMemoryAuthentication().withUser("pankaj").password("pankaj123").roles("USER");
// using custom UserDetailsService DAO
// auth.userDetailsService(new AppUserDetailsServiceDAO());
// using JDBC
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx
.lookup("java:/comp/env/jdbc/MyLocalDB");
final String findUserQuery = "select username,password,enabled "
+ "from Employees " + "where username = ?";
final String findRoles = "select username,role " + "from Roles "
+ "where username = ?";
auth.jdbcAuthentication().dataSource(ds)
.usersByUsernameQuery(findUserQuery)
.authoritiesByUsernameQuery(findRoles);
}
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
// Spring Security should completely ignore URLs ending with .html
.antMatchers("/*.html");
}
}
Lưu ý rằng chúng ta đang bỏ qua tất cả các tệp HTML bằng cách ghi đè phương thức configure(WebSecurity web)
. Đoạn mã trên cũng cho thấy cách cấu hình xác thực JDBC. Chúng ta cần cấu hình bằng cách cung cấp DataSource. Vì đang sử dụng các bảng tùy chỉnh, nên ta cũng cần cung cấp câu lệnh SELECT để lấy thông tin người dùng và vai trò của họ.
Việc cấu hình xác thực in-memory và DAO-based thì đơn giản hơn, chúng đã được ghi chú (comment) trong đoạn mã phía trên. Bạn có thể bỏ ghi chú (uncomment) để sử dụng, nhưng nhớ rằng chỉ nên dùng một phương thức xác thực tại một thời điểm.
Hai annotation @Configuration
và @EnableWebSecurity
là bắt buộc, để Spring Framework biết rằng lớp này sẽ được dùng cho cấu hình bảo mật Spring Security.
Cấu hình Spring Security sử dụng Builder Pattern, và tùy theo phương thức xác thực được chọn, một số phương thức khác sẽ không còn khả dụng sau đó. Ví dụ: auth.userDetailsService()
trả về một thể hiện của UserDetailsService
, và sau đó chúng ta không thể cấu hình DataSource được nữa.
Tích hợp Spring Security Web với Servlet API
Phần cuối cùng là tích hợp lớp cấu hình Spring Security của chúng ta vào Servlet API. Việc này khá đơn giản: chỉ cần mở rộng lớp AbstractSecurityWebApplicationInitializer
và truyền lớp cấu hình bảo mật vào constructor của lớp cha (Super class).
File: SecurityWebApplicationInitializer.java
package com.journaldev.webapp.spring.security;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SecurityWebApplicationInitializer extends
AbstractSecurityWebApplicationInitializer {
public SecurityWebApplicationInitializer() {
super(SecurityConfig.class);
}
}
Khi ứng dụng khởi động, nó sẽ sử dụng ServletContext để thêm ContextLoaderListener và đăng ký lớp cấu hình của chúng ta như một Servlet Filter. Lưu ý rằng việc này chỉ hoạt động trên các Servlet Container tương thích với Servlet 3.0 trở lên. Vì vậy, nếu bạn đang sử dụng Apache Tomcat, hãy đảm bảo rằng phiên bản là 7.0 hoặc cao hơn. Dự án của chúng ta đến đây là đã sẵn sàng, chỉ cần triển khai (deploy) nó vào Servlet Container yêu thích của bạn. Ở đây, tôi đang sử dụng Apache Tomcat 7 để chạy ứng dụng này.
Các hình ảnh bên dưới sẽ minh họa phản hồi của ứng dụng trong nhiều tình huống khác nhau.
Truy cập trang HTML mà không cần xác thực
Xác thực thất bại do thông tin đăng nhập không đúng
Trang chủ với xác thực Spring Security sử dụng JDBC
Trang chủ với xác thực Spring Security sử dụng UserDetailsService (DAO)
Trang chủ với xác thực Spring Security sử dụng In-Memory
Trang đăng xuất
Nếu bạn muốn sử dụng Servlet Container không hỗ trợ Servlet Specs 3.0, thì bạn sẽ cần phải đăng ký DispatcherServlet
thông qua file cấu hình triển khai. Xem phần JavaDoc của WebApplicationInitializer
để biết thêm chi tiết.