Mục tiêu
- Giới thiệu tổng quan về Shiro
- Xây dựng ứng dụng Shiro cơ bản
- Tích hợp Shiro với web container
Shiro là gì?
Apache Shiro là một framework mã nguồn mở dùng để quản lý quyền truy cập, bao gồm xác thực (authentication) và phân quyền (authorization) cho người dùng.
So với Spring Security (trước đây gọi là Acegi), Shiro có ưu điểm là nhẹ hơn và không phụ thuộc mạnh vào Spring. Shiro không chỉ hỗ trợ các ứng dụng web mà còn có thể áp dụng cho hệ thống client-server và hệ thống phân tán. Nhờ tính đơn giản và linh hoạt, Shiro ngày càng được nhiều dự án doanh nghiệp lựa chọn.
Ba thành phần cốt lõi
- Subject: Đại diện cho "người dùng" hiện tại, có thể là người thật, chương trình tự động, robot, v.v. Subject là một khái niệm trừu tượng, tương tác với SecurityManager thông qua một giao diện trung gian (facade).
- SecurityManager: Là trung tâm quản lý bảo mật, chịu trách nhiệm điều phối tất cả các hoạt động liên quan đến bảo mật. Nó quản lý mọi Subject và có thể so sánh như DispatcherServlet trong Spring MVC.
- Realm: Đóng vai trò là "nguồn dữ liệu bảo mật", nơi Shiro truy xuất thông tin người dùng, vai trò và quyền hạn. SecurityManager sử dụng Realm để xác thực và phân quyền.
Kiến trúc chi tiết
- Subject: Đối tượng tương tác với hệ thống, cần được xác thực và phân quyền.
- SecurityManager: Bộ điều phối chính, thực chất ủy quyền các công việc cho các thành phần bên trong.
- Authenticator: Thành phần chịu trách nhiệm xác thực danh tính người dùng.
- Authorizer: Thành phần chịu trách nhiệm phân quyền truy cập.
- SessionManager: Quản lý phiên làm việc, có thể dùng session của web container hoặc tự quản lý.
- SessionDao: Quản lý dữ liệu phiên, hữu ích khi cần lưu trữ phiên tập trung trong hệ thống phân tán.
- CacheManager: Quản lý bộ nhớ đệm cho dữ liệu phiên và phân quyền, thường tích hợp với Ehcache hoặc Redis.
- Realm: Nguồn dữ liệu cho xác thực và phân quyền.
- Cryptography: Cung cấp các tiện ích mã hóa, giải mã và băm (hash), ví dụ như MD5.
Ví dụ khởi tạo Shiro
1. Cấu hình Log4j2
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Properties>
<Property name="LOG_HOME">${sys:user.home}/logs</Property>
<property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t-%L] %-5level %logger{36} - %msg%n</property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${PATTERN}"/>
</Console>
<File name="log" fileName="logs/test.log" append="false">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</File>
<RollingFile name="RollingFileInfo" fileName="${LOG_HOME}/info.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<logger name="org.springframework" level="ERROR"/>
<logger name="org.mybatis" level="ERROR"/>
<logger name="org.apache.shiro" level="ERROR"/>
<root level="all">
<appender-ref ref="Console"/>
</root>
</Loggers>
</Configuration>
2. File POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>shiro-demo</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<properties>
<shiro.version>1.2.5</shiro.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3. File cấu hình shiro.ini
[users]
zs=123
ls=456
ww=789
4. Chương trình xác thực cơ bản
package com.example;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
public class AuthExample {
public static void main(String[] args) {
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager manager = factory.getInstance();
SecurityUtils.setSecurityManager(manager);
Subject currentUser = SecurityUtils.getSubject();
String username = "zs";
String password = "123";
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
currentUser.login(token);
System.out.println("Login successful!");
} catch (Exception e) {
System.out.println("Login failed: " + e.getMessage());
}
currentUser.logout();
System.out.println("Logged out.");
}
}
Tích hợp Shiro với Web Container
1. Cấu hình shiro-web.ini
[main]
authc.loginUrl=/login
roles.unauthorizedUrl=/unauthorized.jsp
perms.unauthorizedUrl=/unauthorized.jsp
[users]
zs=123,role1
ls=123,role2
ww=123,role3
admin=123,admin
[roles]
role1=user:create
role2=user:create,user:update
role3=user:create,user:update,user:delete,user:view,user:load
admin=user:*
[urls]
/login.do=anon
/user/updatePwd.jsp=authc
/admin/*.jsp=roles[admin]
/user/teacher.jsp=perms["user:update"]
2. Cấu hình web.xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<context-param>
<param-name>shiroConfigLocations</param-name>
<param-value>classpath:shiro-web.ini</param-value>
</context-param>
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
3. Servlet xử lý đăng nhập
package com.example;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
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 java.io.IOException;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
req.getRequestDispatcher("main.jsp").forward(req, resp);
} catch (Exception e) {
req.setAttribute("error", "Invalid username or password.");
req.getRequestDispatcher("login.jsp").forward(req, resp);
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
}
4. Servlet xử lý đăng xuất
package com.example;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
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 java.io.IOException;
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Subject subject = SecurityUtils.getSubject();
subject.logout();
resp.sendRedirect(req.getContextPath() + "/login.jsp");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}