Spring Security安全框架(结合RBAC思想实现自用实例模版)

        前面基于RBAC思想做了一个简单自用的权限控制框架,随后在做学校实训项目时受老师启发融合了AI服务,因为使用的Spring Boot版本是3.4,当时想着干脆一不做二不休直接把用到的框架都融入一遍做成自己的模版,实现一次升级。其中就用到Spring Security框架,但是之前也是首次接触也只是简单过滤一遍,现在有空再重新回顾一遍项目,就想着对着官方文档把常用的知识系统的再过一遍,记录一下自己的笔记。

基于角色的权限控制 (RBAC)_rbac权限-CSDN博客


一、Spring Security概述

1、什么是Spring Security?

        Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。总之,Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。

        简单来说Spring Security是一个Java的安全框架,专注于身份认证(Authentication)和授权(Authorization)。

2.核心目标与价值

Spring Security 的核心目标是解决两个问题:

  1. 认证(Who are you?):验证用户身份的合法性(例如:用户名密码是否正确、是否是通过 OAuth2 授权的第三方用户等)。
  2. 授权(What are you allowed to do?):验证通过认证的用户是否有权限执行某个操作(例如:普通用户能否访问管理员页面、能否调用某个接口等)。

        除此之外,它还提供了诸多企业级安全特性,如:会话管理、密码加密、CSRF 防护、CORS 配置、记住我功能等,且与 Spring 生态(Spring Boot、Spring Cloud 等)无缝集成,配置灵活。

二、Spring Security架构

        Spring Security的核心就是基于过滤器链(Filter Chain) 来处理 Web 层安全。

        Spring Security 对 Web 资源的保护,本质是通过一系列安全过滤器对请求进行拦截和处理。这些过滤器并非直接注册到 Servlet 容器,而是通过一个特殊的代理过滤器 FilterChainProxy 统一管理,形成「安全过滤器链」。

1.FilterChainProxy 

        FilterChainProxy 是 Spring Security 与 Servlet 容器交互的入口,它本身是一个标准的 Servlet Filter,负责将请求转发到对应的「安全过滤器链」(可配置多个过滤器链,根据请求路径匹配)。

2.SecurityFilterChain

        SecurityFilterChain 是一个接口,定义了「一组安全过滤器」和「该链适用的请求路径」。开发者通过配置类(如 SecurityConfig 中的 securityFilterChain 方法)定义 SecurityFilterChain,指定哪些过滤器生效、生效的顺序以及适用的 URL 路径。

        在上图,FilterChainProxy 决定应该使用哪个 SecurityFilterChain。只有第一个匹配的 SecurityFilterChain 被调用。如果请求的URL是 /api/messages/,它首先与 /api/** 的 SecurityFilterChain0 模式匹配,所以只有 SecurityFilterChain0 被调用,尽管它也与 SecurityFilterChainn 匹配。如果请求的URL是 /messages/,它与 /api/** 的 SecurityFilterChain0 模式不匹配,所以 FilterChainProxy 继续尝试每个 SecurityFilterChain。假设没有其他 SecurityFilterChain 实例相匹配,则调用 SecurityFilterChainn

3.Security Filter

        Security Filter 是通过 SecurityFilterChain API 插入 FilterChainProxy 中的。

        这些 filter 可以用于许多不同的目的,如 认证、 授权、 漏洞保护 等等。filter 是按照特定的顺序执行的,以保证它们在正确的时间被调用,例如,执行认证的 Filter 应该在执行授权的 Filter 之前被调用。

示例:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults())
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults())
            .formLogin(Customizer.withDefaults());
        return http.build();
    }

}

上述配置中的 Filter 顺序如下:

Filter添加者

CsrfFilter

HttpSecurity#csrf

UsernamePasswordAuthenticationFilter

HttpSecurity#formLogin

BasicAuthenticationFilter

HttpSecurity#httpBasic

AuthorizationFilter

HttpSecurity#authorizeHttpRequests

  1. 首先,调用 CsrfFilter 来防止 CSRF 攻击

  2. 其次,认证 filter 被调用以认证请求。

  3. 第三,调用 AuthorizationFilter 来授权该请求。

三、认证(Authentication)

        认证是确认用户身份的过程,Spring Security 的认证架构基于「抽象接口 + 分层委托」设计,核心接口包括 AuthenticationManagerAuthenticationProviderUserDetailsService 等。        

       Spring Security提供了多种认证机制,其中最常用的还是基于用户名密码认证:

1.认证流程

认证流程简述:

  1. 用户提交凭证:客户端提交用户名和密码至UsernamePasswordAuthenticationFilter

  2. 发起认证请求:该过滤器调用ProviderManager.authenticate()方法。

  3. 委托认证处理ProviderManager委托DaoAuthenticationProvider进行具体认证逻辑。

  4. 加载用户数据DaoAuthenticationProvider通过InMemoryUserDetailsManager加载用户信息(UserDetails)。

  5. 密码验证:使用PasswordEncoder比对提交的密码与UserDetails中的密码。

  6. 认证结果处理

    • 成功:生成包含权限信息的Authentication对象,存入SecurityContextHolder

    • 失败:抛出异常终止流程。

  7. 完成认证:后续过滤器链根据SecurityContext中的认证信息进行授权控制。

2.核心组件

1. AbstractAuthenticationProcessingFilter

  • 角色:认证的入口过滤器(拦截HTTP请求)。

  • 功能

    • 从请求中提取用户名/密码等凭证。

    • 封装为Authentication对象(如UsernamePasswordAuthenticationToken)。

    • 调用AuthenticationManager启动认证流程。

  • 常见实现

    • UsernamePasswordAuthenticationFilter:处理表单登录请求(默认/login)。


2. AuthenticationManager

  • 角色:认证的核心调度器(接口)。

  • 功能

    • 协调多个AuthenticationProvider进行认证。

    • 决定哪个Provider适合处理当前Authentication对象。

  • 常见实现

    • ProviderManager:代理一组AuthenticationProvider,按顺序尝试认证。


3. AuthenticationProvider

  • 角色:具体认证逻辑的执行者(接口)。

  • 功能

    • 校验凭证(如密码、令牌等)。

    • 返回完全填充的Authentication对象(含权限信息)。

  • 常见实现

    • DaoAuthenticationProvider:基于数据库的认证(委托UserDetailsService加载用户)。


4. UserDetailsService

  • 角色:用户数据加载器(接口)。

  • 功能

    • 根据用户名加载用户详情(如密码、权限等)。

    • 返回UserDetails对象(标准化用户数据)。

  • 常见实现

    • InMemoryUserDetailsManager:内存存储用户信息(测试用)。

    • JdbcUserDetailsManager:基于JDBC从数据库加载用户。


5. UserDetails

  • 角色:用户数据的标准化接口。

  • 关键字段

    • 用户名、密码(加密后)、权限列表(GrantedAuthority)、账户状态(是否锁定等)。

  • 作用

    • AuthenticationProvider对比凭证并生成最终的Authentication对象。


6. PasswordEncoder

  • 角色:密码编码器(接口)。

  • 功能

    • 加密用户密码(存储时)。

    • 比对提交的密码与存储的加密密码(认证时)。

  • 常见实现

    • BCryptPasswordEncoder:推荐的安全哈希算法。

四、授权(Authorization)

        授权是控制用户访问权限的过程,Spring Security 支持「Web 资源授权」(基于 URL)和「方法授权」(基于方法调用),两者共享核心授权组件。

1. Web 授权流程(以 FilterSecurityInterceptor 为例)

  1. FilterSecurityInterceptor 拦截请求,作为过滤器链的最后一环;
  2. 调用 FilterInvocationSecurityMetadataSource 获取当前 URL 所需的权限(ConfigAttribute 集合,如 [ROLE_ADMIN]);
  3. 从 SecurityContextHolder 获取当前用户的 Authentication(包含用户权限)
  4. 调用 AccessDecisionManager 的 decide 方法,传入用户权限、当前请求对象、资源所需权限;
  5. AccessDecisionManager 委托给内部的 AccessDecisionVoter 投票,根据投票结果决策:
    • 决策通过:允许请求继续访问目标资源(如 Controller 方法);
    • 决策失败:抛出 AccessDeniedException,由 ExceptionTranslationFilter 处理(返回 403)。

2. 方法授权流程(基于 AOP)

        方法授权通过 @EnableMethodSecurity 启用,底层基于 Spring AOP,核心拦截器为 MethodSecurityInterceptor。流程:

  1. 当调用带有 @PreAuthorize 等注解的方法时,MethodSecurityInterceptor 作为切面拦截方法调用;
  2. 调用 MethodSecurityMetadataSource 解析方法上的注解,获取所需权限(如 hasRole('ADMIN'));
  3. 从 SecurityContextHolder 获取当前用户的 Authentication
  4. 调用 AccessDecisionManager 进行授权决策;
  5. 决策通过:执行目标方法;决策失败:抛出 AccessDeniedException

3.总结

        以上两种授权流程都包含了一个重要点:从 SecurityContextHolder 获取当前用户的 Authentication

        因此我们授权操作要在认证通过后将用户的权限信息上传到 SecurityContextHolder 中才可以进行后序权限管理操作。

五、自定义认证失败处理器

        Spring Security提供了认证失败处理器,我们也可以自定义,通过实现AuthenticationEntryPoint接口,用于处理未通过认证的用户请求。自定义的处理器需要在SecurityFilterChain中配置。

package com.hl.campusservicesys.service.Impl;

import com.alibaba.fastjson.JSON;
import com.hl.campusservicesys.utils.Result;
import com.hl.campusservicesys.utils.WebUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 实现AuthenticationEntryPoint接口,用于处理未通过认证的用户请求
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    /**
     * 处理未通过认证的用户请求
     * <p>
     * 当用户尝试访问需要认证的资源但未提供有效认证信息时,此方法将被调用
     * 它会返回一个包含错误信息的HTTP响应,指示用户需要重新登录
     *
     * @param request 请求对象,包含用户请求的信息
     * @param response 响应对象,用于向用户发送响应
     * @param authException 认证异常,包含认证失败的原因
     * @throws IOException 如果在处理请求或发送响应时发生I/O错误
     * @throws ServletException 如果在处理请求时发生Servlet相关的错误
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 创建一个包含错误信息的ResponseResult对象
        Result result = new Result(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
        // 将ResponseResult对象转换为JSON字符串
        String json = JSON.toJSONString(result);
        // 使用WebUtils工具类将JSON字符串作为响应体发送给用户
        WebUtils.renderString(response,json);
    }
}

六、自定义访问拒绝处理器

        Spring Security提供了访问拒绝处理器,我们也可以自定义,通过实现AccessDeniedHandler接口,定义访问拒绝处理器实现类当用户已通过身份验证但没有足够的权限访问特定资源时,此处理器将被调用。自定义的处理器需要在SecurityFilterChain中配置。

package com.hl.campusservicesys.service.Impl;

import com.alibaba.fastjson.JSON;
import com.hl.campusservicesys.utils.Result;
import com.hl.campusservicesys.utils.WebUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 自定义访问拒绝处理器实现类
 * 当用户已通过身份验证但没有足够的权限访问特定资源时,此处理器将被调用
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    /**
     * 处理访问拒绝异常
     *
     * @param request        HTTP请求对象,包含请求信息
     * @param response       HTTP响应对象,用于向客户端发送响应
     * @param accessDeniedException 访问拒绝异常,包含异常信息
     * @throws IOException  如果在处理过程中发生I/O错误
     * @throws ServletException 如果在处理过程中发生Servlet错误
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 创建响应结果对象,设置状态码为403(禁止访问),并提供权限不足的错误信息
        Result result = new Result(HttpStatus.FORBIDDEN.value(), "权限不足");
        // 将响应结果对象转换为JSON字符串
        String json = JSON.toJSONString(result);
        // 使用WebUtils工具类将JSON字符串作为HTTP响应体发送给客户端
        WebUtils.renderString(response, json);
    }
}

七、基于RBAC和Spring Security的安全框架实例

        理论了解的差不多了,下面就来实现一个RBAC理论与Spring Security结合的安全框架模版实例。

说明:实现不易,参考了很多资料,做了版本统一升级,解决了很多冲突,模版自用。

流程说明:提供/login、/register和/hello接口,Security对/login、/register放行(测试认证),拦截/hello(测试权限管理)。

首先对于注册接口,提供username和roles列表参数,进行用户添加和权限分配。

其次对于登录接口,提供用户名和密码参数,先进行自定义认证操作,认证通过生成token并将详情存入Redis以便后续使用,返回响应。

最后对于hello接口,请求头携带token首先经过UsernamePasswordAuthenticationFilter进行认证,然后进行JwtAuthenticationTokenFilter进行Token认证,并将权限信息存入SecurityContextHolder,以便后序使用。

1.引入依赖

技术栈:SpringBoot3.4.4、Redis、Redisson、Mybatis-plus、jwt、validation 

pom文件如下,JDK17

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.hl</groupId>
    <artifactId>StuKnowManageSys</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>StuKnowManageSys</name>
    <description>StuKnowManageSys</description>
    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>3.4.4</spring-boot.version>
        <mybatis-plus.version>3.5.10.1</mybatis-plus.version>
        <spring-ai.version>1.0.0</spring-ai.version>
        <jjwt.version>0.12.6</jjwt.version>
        <fastjson.version>1.2.76</fastjson.version>
        <redisson.version>3.16.2</redisson.version>
        <hutool.version>5.8.11</hutool.version>
        <knife4j.version>4.5.0</knife4j.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--使用的是Java 9及以上的版本,则需要添加JAXB相关依赖,支持 Java 中的 XML 数据绑定 -->
        <dependency>
            <groupId>jakarta.xml.bind</groupId>
            <artifactId>jakarta.xml.bind-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- SpringSecurity -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>
        <!-- jdk 11+ 引入可选模块 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-jsqlparser</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!-- jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Redisson 依赖 -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>${redisson.version}</version>
        </dependency>
        <!-- validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- hutool -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <!-- knife4j -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <!-- Spring Boot 依赖管理 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- MyBatis-Plus BOM 管理 -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-bom</artifactId>
                <version>${mybatis-plus.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <compilerArgs>
                        <compilerArg>-parameters</compilerArg>
                    </compilerArgs>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.hl.stuknowmanagesys.StuKnowManageSysApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

2.配置yml文件

RFC 7518标准的HS256算法密钥长度至少要256位:

server:
  port: 8080

spring:
  jmx:
    enabled: false
  application:
    name: StuKnowManageSys
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm?allowMultiQueries=true&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: 1234
  data:
    redis:
      # Redis服务器地址
      host: 127.0.0.1
      # Redis服务器连接端口
      port: 6379
      # Redis服务器连接密码
      password:
      # Redis数据库索引
      database: 3
      # 连接超时时间(毫秒)
      timeout: 30000
      lettuce:
        pool:
          max-active: 50
          max-wait: -1
          max-idle: 50
          min-idle: 1

#spring事务管理日志
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug
#mybatisplus
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true # 开启下划线和驼峰的映射
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #配置mybatis的日志,指定输出到控制台
# knife4j配置
knife4j:
  enable: true
  setting:
    language: zh_cn

hl:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    user-secret-key: xuanyu1122-CampusServiceSys-secret-key
    # 设置jwt过期时间(分钟)
    user-ttl: 60
    # 设置前端传递过来的令牌名称
    user-token-name: token

3.表结构

因为使用RBAC思想,包含5张表:

SQL脚本如下:

-- MySQL dump 10.13  Distrib 8.0.35, for Win64 (x86_64)
--
-- Host: 127.0.0.1    Database: ssm
-- ------------------------------------------------------
-- Server version	8.0.35

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `permission`
--

DROP TABLE IF EXISTS `permission`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permission` (
  `permission_id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限ID',
  `permission_name` varchar(50) NOT NULL COMMENT '权限名称',
  PRIMARY KEY (`permission_id`),
  UNIQUE KEY `permission_pk_2` (`permission_name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `permission`
--

LOCK TABLES `permission` WRITE;
/*!40000 ALTER TABLE `permission` DISABLE KEYS */;
INSERT INTO `permission` VALUES (1,'look1'),(2,'look2'),(3,'look3');
/*!40000 ALTER TABLE `permission` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `role`
--

DROP TABLE IF EXISTS `role`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role` (
  `role_id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(20) NOT NULL COMMENT '角色名称',
  PRIMARY KEY (`role_id`),
  UNIQUE KEY `role_pk_2` (`role_name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `role`
--

LOCK TABLES `role` WRITE;
/*!40000 ALTER TABLE `role` DISABLE KEYS */;
INSERT INTO `role` VALUES (1,'admin'),(3,'student'),(2,'teacher');
/*!40000 ALTER TABLE `role` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `role_permission`
--

DROP TABLE IF EXISTS `role_permission`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role_permission` (
  `role_permission_id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `permission_id` bigint NOT NULL COMMENT '权限ID',
  PRIMARY KEY (`role_permission_id`),
  UNIQUE KEY `role_permission_pk_2` (`role_id`,`permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `role_permission`
--

LOCK TABLES `role_permission` WRITE;
/*!40000 ALTER TABLE `role_permission` DISABLE KEYS */;
INSERT INTO `role_permission` VALUES (1,1,1),(2,1,2),(3,1,3);
/*!40000 ALTER TABLE `role_permission` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `user`
--

DROP TABLE IF EXISTS `user`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user` (
  `user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(255) NOT NULL COMMENT '密码',
  `avatar` varchar(255) DEFAULT NULL,
  `status` char(1) NOT NULL DEFAULT '1' COMMENT '用户状态(0封禁1正常)',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `user_pk_2` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `user`
--

LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;
INSERT INTO `user` VALUES (1,'hl','$2a$10$UnR.yOwCUefsbA2HJKu33..REVZ52DBSWhhefT/skbnPxYEVzukFO','/local-plus/avatar/default.jpg','1','2025-08-13 18:07:40');
/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `user_role`
--

DROP TABLE IF EXISTS `user_role`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user_role` (
  `user_role_id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` bigint NOT NULL COMMENT '角色ID',
  `role_id` bigint NOT NULL COMMENT '用户ID',
  PRIMARY KEY (`user_role_id`),
  UNIQUE KEY `user_role_pk_2` (`user_id`,`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `user_role`
--

LOCK TABLES `user_role` WRITE;
/*!40000 ALTER TABLE `user_role` DISABLE KEYS */;
INSERT INTO `user_role` VALUES (1,1,1);
/*!40000 ALTER TABLE `user_role` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2025-08-13 18:57:09

4.实体类

除了基础表的5个实体,还需使用UserDTO、LoginUser(UserDetails实现类)和UserVO。

重要的是LoginUser,用于权限转换:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    /**
     * 用户ID,主键,自增
     */
    private Long userId;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     * */
    private String password;

    /**
     * 用户头像URL
     */
    private String avatar;

    /**
     * token
     * */
    private String token;

    /**
     * 用户角色集合
     * */
    private List<String> roles;

    /**
     * 权限集合
     * */
    private List<String> permissions;

    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    @JSONField(serialize = false)
    private boolean accountNonExpired = true;

    @JSONField(serialize = false)
    private boolean accountNonLocked = true;

    @JSONField(serialize = false)
    private boolean credentialsNonExpired = true;

    @JSONField(serialize = false)
    private boolean enabled = true;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) {
            return authorities;
        }

        // 转换权限
        List<GrantedAuthority> permissionAuthorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        // 转换角色(注意添加ROLE_前缀)
        List<GrantedAuthority> roleAuthorities = roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());

        // 合并权限和角色
        authorities = new ArrayList<>();
        authorities.addAll(permissionAuthorities);
        authorities.addAll(roleAuthorities);

        return authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

5.配置类配置

对Redis、Redission、Validation、Cors和Security进行配置:

重要的是Security,其中包含一些自定义的组件配置和过滤器链配置:

package com.hl.stuknowmanagesys.config;

import com.hl.stuknowmanagesys.filter.CustomAuthenticationProvider;
import com.hl.stuknowmanagesys.filter.JwtAuthenticationTokenFilter;
import com.hl.stuknowmanagesys.service.impl.UserDetailsServiceImpl;
import com.hl.stuknowmanagesys.service.impl.AccessDeniedHandlerImpl;
import com.hl.stuknowmanagesys.service.impl.AuthenticationEntryPointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new AuthenticationEntryPointImpl();
    }
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new AccessDeniedHandlerImpl();
    }

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public CustomAuthenticationProvider customAuthenticationProvider(UserDetailsService userDetailsService) {
        return new CustomAuthenticationProvider((UserDetailsServiceImpl) userDetailsService);
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthenticationProvider customAuthenticationProvider) throws Exception {

//        若要让requestMatchers匹配所有地址,可使用"/**"
        http
                // 使用新的csrf配置方式
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/login","/register").permitAll()
                        .requestMatchers("/doc.html","/webjars/**","/swagger-resources/**","/v3/**").permitAll()

                        .anyRequest().authenticated()
                )
                // 配置认证提供者
                .authenticationProvider(customAuthenticationProvider)
                // 配置异常处理器
                .exceptionHandling(exceptions -> exceptions
                        .authenticationEntryPoint(authenticationEntryPoint())
                        .accessDeniedHandler(accessDeniedHandler())
                );

        http.addFilterAfter(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

}

6.自定义UserDetailsService

进行用户数据加载:

package com.hl.stuknowmanagesys.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hl.stuknowmanagesys.entity.pojo.User;
import com.hl.stuknowmanagesys.entity.vo.LoginUser;
import com.hl.stuknowmanagesys.exception.LoginFailedException;
import com.hl.stuknowmanagesys.mapper.PermissionMapper;
import com.hl.stuknowmanagesys.mapper.RoleMapper;
import com.hl.stuknowmanagesys.mapper.UserMapper;
import jakarta.annotation.Resource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    // 用户信息Mapper
    @Resource
    private UserMapper userMapper;
    @Resource
    private PermissionMapper permissionMapper;
    @Resource
    private RoleMapper roleMapper;

    /**
     * 根据账号加载用户详细信息
     * @param username 用户名
     * @return UserDetails对象,包含用户信息和权限信息
     * @throws UsernameNotFoundException 如果用户不存在,则抛出异常
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername, username)
                .select(User::getUserId, User::getUsername, User::getPassword, User::getStatus);
        User user = userMapper.selectOne(wrapper);
        // 用户不存在,抛出异常
        if(Objects.isNull(user)){
            throw new LoginFailedException("用户名或密码错误");
        }
        // 用户被禁用,抛出异常
        if(user.getStatus().equals("0")){
            throw new LoginFailedException("用户账号已被禁用");
        }
        // 查询拥有的角色
        List<String> roles = roleMapper.selectRoleNamesByUserId(user.getUserId());
        // 查询拥有的权限
        List<String> permissions = permissionMapper.selectPermissionNamesByUserId(user.getUserId());
        // 返回用户详细信息
        LoginUser loginUser = LoginUser.builder()
                .userId(user.getUserId())
                .username(user.getUsername())
                .avatar(user.getAvatar())
                .password(user.getPassword())
                .roles(roles)
                .permissions(permissions)
                .build();
        return loginUser;
    }

}



7.自定义认证提供者AuthenticationProvider

执行具体的认证逻辑:

package com.hl.stuknowmanagesys.filter;

import com.hl.stuknowmanagesys.service.impl.UserDetailsServiceImpl;
import com.hl.stuknowmanagesys.utils.SecurityUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

/**
 * 自定义认证提供者,支持通过账号进行认证
 */
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsServiceImpl userDetailsService;

    public CustomAuthenticationProvider(UserDetailsServiceImpl userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    /**
     * 根据提供的认证信息对用户进行认证
     *
     * @param authentication 包含用户凭证的认证信息
     * @return 如果认证成功,返回包含用户详情的认证对象
     * @throws AuthenticationException 如果认证失败,抛出认证异常
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // principal(账号)和credentials(密码)来验证用户身份
        String principal = authentication.getName();
        String credentials = (String) authentication.getCredentials();
        // 加载与该用户名关联的用户详细信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(principal);
        // 验证用户提供的credentials是否与用户密码匹配
        if (SecurityUtils.matchesPassword(credentials, userDetails.getPassword())) {
            // 认证成功,返回包含用户信息和权限的Authentication对象
            return new UsernamePasswordAuthenticationToken(userDetails, credentials, userDetails.getAuthorities());
        }
        // 如果以上认证逻辑均未通过,抛出认证异常
        throw new BadCredentialsException("无效的凭证");
    }

    /**
     * 检查是否支持给定的认证类型
     *
     * @param authentication 要检查的认证类型
     * @return 如果支持该认证类型则返回true,否则返回false
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

8.jwt过滤器

将其配置在认证过滤器UsernamePasswordAuthenticationFilter之后执行:

package com.hl.stuknowmanagesys.filter;

import com.hl.stuknowmanagesys.constant.Constants;
import com.hl.stuknowmanagesys.constant.RedisConstant;
import com.hl.stuknowmanagesys.entity.vo.LoginUser;
import com.hl.stuknowmanagesys.exception.TokenException;
import com.hl.stuknowmanagesys.utils.JwtProperties;
import com.hl.stuknowmanagesys.utils.JwtUtil;
import com.hl.stuknowmanagesys.utils.RedisCache;
import io.jsonwebtoken.Claims;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;
    @Resource
    private JwtProperties jwtProperties;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("当前请求的URL:{}",request.getRequestURL());
        //获取token
        String token = request.getHeader(jwtProperties.getUserTokenName());
        if (!StringUtils.hasText(token)) {
            //如果没有token 则进行下次的filter 放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        Long userId;
        try {
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(),token);
            userId = Long.valueOf(claims.get(Constants.JWT_USERID).toString());
        } catch (Exception e) {
            // 设置响应状态码为 401,表示未授权
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            // 设置响应内容类型为 JSON
            response.setContentType("application/json;charset=UTF-8");
            // 构造异常信息的 JSON 字符串
            String errorJson = "{" +
                    "\"code\":401," +
                    "\"message\": \""+e.getMessage()+"\"}";
            // 获取响应输出流
            response.getWriter().write(errorJson);
            return;
        }
        String redisKey = RedisConstant.LOGIN_TOKEN_KEY + userId;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        // 检查token是否过期,若剩余过期时间小于原来时间的1/5,则刷新token
        if (Objects.isNull(loginUser)) {
            throw new TokenException("登录过期请重新登录");
        } else {
            // 检查Redis中token的剩余过期时间
            Long expireTime = redisCache.getExpire(redisKey);
            if (expireTime != null && expireTime > 0) {
                // 如果剩余时间小于原来时间的1/5,则刷新token过期时间
                long originalTtl = jwtProperties.getUserTtl() * 60; // 转换为秒
                if (expireTime < originalTtl / 5) {
                    // 刷新token过期时间
                    redisCache.expire(redisKey, jwtProperties.getUserTtl(), TimeUnit.MINUTES);
                }
            }
        }

        //存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }


}

9.启动类

开启EnableMethodSecurity权限管理和@EnableScheduling接口文档:

@SpringBootApplication
@EnableMethodSecurity
@EnableScheduling
@MapperScan("com.hl.stuknowmanagesys.mapper")
public class StuKnowManageSysApplication {
    public static void main(String[] args) {
        SpringApplication.run(StuKnowManageSysApplication.class, args);
    }
}

10.register接口实现

未登录测试时先将@PreAuthorize("hasRole('admin')")注释,只有admin权限才可以执行此操作:

package com.hl.stuknowmanagesys.controller;

import com.hl.stuknowmanagesys.entity.dto.UserDTO;
import com.hl.stuknowmanagesys.service.UserService;
import com.hl.stuknowmanagesys.utils.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "用户注册接口")
@Slf4j
@RestController
@RequestMapping("/register")
public class RegisterController {

    @Resource
    private UserService userService;

    /**
     * 注册用户
     */
    @Operation(summary = "用户注册", description = "请求参数{必须:username、roles}")
    @PreAuthorize("hasRole('admin')")
    @PostMapping
    public Result register(@RequestBody @Validated(UserDTO.Register.class) UserDTO userDTO) {
        log.info("用户注册:{}", userDTO);
        return userService.register(userDTO);
    }
}

Service如下,同时进行权限分配:

@Override
    @Transactional(rollbackFor = Exception.class)
    public Result register(UserDTO userDTO) {

        // 1. 判断用户是否存在
        RBloomFilter<Long> userBloomFilter = userBloomFilterFactory.createRBF(RedisConstant.USER);
        if (userBloomFilter.contains(userDTO.getUserId())) {
            throw new RegisterFailedException("用户已存在");
        }

        // 2. 创建用户
        User newUser = User.builder()
                .username(userDTO.getUsername())
                .password(SecurityUtils.encryptPassword("1234"))
                .avatar("/local-plus/avatar/default.jpg")
                .build();
        baseMapper.insert(newUser);

        // 3. 角色相关处理
        // 3.1 绑定用户角色联系
        LambdaQueryWrapper<Role> roleQueryWrapper = new LambdaQueryWrapper<>();
        roleQueryWrapper.in(Role::getRoleName,userDTO.getRoles());
        List<Role> roles = roleMapper.selectList(roleQueryWrapper);
        if (Objects.isNull(roles) || roles.isEmpty()) {
            throw new TypeException("用户角色错误");
        }

        // 批量插入用户角色关系
        List<UserRole> userRoles = roles.stream()
                .map(role -> UserRole.builder()
                        .userId(newUser.getUserId())
                        .roleId(role.getRoleId())
                        .build())
                .collect(Collectors.toList());
        userRoleMapper.insert(userRoles);

        // 4.将用户Id添加到布隆过滤器
        userBloomFilter.add(newUser.getUserId());
        return new Result(200,"注册成功");
    }

接口测试:启动访问localhost:8080/doc.html

先来看看权限不足访问失败:可以看到用户未登录切没有权限访问接口,触发自定义认证失败处理器AuthenticationEntryPoint。

下面去登录接口进行登录:

11.login接口实现

package com.hl.stuknowmanagesys.controller;

import com.hl.stuknowmanagesys.entity.dto.UserDTO;
import com.hl.stuknowmanagesys.entity.vo.LoginUser;
import com.hl.stuknowmanagesys.entity.vo.UserVO;
import com.hl.stuknowmanagesys.service.UserService;
import com.hl.stuknowmanagesys.utils.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Tag(name = "登录业务")
@Slf4j
@RestController
public class LoginController {

    @Resource
    private UserService userService;

    /**
     * 登录接口
     */
    @Operation(summary = "登录接口", description = "请求参数{必须:account、password}")
    @PostMapping("/login")
    public Result login(@RequestBody @Validated(UserDTO.Login.class) UserDTO userDTO) {
        log.info("用户{}登录", userDTO);
        LoginUser loginUser =userService.login(userDTO);
        UserVO userVO = UserVO.builder()
                .userId(loginUser.getUserId())
                .username(loginUser.getUsername())
                .avatar(loginUser.getAvatar())
                .token(loginUser.getToken())
                .roles(loginUser.getRoles())
                .permissions(loginUser.getPermissions())
                .build();
        return new Result(200, "登录成功", userVO);
    }
}

service如下,认证通过后生成token存入Redis并返回前端:

@Override
    public LoginUser login(UserDTO userDTO) {
        // 创建用户名密码认证令牌
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDTO.getUsername(),userDTO.getPassword());
        try{
            // 执行用户认证
            Authentication authenticate = authenticationManager.authenticate(authenticationToken);
            // 获取登录用户信息
            LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
            // 使用用户ID生成JWT token
            String userId = loginUser.getUserId().toString();
            //登录成功后,生成jwt令牌
            Map<String, Object> claims = new HashMap<>();
            claims.put(Constants.JWT_USERID, userId);
            String jwt = JwtUtil.createJWT(
                    jwtProperties.getUserSecretKey(),
                    jwtProperties.getUserTtl(),
                    claims);
            loginUser.setToken(jwt);
            // 将认证信息存入Redis缓存,并设置缓存过期时间
            redisCache.setCacheObject(RedisConstant.LOGIN_TOKEN_KEY +userId, loginUser, Integer.valueOf(String.valueOf(jwtProperties.getUserTtl())), TimeUnit.MINUTES);
            // 返回登录成功结果
            return loginUser;
        }catch (Exception e) {
            // 打印异常信息
            System.out.println(e.getMessage());
            // 返回登录失败结果
            throw new LoginFailedException("用户名或密码错误");
        }
    }

接口测试:

已经准备好了一个用户:username:hl,password:1234

执行查看Redis中也存在信息:

12.hello接口实现

设置权限只有teacher用户可以访问:

package com.hl.stuknowmanagesys.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
public class HelloController {
    @PreAuthorize("hasRole('teacher')")
    @GetMapping
    public String hello() {
        return "hello";
    }
}

将刚才登录生成的token添加到请求头中,可看到权限不足触发自定义访问拒绝处理器:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汤姆大聪明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值