• Spring Security详细讲解(JWT+SpringSecurity登入案例)


    一.SpringSecurity简介

    1.SpringSecurity

    SpringSecurity 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实上的标准。
    SpringSecurity 是一个致力于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以如何轻松地扩展以满足自定义需求

    官网地址:
    Spring Security简介https://spring.io/projects/spring-security

    在这里插入图片描述
    Spring Security英文教程:https://docs.spring.io/spring-security/reference/index.html
    在这里插入图片描述
    Spring Security中文教程:https://docs.gitcode.net/spring/guide/spring-security/overview.html
    在这里插入图片描述

    2.SpringSecurity相关概念

    • 什么是SpringSecurity

    SpringSecurity 是一个提供身份验证、授权和防止常见攻击的框架 。它对保护命令式和反应式应用程序都提供了一流的支持,是保护基于 Spring 的应用程序的事实上的标准。

    • 关于什么是认证和授权

    ​ 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

    ​ 授权:经过认证后判断当前用户是否有权限进行某个操作

    • SpringSecurity 特点

    Spring 无缝整合,全面的权限控制,专门为 Web 开发而设计(旧版本不能脱离 Web 环境使用,新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境),重量级。

    • SpringSecurity和Shiro的比较

    功能,社区资源上SpringSecurity远远优于Shiro,但是Shiro是轻量级更加容易上手,在SSM框架中整合 Spring Security 比较麻烦,但是在SpringBoot项目中 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security,所以推荐在SSM框架中使用Shiro,在SpringBoot和SpringCloud中使用SpringSecurity,你可以通过这篇博文了解这二种方式的实现SpringBoot学习—SpringSecurity与Shiro

    二.认证和授权

    1.认证

    SpringSecurity 为身份验证提供了全面的支持。身份验证是我们验证试图访问特定资源的用户身份的方式。对用户进行身份验证的一种常见方法是要求用户输入用户名和密码。一旦执行了身份验证,我们就知道了身份并可以执行授权。SpringSecurity 内置了对用户身份验证的支持。

    (1) 使用SpringSecurity进行简单的认证(SpringBoot项目中)

    创建一个SpringBoot的Web项目,功能非常简单就是通过访问sayHello接口,在游览器输出字符串 "Hello,SpringSecurity" :
    在这里插入图片描述
    项目pom.xml依赖:

    
    <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.0modelVersion>
        <parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.7.2version>
            <relativePath/> 
        parent>
        <groupId>com.dudugroupId>
        <artifactId>springsecuritydemoartifactId>
        <version>0.0.1-SNAPSHOTversion>
        <name>springsecuritydemoname>
        <description>Demo project for Spring Bootdescription>
        <properties>
            <java.version>1.8java.version>
        properties>
        <dependencies>
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <optional>trueoptional>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                plugin>
            plugins>
        build>
    
    project>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    controller下创建SpringSecurityController:
    在这里插入图片描述

    运行项目,访问sayHello接口,搞定:
    在这里插入图片描述

    接下来在项目的pom.xml配置文件中导入SpringSecurity的依赖:

     <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-securityartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4

    再次访问sayHello接口,就会跳转到一个登入界面,在项目中我们并没有编写该登入界面的代码,其实这就是SpringSecurity 内置的用户身份验证:
    在这里插入图片描述
    身份验证默认的账号为:user,密码在项目启动的时候在控制台会打印,注意每次启动的时候密码会发生变化!
    在这里插入图片描述

    输入账号和密码后点击登入:
    在这里插入图片描述
    登入后,就能够成功访问sayHello接口了:
    在这里插入图片描述

    (2) SpringSecurity的原理

    SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,下图列出的是该过滤链中比较重要的几个过滤器。

    在这里插入图片描述
    上图中的三个过滤器分别是:

    ① UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码。
    ② ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
    ③ FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部

    在项目启动类中,按下图所示进行debug,就可以观察到这个过滤链,过滤链上一共有16个过滤器( run.getBean(DefaultSecurityFilterChain.class) )
    在这里插入图片描述
    这16个过滤器分别是:
    在这里插入图片描述

    (3) SpringSecurity核心类

    📢 注意,下面几个知识点来源于这篇博文:https://www.w3cschool.cn/springsecurity/ted11ii1.html,如果需要更详细的了解请查阅原文。

    • Authentication

    Authentication 是一个接口,用来表示用户认证信息的,在用户登录认证之前相关信息会封装为一个 Authentication 具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。

    • SecurityContextHolder

    SecurityContextHolder 是用来保存 SecurityContext 的。SecurityContext 中含有当前正在访问系统的用户的详细信息。默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext,这也就意味着在处于同一线程中的方法中我们可以从 ThreadLocal 中获取到当前的 SecurityContext。因为线程池的原因,如果我们每次在请求完成后都将 ThreadLocal 进行清除的话,那么我们把 SecurityContext 存放在 ThreadLocal 中还是比较安全的。这些工作 Spring Security 已经自动为我们做了,即在每一次 request 结束后都将清除当前线程的 ThreadLocal。

    • AuthenticationManager

    AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。

      Authentication authenticate(Authentication authentication) throws AuthenticationException;
    
    • 1
    • AuthenticationProvider

    在 Spring Security 中,AuthenticationManager 的默认实现是 ProviderManager,而且它不直接自己处理认证请求,而是委托给其所配置的 AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果。如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException。

    • UserDetailsService

    UserDetailsService是一个加载用户特定数据的核心接口,登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 进行认证,认证通过后会将该 UserDetails 赋给认证通过的 Authentication 的 principal,然后再把该 Authentication 存入到 SecurityContext 中。

    • UserDetails

    UserDetails是一个提供核心用户信息的接口,通过 UserDetailsService 的 loadUserByUsername() 方法获取,然后将该 UserDetails 赋给认证通过的 Authentication 的 principal。

    常用方法:

    方法名解释
    Collection getAuthorities();表示获取登录用户所有权限
    String getPassword();表示获取密码
    String getUsername();表示获取用户名
    boolean isAccountNonExpired();表示判断账户是否过期
    boolean isAccountNonLocked();表示判断账户是否被锁定
    boolean isCredentialsNonExpired();表示凭证{密码}是否过期
    boolean isEnabled();表示当前用户是否可用
    • PasswordEncoder

    Spring Security 的PasswordEncoder接口用于执行密码的单向转换,以允许安全地存储密码。给定PasswordEncoder是单向转换,当密码转换需要双向(即存储用于对数据库进行身份验证的凭据)时,并不打算这样做。通常PasswordEncoder用于存储需要与用户在身份验证时提供的密码进行比较的密码

    常用方法:

    方法名解释
    String encode(CharSequence rawPassword);表示把参数按照特定的解析规则进行解析
    boolean matches(CharSequence rawPassword, String encodedPassword);表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回true;如果不匹配,则返回false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
    default boolean upgradeEncoding(String encodedPassword)表示如果解析的密码能够再次进行解析且达到更安全的结果则返回true,否则返回false。默认返回false。

    内置的PasswordEncoder实现列表:

    实现类说明
    NoOpPasswordEncoder(已废除)明文密码加密方式,该方式已被废除(不建议在生产环境使用),不过还是支持开发阶段测试Spring Security的时候使用。
    BCryptPasswordEncoder使用广泛支持的bcrypt 算法来散列密码
    Argon2Passwordencoder使用Argon2 算法来散列密码, 是一种故意缓慢的算法,需要大量内存
    PBKDF2PASSWORDENCODER使用PBKDF2 算法来散列密码,是一种故意缓慢的算法
    SCryptPasswordEncoder使用scrypt算法来散列密码,是一种故意缓慢的算法,需要大量内存

    SpringSecurity5.x版本默认的PasswordEncoder方式改成了DelegatingPasswordEncoder委托类,这是因为在5.0之前默认采用的NoOpPasswordEncoder,存在如下问题:

    1. 有许多应用程序使用旧的密码编码,无法轻松地进行迁移。
    2. 密码存储的最佳实践将再次改变。
    3. 作为一种框架 Spring,安全不能频繁地进行破坏更改

    SpringSecurity引入了DelegatingPasswordEncoder,它通过以下方式解决了所有问题:

    1. 确保使用当前的密码存储建议对密码进行编码
    2. 允许验证现代和遗留格式的密码。
    3. 允许在将来升级编码

    DelegatingPasswordEncoder委托支持动态的多种密码加密方式,它内部其实是一个Map集合,根据传递的Key(Key为加密方式)获取Map集合的Value,而Value则是具体的PasswordEncoder实现类。

    方式1:创建默认的代理 PasswordEncoder(SpringSecurity5.x)

    在这里插入图片描述
    我们来看看createDelegatingPasswordEncoder()的源码:

    从下图我们可以看出使用PasswordEncoderFactories.createDelegatingPasswordEncoder()创建的PasswordEncoder,默认加密方式是bcrypt,PasswordEncoder的默认实现类是BCryptPasswordEncoder

    在这里插入图片描述

    运行效果:

    从下图我们可以看出密码的一般格式为: {id}encodedPassword,如果使用的NoOpPasswordEncoder编码的话(不推荐使用该编码),数据库中的密码格式为 {noop}密码 ,如 {noop}123456 。

    在这里插入图片描述

    方式2:创建自定义代理 PasswordEncoder

    在这里插入图片描述
    运行效果:
    在这里插入图片描述

    实际项目中如果不采用默认方式,可以通过@Bean的方式来统一配置全局共用的PasswordEncoder,如下所示:
    在这里插入图片描述

    上图所示在配置中还重新配置了users
    在这里插入图片描述

    就一个用户,用户名为user,密码123456,使用该用户进行登入:
    在这里插入图片描述
    登入成功:
    在这里插入图片描述

    (4) 认证登入案例(JWT+SpringSecurity实现登入案例)

    📢 注意,下面的图来源于https://www.bilibili.com/video/BV1mm4y1X7Hc?p=1,并且该视频详细讲解了JWT+SpringSecurity的知识,如果想更详细的了解,跳转观看即可,关于JWT的知识你可以通过这篇博文->JWT详细讲解(保姆级教程)进行学习。

    SpringSecurity的认证流程图如下,如果不按默认方式进行,将对应接口的实现类换成自己的实现类即可:
    在这里插入图片描述
    JWT的流程图如下:
    在这里插入图片描述

    JWT+SpringSecurity实现登入案例:

    案例来源于github上mall项目的学习教程,这个案例的数据并没有从Redis中获取,而是从数据库中获取,之所以说没有用Redis进行获取是因为前面推荐的https://www.bilibili.com/video/BV1mm4y1X7Hc?p=1视频就是采用Redis进行存储。

    在这里插入图片描述

    下面给出关键代码来了解这个认证流程

    项目中有一个SecurityConfig类,表示SpringSecurity的配置
    在这里插入图片描述
    这个configure配置是整个Security Config的配置(过滤器链的配置),下图红框中的配置表示在UsernamePasswordAuthenticationFilter过滤器前添加一个过滤器。
    在这里插入图片描述
    这个过滤器是继承OncePerRequestFilter过滤器(之所以继承 OncePerRequestFilter 因为OncePerRequestFilter 一个请求只被过滤器拦截一次。请求转发不会第二次触发过滤器 ,而Filter会触发二次)
    在这里插入图片描述
    该过滤器存放在component文件下的JwtAuthenticationTokenFilter类
    在这里插入图片描述

    程序运行后,过滤器会一个一个的执行当执行到UsernamePasswordAuthenticationFilter过滤器之前会先执行自定义的JwtAuthenticationTokenFilter过滤器中的doFilterInternal()方法:
    在这里插入图片描述
    该方法业务逻辑非常清晰,就是判断是否在请求头中存在JWT的Token,如果存在( 用户已经登入过了 )就从token的载荷中获取用户名,然后再根据UserDetaulsService的loadUserByUsername(username)获取用户信息 (UserDetaulsService被替换成从数据库中获取用户信息,详细代码如下图所示 ) 然后进行认证,如果不存在( 该请求为登入请求 ),用户的信息从登入界面获取( 用户名,密码 )然后进行认证。
     图1
    这里的AdminUserDetails其实就是UserDetails接口的实现类(进行了扩展):
    在这里插入图片描述

    自定义的JwtAuthenticationTokenFilter过滤器放行后,就会执行UsernamePasswordAuthenticationFilter过滤器,执行过程如下图:
    在这里插入图片描述
    下面通过Postman进行模拟
    用户首次登入,访问http://localhost:8089/admin/login接口,并携带账号和密码:
    在这里插入图片描述
    在自定义JwtAuthenticationTokenFilter过滤器的doFilterInternal()方法中打断点,进行观察,因为首次登入头部没有信息,所以authHeader=null,最后 chain.doFilter(request, response);进行放行
    在这里插入图片描述
    接下来进行认证,代码会执行业务层的login(String username, String password)进行登入操作:
    在这里插入图片描述
    通过userDetailsService.loadUserByUsername(username)获取UserDetails对象,执行该方法会进入到SecurityConfig配置文件下的userDetailsService()中,从数据库中获取用户UmsAdmin信息和权限:
    在这里插入图片描述
    此时控制台输出结果:
    在这里插入图片描述
    从数据库中获取到的用户数据:
    在这里插入图片描述
    通过PasswordEncoder进行密码匹配
    在这里插入图片描述
    匹配成功,封装Authentication对象:
    在这里插入图片描述
    然后将Authentication保存到 SecurityContextHolder中
    在这里插入图片描述
    前面提到过UserDetails 会赋给认证通过的 Authentication 的 principal,确实如此
    在这里插入图片描述
    然后通过Token工具类生成Token字符串,并返回:
    在这里插入图片描述
    虽然传递的UserDetails对象,实际上只将用户名作为JWT的载荷( JWT中不要传入敏感信息 ):
    在这里插入图片描述
    在这里插入图片描述

    最后收到返回的Token并将Token返回给前端:
    在这里插入图片描述
    前端收到服务端的Token信息:
    在这里插入图片描述
    前端收到token信息后将JWT的token信息放在请求头中,然后再去访问相应接口,这里访问http://localhost:8089/admin/permission/1接口

    在这里插入图片描述
    再次经过自定义过滤器的时候,就可以获取该请求头中的数据在这里插入图片描述
    从请求头中获取关键信息,用户名和JWT的token:
    在这里插入图片描述
    如果解析出的username不为null,就通过UserDetailsService.loadUserByUsername(username)获取UserDetails:
    在这里插入图片描述
    一样的,执行UserDetailsService.loadUserByUsername(username)还是执行我们编写的userDetailsService() ,从数据库中获取

    在这里插入图片描述
    获取到的UserDetails对象
    在这里插入图片描述

    控制台输出信息:
    在这里插入图片描述
    然后通过JWT的工具类进行验证:
    在这里插入图片描述
    验证通过后封装Authenticationm,并将authentication保存在SecurityContextHolder中
    在这里插入图片描述
    然后放行执行其他过滤器:
    在这里插入图片描述

    同样的在业务层中实现业务代码,并返回给controller层:
    在这里插入图片描述
    controller层收到后返回给前端:
    在这里插入图片描述

    在这里插入图片描述

    2.授权

    ​ 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息,具体操作如下:

    (1) 加入权限到Authentication中

    所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication中就可以了,在前面的项目中我们已经将权限写入到Authentication中了,就是在获取UserDetails的时候,将权限信息也写入到UserDetails中,然后再封装到Authentication中,整个过程如下:

    将权限信息写入到UserDetails中
    在这里插入图片描述
    再将UserDetails封装到Authentication中
    在这里插入图片描述
    AdminUserDetails是UserDetails的接口实现类
    在这里插入图片描述

    (2) SecurityConfig配置文件中开启注解权限配置

    使用 @EnableGlobalMethodSecurity(prePostEnabled=true) 注解
    在这里插入图片描述

    (3) 给接口中的方法添加访问权限

    使用 @PreAuthorize("hasAuthority('pms:brand:read')") 注解
    在这里插入图片描述

    重新运行后,访问 http://localhost:8089/admin/permission/1 接口 ( 不需要再登入,因为默认JWT失效时间为1周 ),由于该用户没有权限访问,所以访问失败:
    在这里插入图片描述
    接下来更换用户test,密码为123456进行登入:
    在这里插入图片描述

    在http://localhost:8089/admin/permission/1中更换请求头的token信息:
    在这里插入图片描述
    再次访问接口(访问成功):
    在这里插入图片描述

    (4) 用户权限表的建立

    RBAC权限模型

    基于角色的访问控制(RBAC)是实施面向企业安全策略的一种有效的访问控制方式。 其基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。博文推荐:RBAC权限模型——项目实战

    在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限,它们之间的关系如下图所示:
    在这里插入图片描述

    RBAC其实是一种分析模型,主要分为:基本模型RBAC0(Core RBAC)、角色分层模型RBAC1(Hierarchal RBAC)、角色限制模型RBAC2(Constraint RBAC)和统一模型RBAC3(Combines RBAC),其中RBAC0为核心,其他模型在RBAC0基础上进行扩展, RBAC0由用户,角色,会话和许可(“操作”和“控制对象”)四部分组成 ,如下图所示:

    在这里插入图片描述

    前面项目中的权限数据表的设计就是采用RBAC模型,如下图所示( 数据库的设计来源于github上的mall项目,这里不做解释 ):
    在这里插入图片描述

    ums_role角色表,表中存在三种角色,商品管理员,订单管理员和超级管理员
    在这里插入图片描述
    在这里插入图片描述

    ums_admin用户表:
    在这里插入图片描述
    在这里插入图片描述

    ums_admin和ums_role关联表 ums_admin_role_relation:
    在这里插入图片描述
    ums_permission:许可表
    在这里插入图片描述

    在这里插入图片描述
    ums_admin和ums_permission的关联表:ums_admin_permission_relation:
    在这里插入图片描述
    ums_role和ums_permission的关联表ums_role_permission_relation:
    在这里插入图片描述

    查询权限的sql语句如下:

            SELECT
                p.*
            FROM
                ums_admin_role_relation ar
                    LEFT JOIN ums_role r ON ar.role_id = r.id
                    LEFT JOIN ums_role_permission_relation rp ON r.id = rp.role_id
                    LEFT JOIN ums_permission p ON rp.permission_id = p.id
            WHERE
                ar.admin_id = #{adminId}
              AND p.id IS NOT NULL
              AND p.id NOT IN (
                SELECT
                    p.id
                FROM
                    ums_admin_permission_relation pr
                        LEFT JOIN ums_permission p ON pr.permission_id = p.id
                WHERE
                    pr.type = - 1
                  AND pr.admin_id = #{adminId}
            )
            UNION
            SELECT
                p.*
            FROM
                ums_admin_permission_relation pr
                    LEFT JOIN ums_permission p ON pr.permission_id = p.id
            WHERE
                pr.type = 1
              AND pr.admin_id = #{adminId}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    前面的项目中,获取数据库的权限由于有多张表参与,所以无法直接使用mybatis生成的dao进行操作,所以需要自己创建dao,如下:
    在这里插入图片描述
    UmsAdminRoleRelationDao.xml详细代码如下:

    
    DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.dudu.dao.UmsAdminRoleRelationDao">
        <select id="getPermissionList" resultMap="com.dudu.mbg.mapper.UmsPermissionMapper.BaseResultMap">
            SELECT
                p.*
            FROM
                ums_admin_role_relation ar
                    LEFT JOIN ums_role r ON ar.role_id = r.id
                    LEFT JOIN ums_role_permission_relation rp ON r.id = rp.role_id
                    LEFT JOIN ums_permission p ON rp.permission_id = p.id
            WHERE
                ar.admin_id = #{adminId}
              AND p.id IS NOT NULL
              AND p.id NOT IN (
                SELECT
                    p.id
                FROM
                    ums_admin_permission_relation pr
                        LEFT JOIN ums_permission p ON pr.permission_id = p.id
                WHERE
                    pr.type = - 1
                  AND pr.admin_id = #{adminId}
            )
            UNION
            SELECT
                p.*
            FROM
                ums_admin_permission_relation pr
                    LEFT JOIN ums_permission p ON pr.permission_id = p.id
            WHERE
                pr.type = 1
              AND pr.admin_id = #{adminId}
        select>
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    dao层中定义UmsAdminRoleRelationDao的接口
    在这里插入图片描述
    业务层中通过adminRoleRelationDao获取数据库中的权限信息
    在这里插入图片描述

    在这里插入图片描述
    每一个mapper接口中都没有@mapper所以需要添加一个MyBatisConfig的配置类通过@MapperScan注解扫描mapper
    在这里插入图片描述

    3.自定义失败处理

    我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

    • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用 AuthenticationEntryPoint 对象的方法去进行异常处理。
    • 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用 AccessDeniedHandler 对象的方法去进行异常处理。

    所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可,具体操作如下:

    (1) 创建异常处理类

    在这里插入图片描述
    RestAuthenticationEntryPoint类( 当未登录或者token失效访问接口时,自定义的返回结果【认证失败】 ):

    package com.dudu.component;
    
    import cn.hutool.json.JSONUtil;
    import com.dudu.common.api.CommonResult;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * 当未登录或者token失效访问接口时,自定义的返回结果【认证失败】
     */
    @Component
    public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
            response.getWriter().flush();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    RestfulAccessDeniedHandler类( 当访问接口没有权限时,自定义的返回结果【授权失败】 ) :

    package com.dudu.component;
    
    import cn.hutool.json.JSONUtil;
    import com.dudu.common.api.CommonResult;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * 当访问接口没有权限时,自定义的返回结果【授权失败】
     */
    @Component
    public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
            response.getWriter().flush();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    (2) 配置移除处理类

    在SecurityConfig中configure()方法中配置异常处理类:

    • 先注入对应的处理器

    在这里插入图片描述

    • 然后我们可以使用HttpSecurity对象的方法去配置。

    在这里插入图片描述

    4.跨域问题

    浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 (前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。),如何设置跨域,你可以通过这篇博文springboot设置Cors跨域的四种方式进行学习。

    • 添加跨域配置类
    package com.dudu.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.filter.CorsFilter;
    
    /**
     * 全局跨域配置
     */
    @Configuration
    public class GlobalCorsConfig{
    
        /**
         * 允许跨域调用的过滤器
         */
        @Bean
        public CorsFilter corsFilter() {
            CorsConfiguration config = new CorsConfiguration();
            //允许所有域名进行跨域调用
            config.addAllowedOriginPattern("*");
            //允许跨越发送cookie
            config.setAllowCredentials(true);
            //放行全部原始头信息
            config.addAllowedHeader("*");
            //允许所有请求方法跨域调用
            config.addAllowedMethod("*");
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", config);
            return new CorsFilter(source);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 开启SpringSecurity的跨域访问

    在SpringSecurity配置文件的configure(HttpSecurity httpSecurity)方法下通过httpSecurity.cors()运行跨域:

    在这里插入图片描述

    三.源码下载

    JWT+SpringSecurity实现登入案例的代码采用github上的mall项目教程的代码,你可以通过该教程的链接进行下载,该教程路径https://www.macrozheng.com/mall/architect/mall_arch_05.html:
    在这里插入图片描述
    当然你也可以通过我的微信公众号进行下载,里面包括了博文中涉及到的所有代码,微信公众号搜索程序员孤夜(或扫描下方二维码),后台回复 登入案例 ,即可获取本篇文章所使用的源代码下载链接。
    在这里插入图片描述

  • 相关阅读:
    Android开机动画
    linux多线程编程之pthread_join和pthread_once使用
    java-php-python-ssm学校食堂订餐管理计算机毕业设计
    网络安全(黑客)自学
    Linux必会100个命令(五十三)dmesg命令
    记录一次非常麻烦的调试
    python tkinter 使用(三)
    Python 操作 MongoDB 数据库介绍
    一些docker笔记
    golang将pcm格式音频转为mp3格式
  • 原文地址:https://blog.csdn.net/weixin_42753193/article/details/126324474