SpringSecurity前后端分离(动态鉴权)
一、认证流程讲解
1、原始认证流程
原始认证流程通常会配合Session一起使用,但前后端分离后就用不到Session了
SpringSecurity默认的认证流程如下图(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)
DaoAuthenticationProvider
继承AbstractUserDetailsAuthenticationProvider
抽象类,而AbstractUserDetailsAuthenticationProvider
抽象类又实现了AuthenticationProvider
这个接口。
AuthenticationProvider
接口和AuthenticationManager
接口都有authenticate()
这个方法认证流程:
1、传入用户名和密码
2、
UsernamePasswordAuthenticationFilter
会把用户名和密码封装成Authentication对象3、然后又再调用
AuthenticationManager
接口中的authenticate()
方法进行认证,在AuthenticationManager
接口的实现类ProviderManager
中又调用了重写的authenticate()
方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider
中重写了authenticate()
方法4、
AbstractUserDetailsAuthenticationProvider
的authenticate()
方法中调用了抽象方法retrieveUser()
方法5、
DaoAuthenticationProvider
在重写方法retrieveUser()
里调用了loadUserByUsername()
方法6、
loadUserByUsername()
方法会返回UserDetails
对象,认证成功逐一返回上一层
2、前后端分离认证流程
前后端分离后,我们要求在认证成功或者失败的时候能够返回对应的状态码,这时我们不再使用Session进行认证管理,而常采用jwt(JSON Web Token)的方式进行认证,这里引出两种前后端分离的写法
(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)
无论使用下面哪一种写法,这里都需要在
UsernamePasswordAuthenticationFilter
前面添加一个过滤器,用于进行Token认证,如果Token认证成功,则表示该用户已登录;Token认证失败则表明未登录或者登陆已过期。
2.1、继承UsernamePasswordAuthenticationFilter
的写法
认证流程:
1、传入用户名和密码
2、
MyUsernamePasswordAuthenticationFilter
会把用户名和密码封装成Authentication对象3、然后又再调用
AuthenticationManager
接口中的authenticate()
方法进行认证,在AuthenticationManager
接口的实现类ProviderManager
中又调用了重写的authenticate()
方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider
中重写了authenticate()
方法4、
AbstractUserDetailsAuthenticationProvider
的authenticate()
方法中调用了抽象方法retrieveUser()
方法5、
DaoAuthenticationProvider
在重写方法retrieveUser()
里调用了loadUserByUsername()
方法,自定义AuthUserDetailsServiceImpl
类实现UserDetailsService
接口,重写loadUserByUsername()
方法6、在
loadUserByUsername()
方法中,会查询用户和角色,然后返回UserDetails
对象7、在继承
WebSecurityConfigurerAdapter
的类中设置登陆成功、失败处理器,处理器内部定义好返回的状态码等信息
2.2、自定义写法
UsernamePasswordAuthenticationToken
继承了AbstractAuthenticationToken
抽象类,AbstractAuthenticationToken
抽象类实现了Authentication
接口认证流程:
1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层
2、Service层创建
UsernamePasswordAuthenticationToken
对象,把用户名和密码封装成Authentication
对象3、然后调用
AuthenticationManager
的authenticate()
方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider
中重写了authenticate()
方法4、
AbstractUserDetailsAuthenticationProvider
的authenticate()
方法中调用了抽象方法retrieveUser()
方法5、
DaoAuthenticationProvider
在重写方法retrieveUser()
里调用了loadUserByUsername()
方法,自定义AuthUserDetailsServiceImpl
类实现UserDetailsService
接口,重写loadUserByUsername()
方法6、在
loadUserByUsername()
方法中,会查询用户和角色,然后返回UserDetails
对象
2.3、区别
1、使用
UsernamePasswordAuthenticationFilter
的写法需要使用登陆成功、失败处理器,自定义的写法不需要,自定义的写法可以自定义失败处理器(包括认证异常和授权异常,即登陆失败和没有权限)2、使用
UsernamePasswordAuthenticationFilter
的写法对于扩展写法没那么友好,比如说添加手机验证码
二、数据库的设计
该示例是上面自定义的前后端分离的写法
这里使用的是Oracle数据库,这里没有权限的表,但是使用角色来判断也差不多
1、用户表
2、用户角色关系表
3、角色表
4、图片表
5、点赞表
三、初始配置
SpringBoot 版本是 2.6.0
1、项目结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tfbztDyA-1659406016673)(https://dn-simplecloud.shiyanlou.com/courses/uid1534017-20220213-1644749790627)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cEOZZevH-1659406016674)(https://dn-simplecloud.shiyanlou.com/courses/uid1534017-20220213-1644749819365)]
2、导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- SpringSecurity -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--MyBatis-Plus的依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<!--hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.6</version>
</dependency>
<!-- mybatis-plus-generator -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<!--添加 模板引擎 依赖,MyBatis-Plus 支持 Velocity(默认)-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<!--swagger的依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- JWT的依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Oracle数据库 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
3、代码生成器
代码生成器这里最开始使用的是mysql 8.X版本的,读者需要自己修改一下数据库的名字,如果是mysql 5.X还需要修改一下驱动
后面才改用Oracle数据库,这里的代码就懒得改了
package com.guet.APPshareimage;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.apache.commons.lang3.StringUtils;
import java.util.Scanner;
/**
* @Author LZDWTL
* @Date 2021-12-15 17:09
* @ClassName CodeGenerator
* @Description 代码生成器
*/
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 创建代码生成器对象
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir(scanner("请输入你的项目路径") + "/src/main/java");
//作者
gc.setAuthor("LZDWTL");
//生成之后是否打开资源管理器
gc.setOpen(false);
//重新生成时是否覆盖文件
gc.setFileOverride(false);
//%s 为占位符
//mp生成service层代码,默认接口名称第一个字母是有I
gc.setServiceName("%sService");
//设置主键生成策略 自动增长
gc.setIdType(IdType.AUTO);
//设置Date的类型 只使用 java.util.date 代替
gc.setDateType(DateType.ONLY_DATE);
//开启实体属性 Swagger2 注解
gc.setSwagger2(true);
mpg.setGlobalConfig(