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

本篇博文目录:

    • 一.SpringSecurity简介
      • 1.SpringSecurity
      • 2.SpringSecurity相关概念
    • 二.认证和授权
      • 1.认证
        • (1) 使用SpringSecurity进行简单的认证(SpringBoot项目中)
        • (2) SpringSecurity的原理
        • (3) SpringSecurity核心类
        • (4) 认证登入案例(JWT+SpringSecurity实现登入案例)
      • 2.授权
        • (1) 加入权限到Authentication中
        • (2) SecurityConfig配置文件中开启注解权限配置
        • (3) 给接口中的方法添加访问权限
        • (4) 用户权限表的建立
      • 3.自定义失败处理
        • (1) 创建异常处理类
        • (2) 配置移除处理类
      • 4.跨域问题
    • 三.源码下载

一.SpringSecurity简介

1.SpringSecurity

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

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第1张图片
Spring Security英文教程:https://docs.spring.io/spring-security/reference/index.html
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第2张图片
Spring Security中文教程:https://docs.gitcode.net/spring/guide/spring-security/overview.html
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第3张图片

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" :
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第4张图片
项目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>

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

运行项目,访问sayHello接口,搞定:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第5张图片

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

 <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>

再次访问sayHello接口,就会跳转到一个登入界面,在项目中我们并没有编写该登入界面的代码,其实这就是SpringSecurity 内置的用户身份验证:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第6张图片
身份验证默认的账号为:user,密码在项目启动的时候在控制台会打印,注意每次启动的时候密码会发生变化!
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第7张图片

输入账号和密码后点击登入:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第8张图片
登入后,就能够成功访问sayHello接口了:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第9张图片

(2) SpringSecurity的原理

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第10张图片
上图中的三个过滤器分别是:

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

在项目启动类中,按下图所示进行debug,就可以观察到这个过滤链,过滤链上一共有16个过滤器( run.getBean(DefaultSecurityFilterChain.class) )
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第11张图片
这16个过滤器分别是:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第12张图片

(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;
  • 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(); 表示获取用户名
booleanisAccountNonExpired(); 表示判断账户是否过期
booleanisAccountNonLocked(); 表示判断账户是否被锁定
booleanisCredentialsNonExpired(); 表示凭证{密码}是否过期
booleanisEnabled(); 表示当前用户是否可用
  • 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)

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第13张图片
我们来看看createDelegatingPasswordEncoder()的源码:

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第14张图片

运行效果:

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第15张图片

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第16张图片
运行效果:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第17张图片

实际项目中如果不采用默认方式,可以通过@Bean的方式来统一配置全局共用的PasswordEncoder,如下所示:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第18张图片

上图所示在配置中还重新配置了users
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第19张图片

就一个用户,用户名为user,密码123456,使用该用户进行登入:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第20张图片
登入成功:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第21张图片

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

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

SpringSecurity的认证流程图如下,如果不按默认方式进行,将对应接口的实现类换成自己的实现类即可:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第22张图片
JWT的流程图如下:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第23张图片

JWT+SpringSecurity实现登入案例:

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第24张图片

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

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

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

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

最后收到返回的Token并将Token返回给前端:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第43张图片
前端收到服务端的Token信息:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第44张图片
前端收到token信息后将JWT的token信息放在请求头中,然后再去访问相应接口,这里访问http://localhost:8089/admin/permission/1接口

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第45张图片
再次经过自定义过滤器的时候,就可以获取该请求头中的数据Spring Security详细讲解(JWT+SpringSecurity登入案例)_第46张图片
从请求头中获取关键信息,用户名和JWT的token:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第47张图片
如果解析出的username不为null,就通过UserDetailsService.loadUserByUsername(username)获取UserDetails:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第48张图片
一样的,执行UserDetailsService.loadUserByUsername(username)还是执行我们编写的userDetailsService() ,从数据库中获取

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第49张图片
获取到的UserDetails对象
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第50张图片

控制台输出信息:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第51张图片
然后通过JWT的工具类进行验证:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第52张图片
验证通过后封装Authenticationm,并将authentication保存在SecurityContextHolder中
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第53张图片
然后放行执行其他过滤器:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第54张图片

同样的在业务层中实现业务代码,并返回给controller层:
在这里插入图片描述
controller层收到后返回给前端:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第55张图片

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第56张图片

2.授权

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

(1) 加入权限到Authentication中

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

将权限信息写入到UserDetails中
在这里插入图片描述
再将UserDetails封装到Authentication中
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第57张图片
AdminUserDetails是UserDetails的接口实现类
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第58张图片

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

使用 @EnableGlobalMethodSecurity(prePostEnabled=true) 注解
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第59张图片

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

使用 @PreAuthorize("hasAuthority('pms:brand:read')") 注解
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第60张图片

重新运行后,访问 http://localhost:8089/admin/permission/1 接口 ( 不需要再登入,因为默认JWT失效时间为1周 ),由于该用户没有权限访问,所以访问失败:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第61张图片
接下来更换用户test,密码为123456进行登入:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第62张图片

在http://localhost:8089/admin/permission/1中更换请求头的token信息:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第63张图片
再次访问接口(访问成功):
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第64张图片

(4) 用户权限表的建立

RBAC权限模型

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

在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限,它们之间的关系如下图所示:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第65张图片

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第66张图片

前面项目中的权限数据表的设计就是采用RBAC模型,如下图所示( 数据库的设计来源于github上的mall项目,这里不做解释 ):
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第67张图片

ums_role角色表,表中存在三种角色,商品管理员,订单管理员和超级管理员
在这里插入图片描述
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第68张图片

ums_admin用户表:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第69张图片
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第70张图片

ums_admin和ums_role关联表 ums_admin_role_relation:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第71张图片
ums_permission:许可表
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第72张图片

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第73张图片
ums_admin和ums_permission的关联表:ums_admin_permission_relation:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第74张图片
ums_role和ums_permission的关联表ums_role_permission_relation:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第75张图片

查询权限的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}

前面的项目中,获取数据库的权限由于有多张表参与,所以无法直接使用mybatis生成的dao进行操作,所以需要自己创建dao,如下:
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第76张图片
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>

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第77张图片
每一个mapper接口中都没有@mapper所以需要添加一个MyBatisConfig的配置类通过@MapperScan注解扫描mapper
Spring Security详细讲解(JWT+SpringSecurity登入案例)_第78张图片

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();
    }
}

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();
    }
}

(2) 配置移除处理类

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

  • 先注入对应的处理器

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第79张图片

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第80张图片

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);
    }
}

  • 开启SpringSecurity的跨域访问

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

Spring Security详细讲解(JWT+SpringSecurity登入案例)_第81张图片

三.源码下载

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

你可能感兴趣的:(Spring全家桶,spring,java,Spring,Security,后端,spring安全认证)