spring boot 3.x版本中集成spring security 6.x版本进行实现动态权限控制解决方案

一、背景

最近在进行项目从jdk8spring boot 2.7.x版本技术架构向jdk17spring boot 3.3.x版本的代码迁移,在迁移过程中,发现spring boot 3.3.x版本依赖的spring security版本已经升级6.x版本了,语法上和spring security 5.x版本有很多地方不兼容,因此记录试一下spring boot 3.3.x版本下,spring security 6.x的集成方案。

二、技术实现

1. 创建spring boot 3.3.x版本项目

spring boot 3.3.x版本对jdk版本要求较高,我这里使用的是jdk17,不久前,jdk21也已经发布了,可以支持虚拟线程,大家也可以使用jdk21

设置好jdk版本以后,新建项目,导入项目需要的相关依赖:

<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/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.1</version>
  </parent>

  <groupId>com.j.ss</groupId>
  <artifactId>spring-secrity6-spring-boot3-demo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>spring-secrity6-spring-boot3-demo</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
  </dependencies>
</project>

2. 创建两个测试接口

  • 创建两个接口用于测试,源码参考如下

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class SecurityController {
    
        @GetMapping("/hello")
        public String hello() {
            return "hello, spring security.";
        }
    
    
        @PostMapping("/work")
        public String work() {
            return "I am working.";
        }
    
    }
    
  • 启动项目,测试一下接口是否正常

    • hello接口

    • work接口
      在这里插入图片描述

3. 引入spring-boot-starter-security依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

引入spring-boot-starter-security依赖以后,此时访问接口,会有未授权问题。

在这里插入图片描述

4. 定义UserDetailsManager实现类

spring security框架会自动使用UserDetailsManagerloadUserByUsername方法进行用户加载,在加载用户以后,会在UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法中,进行前端输入的用户信息和加载的用户信息进行信息对比。

import lombok.extern.java.Log;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
@Log
public class MyUserDetailsManager implements UserDetailsManager {


    @Override
    public void createUser(UserDetails user) {

    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /**
         * 这里为了演示方便,模拟从数据库查询,直接设置一下权限
         */
        log.info("query user from db!");
        return queryFromDB(username);
    }

    private static UserDetails queryFromDB(String username) {
        GrantedAuthority authority = new SimpleGrantedAuthority("testRole");
        List<GrantedAuthority> list = new ArrayList<>();
        list.add(authority);
        return new User("jack", // 用户名称
                new BCryptPasswordEncoder().encode("123456"), //密码
                list      //权限列表
        );
    }
}

5. 定义权限不足处理逻辑

用户在访问没有权限的接口时,会抛出异常,spring security允许我们自己这里这种异常,我这里就是模拟一下权限不足的提示信息,不做过多处理。

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //登陆状态下,权限不足执行该方法
        response.setStatus(200);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        String body = "403,权限不足!";
        printWriter.write(body);
        printWriter.flush();
    }
}

6. 定义未登录情况处理逻辑

当用户没有登录情况下,访问需要权限的接口时,会抛出异常,spring security允许我们自定义处理逻辑,这里未登录就直接抛出401,提示用户登录。

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        //验证为未登陆状态会进入此方法,认证错误
        response.setStatus(401);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        String body = "401, 请先进行登录!";
        printWriter.write(body);
        printWriter.flush();
    }
}

7. 定义自定义动态权限检验处理逻辑

在请求接口进行安全访问的时候,我们可以指定访问接口需要的角色,但是实际应用中,为了满足系统的灵活性,我们往往需要自定义动态权限的校验逻辑。

import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.function.Supplier;

@Component
public class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
    /**
     * @param authentication the {@link Supplier} of the {@link Authentication} to check
     * @param object         the {@link T} object to check
     * @return
     */
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        // 获取访问url
        String requestURI = object.getRequest().getRequestURI();

        // 模拟从数据库或者缓存里面查询拥有当前URI的权限的角色
        String[] allRole = query(requestURI);

        // 获取当前用户权限
        Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();

        // 判断是否拥有权限
        for (String role : allRole) {
            for (GrantedAuthority r : authorities) {
                if (role.equals(r.getAuthority())) {
                    return new AuthorizationDecision(true); // 返回有权限
                }
            }
        }

        return new AuthorizationDecision(false); //返回没有权限
    }

    /**
     * 查询当前拥有对应url的权限的角色
     *
     * @param requestURI
     * @return
     */
    private String[] query(String requestURI) {
        return new String[]{"testRole"};
    }
}

8. 定义安全访问统一入口

在统一入口,我们可以做一些统一的逻辑,比如前后端分离的情况下,进行token内容的解析,这里我只是用代码模拟演示一下,方便大家理解。

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.java.Log;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

@Component
@Log
public class MyAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String token = request.getHeader("token"); // 前后端分离的时候获取token
        if (StringUtils.hasText(token)) { // 如果token不为空,则需要解析出用户信息,填充到当前上下文中
            UsernamePasswordAuthenticationToken authentication = getUserFromToken(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            if (log.isLoggable(Level.INFO)) {
                log.info("set authentication");
            }
        } else {
            if (log.isLoggable(Level.INFO)) {
                log.info("user info is null.");
            }
        }

        filterChain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getUserFromToken(String token) {
        GrantedAuthority authority = new SimpleGrantedAuthority(token);
        List<GrantedAuthority> list = new ArrayList<>();
        list.add(authority);
        User user = new User("jack", // 用户名称
                new BCryptPasswordEncoder().encode("123456"), //密码
                list      //权限列表
        );
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        usernamePasswordAuthenticationToken.setDetails(user);
        return usernamePasswordAuthenticationToken;
    }
}

9. 编写spring security配置类

当所有准备工作,做好以后,下面就是编写spring security的配置类了,使我们的相关配置生效。

import com.j.ss.MyAccessDeniedHandler;
import com.j.ss.MyAuthenticationEntryPoint;
import com.j.ss.MyAuthenticationFilter;
import com.j.ss.MyAuthorizationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @Configuration 注解表示将该类以配置类的方式注册到spring容器中
 */
@Configuration
/**
 * @EnableWebSecurity 注解表示启动spring security
 */
@EnableWebSecurity
/**
 * @EnableMethodSecurity 注解表示启动全局函数权限
 */
@EnableMethodSecurity
public class WebSecurityConfig {

    /**
     * 权限不足处理逻辑
     */
    @Autowired
    private MyAccessDeniedHandler accessDeniedHandler;

    /**
     * 未授权处理逻辑
     */
    @Autowired
    private MyAuthenticationEntryPoint authenticationEntryPoint;

    /**
     * 访问统一处理器
     */
    @Autowired
    private MyAuthenticationFilter authenticationTokenFilter;

    /**
     * 自定义权限校验逻辑
     */
    @Autowired
    private MyAuthorizationManager myAuthorizationManager;

    /**
     * spring security的核心过滤器链
     *
     * @param httpSecurity
     * @return
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        // 定义安全请求拦截规则
        httpSecurity.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
                    authorizationManagerRequestMatcherRegistry
                            .requestMatchers("/hello")
                            .permitAll() // hello 接口放行,不进行权限校验
                            .anyRequest()
                            // .hasRole() 其他接口不进行role具体校验,进行动态权限校验
                            .access(myAuthorizationManager); // 动态权限校验逻辑
                })
                // 前后端分离,关闭csrf
                .csrf(AbstractHttpConfigurer::disable)
                // 前后端分离架构禁用session
                .sessionManagement(httpSecuritySessionManagementConfigurer -> {
                    httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                })
                // 访问异常处理
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
                    httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler);
                })
                // 未授权异常处理
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
                    httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(authenticationEntryPoint);
                })
                .headers(httpSecurityHeadersConfigurer -> {
                    // 禁用缓存
                    httpSecurityHeadersConfigurer.cacheControl(HeadersConfigurer.CacheControlConfig::disable);
                    httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable);
                });
        // 添加入口filter, 前后端分离的时候,可以进行token解析操作
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

    /**
     * 明文密码加密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 忽略权限校验
     *
     * @return
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web -> web.ignoring().requestMatchers("/hello"));
    }

}

三、 功能测试

上述代码编写完成以后,启动项目,下面进行功能测试。

1. 忽略权限校验测试

访问/hello接口

在这里插入图片描述

可以看到,此时接口在无登录信息的情况下,也可以正常访问的。

2. 无权限测试

同样的,我们直接访问/work接口

在这里插入图片描述

可以看到,此时提醒我们需要登录了。

3. 有权限测试

再次访问/work接口,模拟已经登录,并拥有对应的权限。

在这里插入图片描述

可以看到,我们模拟有testRole权限,此时访问是正常的。

4. 权限不足测试

再次访问/work接口,模拟已经登录,但拥有错误的权限。

在这里插入图片描述

可以看到,此时报出了权限不足的异常。

四、写在最后

上面的案例只是演示,spring security的实际应用,应该根据具体项目权限要求来进行合理实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/784749.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AntV X6 图编辑引擎速通

前言&#xff1a;参考 [AntV X6 官网](https://x6.antv.antgroup.com/) 一、简介 X6 可以快速搭建 DAG 图、ER 图、流程图、血缘图等应用。 二、快速上手 1. 安装 npm install antv/x6 --save# oryarn add antv/x6# orpnpm add antv/x6 2. 使用 2.1 初始画布 在页面中创…

【Linux进阶】文件系统3——目录树,挂载

前言 在Windows 系统重新安装之前&#xff0c;你可能会事先考虑&#xff0c;到底系统盘C盘要有多大容量&#xff1f;而数据盘D盘又要给多大容量等&#xff0c;然后实际安装的时候&#xff0c;你会发现其实C盘之前会有个100MB的分区被独立出来&#xff0c;所以实际上你就会有三个…

Java进阶----继承

继承 一.继承概述 继承是可以通过定义新的类&#xff0c;在已有类的基础上扩展属性和功能的一种技术. 案例&#xff1a;优化 猫、狗JavaBean类的设计 狗类&#xff1a;Dog 属性&#xff1a;名字 name&#xff0c;年龄 age 方法&#xff1a;看家 watchHome()&#xff0c;Gett…

QT5.12.9 通过MinGW64 / MinGW32 cmake编译Opencv4.5.1

一、安装前准备&#xff1a; 1.安装QT,QT5.12.9官方下载链接&#xff1a;https://download.qt.io/archive/qt/5.12/5.12.9/ QT安装教程&#xff1a;https://blog.csdn.net/Mark_md/article/details/108614209 如果电脑是64位就编译器选择MinGW64&#xff0c;32位就选择MinGW…

C#描述-计算机视觉OpenCV(5):直方图算法

C#描述-计算机视觉OpenCV&#xff08;5&#xff09;&#xff1a;直方图算法 前文链接图像直方图灰度直方图的计算灰度直方图的绘制BGR三通道的直方图直方图的均衡化算法相似图像检测 前文链接 文中没提到的东西&#xff0c;很可能都在前文描述过 C#描述-计算机视觉OpenCV&…

【C++深度探索】继承机制详解(二)

hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#xff1a;大耳朵土土垚的博客 &#x1…

深入解析:抖音视频标题的Python爬虫提取方法

引言 随着短视频的兴起&#xff0c;抖音已经成为全球最受欢迎的社交媒体平台之一。对于数据分析师、市场研究人员以及内容创作者来说&#xff0c;能够从抖音上抓取数据是一项宝贵的技能。本文将深入解析如何使用Python编写爬虫程序来提取抖音视频的标题。 爬虫基础 在开始编…

K8S 上部署大数据相关组件

文章目录 一、前言二、Redis 一、前言 Artifact Hub 是一个专注于云原生应用的集中式搜索和发布平台。它旨在简化开发者在 CNCF&#xff08;Cloud Native Computing Foundation&#xff09;项目中寻找、安装和分享包与配置的过程。用户可以通过这个平台方便地发现、安装各类云原…

为什么需要重写equals和如何重写equals

首先先看Java中的 &#xff0c;比较的两个对象的地址值。 如果是基本数据类型&#xff0c;那么就是比较的是值。 如果是引用数据类型&#xff0c;比较的就是地址. object类中的equals方法也是用的&#xff1b; 所以要比较两个对象的大小&#xff0c;去调用默认的equals方法…

Apache Spark分布式计算框架架构介绍

目录 一、概述 二、Apache Spark架构组件栈 2.1 概述 2.2 架构图 2.3 架构分层组件说明 2.3.1 支持数据源 2.3.2 调度运行模式 2.3.3 Spark Core核心 2.3.3.1 基础设施 2.3.3.2 存储系统 2.3.3.3 调度系统 2.3.3.4 计算引擎 2.3.4 生态组件 2.3.4.1 Spark SQL 2.…

关系型数据库MySQL和时序数据库的区别?

时序数据库和关系型数据库是两种不同类型的数据库系统&#xff0c;它们在设计理念、存储结构、性能优化等方面有显著差异&#xff0c;以适应不同的应用场景和需求。具体对比如下&#xff1a; 数据存储结构 时序数据库&#xff1a;使用列式存储&#xff0c;每条记录通常包含时间…

MySQL资源组的使用方法

MySQL支持创建和管理资源组&#xff0c;并允许将服务器内运行的线程分配给特定的组&#xff0c;以便线程根据组可用的资源执行。组属性允许控制其资源&#xff0c;以启用或限制组中线程的资源消耗。DBA可以针对不同的工作负载适当地修改这些属性。 目前&#xff0c;CPU时间是一…

田地行走-美团2023笔试(codefun2000)

题目链接 田地行走-美团2023笔试(codefun2000) 题目内容 塔子哥是一个农民&#xff0c;他有一片 nm 大小的田地&#xff0c;共 n 行 m 列&#xff0c;其中行和列都用从 1 开始的整数编号&#xff0c;田地中有 k 个格子中埋有土豆。我们记第 a 行第 b 列的格子为 (a,b) 。塔子哥…

LM2596/LM2596S多路降压稳压DC-DC开关电源芯片详解(第二部分:电路设计)(12V转5V、12V转3.3V、任意电压转任意电压)

目录 一、固定电压&#xff08;3.3/5/12V&#xff09;模块设计实例 1.设计条件&#xff1a;VOUT5V&#xff0c;VIN(MAX)12V&#xff0c;ILOAD(MAX)3A 2.设计步骤&#xff1a; &#xff08;1&#xff09;电感的选择&#xff08;L1&#xff09; &#xff08;2&#xff09;输…

C++入门基础

前言 本篇博客讲解一下c得入门基础 &#x1f493; 个人主页&#xff1a;普通young man-CSDN博客 ⏩ 文章专栏&#xff1a;C_普通young man的博客-CSDN博客 ⏩ 本人giee:普通小青年 (pu-tong-young-man) - Gitee.com 若有问题 评论区见&#x1f4dd; &#x1f389;欢迎大家点赞&…

掌握计算机网络基础:从零开始的指南

计算机网络是现代信息社会的重要基石。本文将以简洁明了的方式为基础小白介绍计算机网络的基本概念、分类、以及其在信息时代中的重要作用。 计算机网络在信息时代中的作用 21世纪是以数字化、网络化、信息化为重要特征的信息时代。 计算机网络作为信息的最大载体和传输媒介&…

微信自动加好友工具

批量导入数据到后台&#xff0c;可设置添加速度、间隔时间、验证信息和自动备注等&#xff0c;任务执行时间&#xff0c;后台会自动执行操作。

ubuntu 分区情况

ubuntu系统安装与分区指南 - Philbert - 博客园 (cnblogs.com)https://www.cnblogs.com/liangxuran/p/14872811.html 详解安装Ubuntu Linux系统时硬盘分区最合理的方法-腾讯云开发者社区-腾讯云 (tencent.com)https://cloud.tencent.com/developer/article/1711884

基于flask的猫狗图像预测案例

&#x1f4da;博客主页&#xff1a;knighthood2001 ✨公众号&#xff1a;认知up吧 &#xff08;目前正在带领大家一起提升认知&#xff0c;感兴趣可以来围观一下&#xff09; &#x1f383;知识星球&#xff1a;【认知up吧|成长|副业】介绍 ❤️如遇文章付费&#xff0c;可先看…

uni-app 封装http请求

1.引言 前面一篇文章写了使用Pinia进行全局状态管理。 这篇文章主要介绍一下封装http请求&#xff0c;发送数据请求到服务端进行数据的获取。 感谢&#xff1a; 1.yudao-mall-uniapp: 芋道商城&#xff0c;基于 Vue Uniapp 实现&#xff0c;支持分销、拼团、砍价、秒杀、优…