【JavaWeb学习 | 第23篇】监听器、RBAC权限模型


🌈 个人主页: Hygge_Code
🔥 热门专栏:从0开始学习Java | Linux学习| 计算机网络
💫 个人格言: "既然选择了远方,便不顾风雨兼程"

文章目录

Java Web核心技术应用(监听器+RBAC权限模型)

第一部分:Servlet监听器详解🥝

一、监听器核心概念🍋‍🟩

1. 什么是Servlet监听器?

Servlet监听器是Java Web的核心组件之一,用于监听Web应用中特定对象的状态变化,并在事件触发时执行预设逻辑。它就像一个"哨兵",实时监控目标对象的生命周期或属性变化,适用于资源初始化、状态监控等场景。

2. 核心监听对象与场景
监听对象 作用范围 常用场景
ServletContext 应用全局(唯一) 初始化数据源、加载全局配置、统计系统启动时间
HttpSession 用户会话 统计在线人数、监听会话超时、记录用户行为
ServletRequest 单次请求 记录请求参数、监控请求响应时间
3. 本次焦点:ServletContextListener🍂

ServletContextListener是最常用的监听器,专注于应用上下文的创建与销毁,核心方法:

  • contextInitialized(ServletContextEvent sce):应用启动时触发(Tomcat启动后自动执行),适合初始化全局资源;
  • contextDestroyed(ServletContextEvent sce):应用关闭时触发(Tomcat停止前执行),适合释放资源。

二、监听器实战:Druid数据源初始化🍋‍🟩

1. 核心需求

应用启动时自动初始化Druid数据库连接池,避免重复创建连接导致的资源浪费;应用关闭时自动关闭连接池,释放数据库资源。

2. 创建resources资源依赖文件

在resources目录下创建jdbc.properties:

properties 复制代码
druid.url=jdbc:mysql://localhost:3306/lesson?serverTimezone=UTC
druid.driverClassName=com.mysql.cj.jdbc.Driver
druid.username=root
druid.password=root
3. 配套配置(web.xml)

若需通过配置文件指定JDBC参数路径,在web.xml中添加如下配置:

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_4_0.xsd"
         version="4.0">

    <!-- 全局参数:JDBC配置文件路径 -->
    <context-param>
        <param-name>jdbcConfig</param-name>
        <param-value>/jdbc.properties</param-value> <!-- 配置文件放在类路径根目录 -->
    </context-param>

    <!-- 可选:若不使用@WebListener注解,可手动注册监听器 -->
    <!--
    <listener>
        <listener-class>com.listener.ApplicationContextListener</listener-class>
    </listener>
    -->
</web-app>
4. 监听器完整代码
java 复制代码
package com.listener;

import com.jdbc.JdbcUtil;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * Servlet上下文监听器
 * 作用:在Web应用启动时初始化JDBC数据源,在应用销毁时关闭数据源
 * 监听ServletContext的生命周期(初始化和销毁),实现资源的统一管理
 *
 * @WebListener 注解:表明该类是Servlet上下文监听器,容器启动时自动注册,无需在web.xml中手动配置
 */
@WebListener
public class ApplicationContextListener implements ServletContextListener {

    /**
     * 监听ServletContext初始化事件(Web应用启动时触发)
     * 核心职责:读取JDBC配置文件,初始化数据源,为整个应用提供数据库连接支持
     *
     * @param sce ServletContextEvent:上下文事件对象,用于获取ServletContext和相关事件信息
     */
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("Servlet上下文初始化");

        // 1. 获取ServletContext(应用上下文对象),全局唯一,用于获取应用级配置和资源
        ServletContext context = sce.getServletContext();

        // 2. 从web.xml中读取名为"jdbcConfig"的上下文初始化参数(配置的是JDBC配置文件路径)
        String jdbcConfig = context.getInitParameter("jdbcConfig");

        // 3. 通过类加载器获取配置文件的输入流(读取类路径下的配置文件)
        // this.getClass():获取当前监听器类的Class对象
        // getResourceAsStream():根据配置的路径加载资源,返回输入流
        InputStream is = this.getClass().getResourceAsStream(jdbcConfig);

        // 4. 创建Properties对象,用于存储JDBC配置信息(如url、username、password等)
        Properties props = new Properties();

        try {
            // 5. 加载输入流中的配置信息到Properties对象
            props.load(is);

            // 6. 调用JdbcUtil工具类的初始化方法,传入配置参数,初始化数据源(如连接池)
            JdbcUtil.initDataSource(props);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 监听ServletContext销毁事件(Web应用停止时触发)
     * 核心职责:关闭JDBC数据源,释放数据库连接等资源,避免资源泄露
     */
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("Servlet上下文销毁");
        JdbcUtil.destroyDataSource();
    }
}
5. JDBC工具类(配合监听器使用)

封装Druid连接池操作,提供查询和增删改方法:

java 复制代码
package com.jdbc;

import com.alibaba.druid.pool.DruidDataSource;
import com.handler.ResultHandler;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

public class JdbcUtil {
    private static final DruidDataSource dataSource = new DruidDataSource();

    // 初始化数据源
    public static void initDataSource(Properties props) {
        dataSource.configFromPropety(props);
    }

    // 关闭数据源
    public static void destroyDataSource() {
        dataSource.close();
    }


    // 封装的万能查询
    public static <T> T query(String sql, ResultHandler<T> handler, Object... params) {

        try {
            Connection conn = dataSource.getConnection();
            // 预处理SQL(防注入)
            PreparedStatement ps = conn.prepareStatement(sql);
            // 设置SQL参数(JDBC参数索引从1开始)
            if (params != null && params.length > 0) {
                for (int i = 0; i < params.length; i++) {
                    ps.setObject(i + 1, params[i]);
                }
            }
            ResultSet rs = ps.executeQuery();
            T t = handler.handle(rs);
            rs.close();
            ps.close();
            conn.close();
            return t;
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static int update(String sql, Object... params) throws SQLException {
        Connection conn = null;
        try {
            conn = dataSource.getConnection();
            PreparedStatement ps = conn.prepareStatement(sql);
            if (params != null && params.length > 0) {
                for (int i = 0; i < params.length; i++) {
                    ps.setObject(i + 1, params[i]);
                }
            }
             // 执行操作,返回受影响行数
            int affectedRows = ps.executeUpdate();
            //conn.commit();
            ps.close();
            conn.close();
            return affectedRows;
        } catch (SQLException e) {
            e.printStackTrace();
            if(conn != null){
                conn.close();
            }
        }
        return -1;
    }
}
6. 监听器运行效果

开启服务器和关闭服务器,查看控制台消息:

-->由此可以看出,Servelt上下文监听器可以读取到上下文参数,这些参数可以用来配置开发中所需要的环境。


第二部分:RBAC权限模型完整实现🐦‍🔥

一、RBAC模型核心概念🤔

1. 什么是RBAC?

RBAC(Role-Based Access Control,基于角色的访问控制)是一种灵活的权限管理模型,核心思想是通过角色关联用户与权限**,而非直接给用户分配权限,降低权限管理复杂度。**

RBAC的设计主要是控制服务器端的资源访问。RBAC怎么与用户建立联系?服务器感知用户是通过session来感知的,因此,RBAC的实现需要与session配合。前提是用户需要登录,登录后将用户信息存储在session中,这样才能在session中获取用户的信息

RBAC模型简单结构图示意:

2. 核心术语与关系
  • 用户(User):系统操作者(如"李四""张三");
  • 角色(Role):权限的集合(如"管理员""普通用户");
  • 权限(Permission):访问资源的资格(如"查询学生""修改学生"),与URL绑定;
  • 资源(Resource):系统可访问的内容(服务器上的一切数据都是资源,比如静态文件,查询的动态数据等)。
3. 核心关系示例
  1. 用户 ↔ 角色(多对多):通过user_role表关联;
  2. 角色 ↔ 权限(多对多):通过role_permission表关联;
  3. 权限 ↔ 资源(一对一):permission表存储权限与URL的映射。

二、RBAC模型实现步骤

1. 数据库设计(5张核心表)
sql 复制代码
-- 1. 创建用户表(user)
CREATE TABLE `user` (
  `username` VARCHAR(50) NOT NULL COMMENT '用户名(主键)',
  `password` VARCHAR(50) NOT NULL COMMENT '密码',
  `name` VARCHAR(50) COMMENT '用户姓名',
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 2. 创建角色表(role)
CREATE TABLE `role` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT '角色ID(主键)',
  `name` VARCHAR(50) NOT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_role_name` (`name`) -- 角色名称唯一
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 3. 创建权限表(permission)
CREATE TABLE `permission` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT '权限ID(主键)',
  `name` VARCHAR(50) NOT NULL COMMENT '权限名称',
  `url` VARCHAR(50) COMMENT '权限对应的URL路径',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_permission_name` (`name`) -- 权限名称唯一
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 4. 创建用户-角色关联表(user_role)【多对多关系】
CREATE TABLE `user_role` (
  `username` VARCHAR(50) NOT NULL COMMENT '关联用户表的username',
  `role_id` INT NOT NULL COMMENT '关联角色表的id',
  PRIMARY KEY (`username`, `role_id`), -- 复合主键,避免重复关联
  -- 外键约束:关联user表的username
  FOREIGN KEY (`username`) REFERENCES `user`(`username`) ON DELETE CASCADE,
  -- 外键约束:关联role表的id
  FOREIGN KEY (`role_id`) REFERENCES `role`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 5. 创建角色-权限关联表(role_permission)【多对多关系】
CREATE TABLE `role_permission` (
  `role_id` INT NOT NULL COMMENT '关联角色表的id',
  `permission_id` INT NOT NULL COMMENT '关联权限表的id',
  PRIMARY KEY (`role_id`, `permission_id`), -- 复合主键,避免重复关联
  -- 外键约束:关联role表的id
  FOREIGN KEY (`role_id`) REFERENCES `role`(`id`) ON DELETE CASCADE,
  -- 外键约束:关联permission表的id
  FOREIGN KEY (`permission_id`) REFERENCES `permission`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

五张数据表之间的物理关系表如下图所示:

2. 核心工具:结果集处理器(ResultHandler)

将数据库查询结果(ResultSet)转换为Java对象,简化数据映射:

java 复制代码
// ResultHandler接口
package com.handler;

import java.sql.ResultSet;
import java.sql.SQLException;

//对查询的结果集进行处理的接口,具体怎么处理需要用户来实现

public interface ResultHandler<T> {

    T handle(ResultSet rs) throws SQLException;

}


// 单个结果处理器(适配:用户查询、权限计数)
package com.handler;

import org.apache.commons.beanutils.BeanUtils;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

public class SingleResultHandler<T> implements ResultHandler {
    private Class<T> clazz;

    public SingleResultHandler(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public Object handle(ResultSet rs) throws SQLException {
        int count = 0;
        while (rs.next()) {
            count++;
            if (count > 1) throw new RuntimeException("查询结果不唯一");
            try {
                // 处理基础类型/包装类(如COUNT(*)返回的Integer)
                if (clazz.isPrimitive() || Integer.class == clazz || Long.class == clazz) {
                    return rs.getObject(1, clazz);
                }
                // 处理复杂对象(如User)
                T t = clazz.newInstance();
                Map<String, Object> values = new HashMap<>();
                ResultSetMetaData rsmd = rs.getMetaData();
                for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                    values.put(rsmd.getColumnLabel(i), rs.getObject(i));
                }
                // 使用工具类对我们的对象的属性进行注入
                BeanUtils.populate(t, values);
                return t;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}


// 多个结果处理器(适配:学生列表查询)
package com.handler;

import org.apache.commons.beanutils.BeanUtils;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MultiResultHandler<T> implements ResultHandler<List<T>> {
    private Class<T> clazz;

    public MultiResultHandler(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public List<T> handle(ResultSet rs) throws SQLException {
        List<T> dataList = new ArrayList<>();
        while (rs.next()) {
            try {
                T t = clazz.newInstance();
                Map<String, Object> values = new HashMap<>();
                ResultSetMetaData rsmd = rs.getMetaData();
                for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                    values.put(rsmd.getColumnLabel(i), rs.getObject(i));
                }
                BeanUtils.populate(t, values);
                dataList.add(t);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return dataList;
    }
}
3. 数据访问层(DAO)

实现用户、学生、权限相关的数据库操作:

java 复制代码
// 用户DAO:处理用户查询与权限计数
public class UserDaoImpl implements UserDao {
    // 根据用户名查询用户(登录用)
    @Override
    public User getUserByUsername(String username) {
        String sql = "SELECT username,password,name FROM user WHERE username=?";
        return (User) JdbcUtil.query(sql, new SingleResultHandler<>(User.class), username);
    }

    // 查询用户拥有目标URL的权限数量(核心权限校验SQL)
    @Override
    public Integer getUrlCount(String username, String url) {
        String sql = "SELECT COUNT(*) FROM user_role a " +
                     "INNER JOIN role b ON a.role_id = b.id " +
                     "INNER JOIN role_permission c ON b.id = c.role_id " +
                     "INNER JOIN permission d ON c.permission_id = d.id " +
                     "WHERE a.username=? AND d.url=?";
        return (Integer) JdbcUtil.query(sql, new SingleResultHandler<>(Integer.class), username, url);
    }
}

// 学生DAO:处理学生查询与修改
public class StudentDaoImpl implements StudentDao {
    @Override
    public List<Student> searchStudents() {
        String sql = "SELECT id,name,sex,age FROM student";
        return JdbcUtil.query(sql, new MultiResultHandler<>(Student.class));
    }

    @Override
    public int updateStudent(String id, String name, String sex, String age) throws SQLException {
        String sql = "UPDATE student SET name=?, sex=?, age=? WHERE id=?";
        return JdbcUtil.update(sql, name, sex, age, id);
    }
}
4. 业务逻辑层(Service)

封装登录校验与权限校验核心逻辑:

java 复制代码
// 用户Service:登录+权限校验
public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl();

    /**
     * 登录校验:1-成功,-1-用户不存在,0-密码错误
     */
    @Override
    public int login(String username, String password) {
        User user = userDao.getUserByUsername(username);
        if (user == null) return -1;
        return password.equals(user.getPassword()) ? 1 : 0;
    }

    /**
     * 权限校验:true-有权限,false-无权限
     */
    @Override
    public boolean hasPermission(String username, String url) {
        Integer count = userDao.getUrlCount(username, url);
        return count != null && count > 0; // 处理null避免空指针
    }
}

// 学生Service:业务逻辑封装
public class StudentServiceImpl implements StudentService {
    private StudentDao studentDao = new StudentDaoImpl();

    @Override
    public List<Student> searchStudents() {
        return studentDao.searchStudents();
    }

    @Override
    public int updateStudent(String id, String name, String sex, String age) throws SQLException {
        return studentDao.updateStudent(id, name, sex, age);
    }
}
5. 核心控制:权限过滤器(Filter)

拦截所有请求,实现登录校验与权限校验,是RBAC模型的核心:

java 复制代码
package com.filter;

import com.service.UserService;
import com.service.impl.UserServiceImpl;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * RBAC权限过滤器:拦截所有请求,校验登录与权限
 */
@WebFilter("/*")
public class PermissionFilter extends HttpFilter {
    private UserService userService = new UserServiceImpl();

    @Override
    protected void doFilter(HttpServletRequest request,
                            HttpServletResponse response,
                            FilterChain chain) throws IOException, ServletException {
        // 1. 统一处理URI(去掉项目前缀、参数、末尾斜杠)
        String requestURI = request.getRequestURI();
        String contextPath = request.getContextPath();
        String uri = requestURI.replace(contextPath, "");
        uri = uri.split("\\?")[0]; // 去掉查询参数
        if (uri.endsWith("/") && uri.length() > 1) {
            uri = uri.substring(0, uri.length() - 1); // 去掉末尾斜杠
        }

        // 2. 放行无需权限的资源
        if ("/".equals(uri) || "/login".equals(uri) || "/login.jsp".equals(uri) || uri.startsWith("/js/")) {
            chain.doFilter(request, response);
            return;
        }

        // 3. 登录校验:未登录则拦截
        HttpSession session = request.getSession();
        String username = (String) session.getAttribute("username");
        if (username == null) {
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter writer = response.getWriter();
            writer.print("请先登录!");
            writer.flush();
            writer.close();
            return;
        }

        // 4. 权限校验:无权限则拦截
        if (userService.hasPermission(username, uri)) {
            chain.doFilter(request, response); // 有权限,放行
        } else {
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter writer = response.getWriter();
            writer.print("没有访问权限!");
            writer.flush();
            writer.close();
        }
    }
}
6. 控制层(Servlet)

处理前端请求,对接业务逻辑层:

java 复制代码
// 登录Servlet
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        int result = userService.login(username, password);
        if (result == 1) {
            req.getSession().setAttribute("username", username); // 登录成功存Session
        }
        resp.getWriter().print(result);
    }
}

// 查询学生Servlet
@WebServlet("/search")
public class SearchServlet extends HttpServlet {
    private StudentService studentService = new StudentServiceImpl();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<Student> students = studentService.searchStudents();
        resp.setCharacterEncoding("UTF-8");
        resp.getWriter().print(JSONObject.toJSON(students));
    }
}

// 修改学生Servlet
@WebServlet("/update")
public class UpdateServlet extends HttpServlet {
    private StudentService studentService = new StudentServiceImpl();

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String id = req.getParameter("id");
        String name = req.getParameter("name");
        String sex = req.getParameter("sex");
        String age = req.getParameter("age");
        try {
            int result = studentService.updateStudent(id, name, sex, age);
            resp.getWriter().print(result);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
7. 前端页面(JSP+AJAX)
jsp 复制代码
<!-- 登录页面(login.jsp) -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录</title>
</head>
<body>
    <h3>RBAC权限系统 - 登录</h3>
    <input type="text" id="username" placeholder="用户名">
    <input type="password" id="password" placeholder="密码">
    <input type="button" value="登录" id="loginBtn">

    <script src="js/jquery-3.6.0.js"></script>
    <script>
        $("#loginBtn").click(function(){
            $.ajax({
                url: "login",
                type: "post",
                data: {username: $("#username").val(), password: $("#password").val()},
                success: function(resp){
                    if(resp === "1") window.location.href = "main.jsp";
                    else if(resp === "-1") alert("账号不存在");
                    else alert("密码错误");
                }
            });
        });
    </script>
</body>
</html>

<!-- 主页面(main.jsp) -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>主页面</title>
</head>
<body>
    <h3>系统主页面</h3>
    <input type="button" value="查询学生" id="searchBtn">
    <input type="button" value="修改学生" id="updateBtn">

    <script src="js/jquery-3.6.0.js"></script>
    <script>
        $("#searchBtn").click(function(){
            $.ajax({url: "search", type: "get", success: function(resp){console.log("学生列表:", resp);}});
        });

        $("#updateBtn").click(function(){
            $.ajax({
                url: "update",
                type: "post",
                data: {id:3, name:"王五", sex:"女", age:30},
                success: function(resp){alert(resp === "1" ? "修改成功" : "修改失败");}
            });
        });
    </script>
</body>
</html>

三、上面的RBAC模型运行流程

简单逻辑
  1. 用户访问login.jsp,输入用户名密码登录;
  2. 登录成功后,用户名存入Session;
  3. 用户点击"查询/修改"按钮,请求被PermissionFilter拦截;
  4. 过滤器提取URI,先校验登录状态(Session是否有用户名);
  5. 登录通过后,查询用户-角色-权限关联关系,校验是否有权限访问该URI;
  6. 有权限则放行请求,执行Servlet逻辑;无权限则返回"没有访问权限"。
深入探寻🐦‍🔥🤔🐦‍🔥
一、整体架构概览(从上到下分层)
复制代码
前端页面(JSP+AJAX)→ 权限过滤器(Filter)→ 控制层(Servlet)→ 业务层(Service)→ 数据访问层(DAO)→ 工具层(JdbcUtil+ResultHandler)→ 数据库
  • 每一层只做自己的事(单一职责),上层依赖下层,下层不感知上层;
  • 核心核心是 PermissionFilter 过滤器,它拦截所有请求,是权限控制的"大门";
  • 关键工具是 ResultHandler 结果集处理器,简化数据库结果到 Java 对象的映射,避免重复代码。
二、各模块运行逻辑拆解(按请求流程顺序)
1. 前端页面:发起请求的入口
  • 核心页面:login.jsp(登录)、main.jsp(系统主页面);
  • 交互方式:通过 AJAX(jQuery) 发起异步请求(登录、查询学生、修改学生),不刷新页面就能获取结果;
  • 关键逻辑:
    • 登录页面:输入用户名/密码,点击登录按钮 → 向 /login 接口发 POST 请求;
    • 主页面:登录成功后跳转,点击"查询学生"→ 向 /search 发 GET 请求,点击"修改学生"→ 向 /update 发 POST 请求。
2. 权限过滤器(PermissionFilter)

这是整个系统的核心控制层,拦截所有请求(@WebFilter("/*")),执行「登录校验」和「权限校验」,决定请求是否能放行:

  • 步骤1:统一处理请求 URI(去掉项目前缀、参数、末尾斜杠),避免因 URL 格式不一致导致权限校验失败(比如 /search/search?name=xxx 视为同一个接口);
  • 步骤2:放行"白名单"资源(无需登录/权限的资源):
    • 根路径 /、登录接口 /login、登录页面 /login.jsp、静态资源 /js/(jQuery 文件);
  • 步骤3:登录校验(未登录则拦截):
    • 从 Session 中获取 username(登录成功后会存入);
    • 若 Session 中没有 username → 返回"请先登录",拒绝放行;
  • 步骤4:权限校验(已登录但无权限则拦截):
    • 调用 UserService.hasPermission(username, uri),检查当前用户是否有访问目标接口(URI)的权限;
    • 有权限 → 放行请求,继续执行后续流程;
    • 无权限 → 返回"没有访问权限",拒绝放行。
3. 控制层(Servlet):请求的分发

接收前端请求,对接业务层,处理请求参数和响应结果:

  • 每个 Servlet 对应一个核心接口,通过 @WebServlet("/xxx") 标注接口路径;
  • 核心逻辑:
    • LoginServlet:接收 /login 请求 → 获取用户名/密码参数 → 调用 UserService.login() 校验 → 返回结果(1=成功,-1=用户不存在,0=密码错误);
    • SearchServlet:接收 /search 请求 → 调用 StudentService.searchStudents() 获取学生列表 → 转 JSON 格式返回给前端;
    • UpdateServlet:接收 /update 请求 → 获取学生 ID/姓名/性别/年龄参数 → 调用 StudentService.updateStudent() 执行修改 → 返回受影响行数(1=成功,0=失败)。
4. 业务层(Service):核心逻辑实现

封装业务规则,调用 DAO 层操作数据库,不直接接触 JDBC:

  • 两大核心业务:
    • (1)用户相关(UserServiceImpl):
      • 登录校验 login():调用 UserDao.getUserByUsername() 查询用户 → 对比密码 → 返回结果;
      • 权限校验 hasPermission():调用 UserDao.getUrlCount() 查询用户拥有目标 URL 的权限数量 → 数量>0 则有权限;
    • (2)学生相关(StudentServiceImpl):
      • 查询学生 searchStudents():直接调用 StudentDao.searchStudents() 获取列表;
      • 修改学生 updateStudent():直接调用 StudentDao.updateStudent() 执行修改,抛出异常给 Servlet 处理。
5. 数据访问层(DAO):数据库操作执行

调用 JdbcUtil 工具类,执行 SQL 语句,是唯一直接操作数据库的层:

  • 每个 DAO 对应一类数据操作(用户、学生);
  • 核心逻辑:
    • UserDaoImpl
      • getUserByUsername():执行查询 SQL(根据用户名查用户信息)→ 使用 SingleResultHandler 将结果映射为 User 对象;
      • getUrlCount():执行多表关联的 COUNT SQL(用户→角色→权限→URL)→ 使用 SingleResultHandler 将结果映射为 Integer(权限数量);
    • StudentDaoImpl
      • searchStudents():执行查询 SQL(查所有学生)→ 使用 MultiResultHandler 将结果映射为 List<Student>
      • updateStudent():执行更新 SQL(修改学生信息)→ 调用 JdbcUtil.update() 返回受影响行数。
6. 工具层:简化代码量
  • (1)JdbcUtil:基于 Druid 连接池的 JDBC 工具类,封装了「数据源初始化/销毁」「查询(query)」「增删改(update)」通用逻辑,避免重复写 JDBC 样板代码;
  • (2)ResultHandler 接口及实现类:
    • 核心作用:将数据库返回的 ResultSet(结果集)自动映射为 Java 对象(如 UserList<Student>Integer);
    • SingleResultHandler:映射为单个对象(如登录查询的 User、权限计数的 Integer),多行会抛异常;
    • MultiResultHandler:映射为对象列表 (如学生列表 List<Student>);
    • 底层依赖 BeanUtils 工具类,自动将"列名-列值"映射到 JavaBean 的属性(需保证数据库列名与 JavaBean 属性名一致)。
7. 数据库:

基于 RBAC 模型设计 5 张表(用户、角色、权限、用户-角色关联、角色-权限关联),通过多表关联实现权限控制:

  • 核心关联逻辑:用户 ↔ 角色(多对多)、角色 ↔ 权限(多对多);
  • 权限校验的 SQL 核心:通过 INNER JOIN 串联 4 张表(user_role → role → role_permission → permission),筛选出"指定用户"且"指定 URL"的权限记录,统计数量。
三、核心流程示例(以"查询学生"为例)
  1. 前端:用户登录成功后,在 main.jsp 点击"查询学生"→ 向 /search 发 GET 请求;
  2. 过滤器:拦截 /search 请求 → 检查 Session 有 username(已登录)→ 调用 hasPermission() 校验权限 → 有权限,放行;
  3. 控制层:SearchServlet 接收请求 → 调用 StudentService.searchStudents()
  4. 业务层:StudentServiceImpl 调用 StudentDao.searchStudents()
  5. 数据访问层:StudentDaoImpl 执行 SQL(SELECT id,name,sex,age FROM student)→ 用 MultiResultHandlerResultSet 映射为 List<Student>
  6. 工具层:JdbcUtil 从 Druid 连接池获取连接 → 执行 SQL → 返回结果集给 DAO;
  7. 数据库:执行 SQL 查询学生数据 → 返回结果集;
  8. 反向返回:List<Student> → Service → Servlet → 转 JSON → 前端 → 控制台打印学生列表。

如果我的内容对你有帮助,请 点赞 , 评论 , 收藏 。创作不易,大家的支持就是我坚持下去的动力!

相关推荐
num_killer9 小时前
小白的Langchain学习
java·python·学习·langchain
wdfk_prog10 小时前
[Linux]学习笔记系列 -- hashtable
linux·笔记·学习
期待のcode10 小时前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐10 小时前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
a程序小傲10 小时前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红10 小时前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥10 小时前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v10 小时前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地11 小时前
NIO的三个组件解决三个问题
java·后端·nio
czlczl2002092511 小时前
Guava Cache 原理与实战
java·后端·spring