SpringSecurity前后端分离(动态鉴定)
一、解释认证流程
1.原始认证流程
原始认证过程通常与Session一起使用,但是前后端分离后能使用Session了
SpringSecurity默认认证程序如下图所示(此图为B站UP主三更草堂讲SpringSecurity课程的图)
DaoAuthenticationProvider
继承AbstractUserDetailsAuthenticationProvider
抽象类,而AbstractUserDetailsAuthenticationProvider
抽象类又实现了AuthenticationProvider
这个接口。
AuthenticationProvider
接口和AuthenticationManager
接口都有authenticate()
这个方法认证流程:
1.输入用户名和密码
2、
UsernamePasswordAuthenticationFilter
将用户名和密码包装成密码Authentication对象然后再调用
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对象然后再调用
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、项目结构
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(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/shareimage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("shareimage");
dsc.setPassword("888888");
//使用mysql数据库
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
//pc.setModuleName(scanner("请输入模块名"));
pc.setParent("com.guet.APPshareimage");
pc.setController("controller");
pc.setService("service");
pc.setServiceImpl("service.impl");
pc.setMapper("mapper");
pc.setEntity("entity");
pc.setXml("mapper");
mpg.setPackageInfo(pc);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
//设置哪些表需要自动生成
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
//实体类名称驼峰命名
strategy.setNaming(NamingStrategy.underline_to_camel);
//列名名称驼峰命名
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//使用简化getter和setter
strategy.setEntityLombokModel(true);
//设置controller的api风格 使用RestController
strategy.setRestControllerStyle(true);
//驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
//忽略表中生成实体类的前缀
//strategy.setTablePrefix("t_");
mpg.setStrategy(strategy);
mpg.execute();
}
}
运行代码生成器,复制路径输入,然后依次输入数据库中表的名字
D:\WorkSpace\JavaWorkSpce\ideal\APP-shareimage\APP-shareimage
t_user,t_picture,t_like,t_user_role,t_role
4、配置application.yml
根据自己的数据库和redis进行配置
server:
port: 8080
spring:
# 数据库配置
datasource:
driver-class-name: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@120.77.80.135:1521:orcl
username: XXXXXX
password: XXXXXX
# 连接池
hikari:
# 连接池名
pool-name: DateHikariCP
# 最小空闲连接数
minimum-idle: 5
# 空闲连接最大存活时间,默认600000(10分钟)
idle-timeout: 180000
# 最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接自动提交
auto-commit: true
# 连接最大存活时间,1800000(30分钟)
max-lifetime: 1800000
# 连接超时时间,默认30000(30秒)
connection-timeout: 30000
# 测试连接是否可用的查询语句
#connection-test-query: SELECT 1 #这个是mysql的测试语句
connection-test-query: SELECT * from dual #这个是oracle的测试语句
#redis配置
redis:
#服务器地址
host: 120.77.80.135
#端口
port: 6379
#redis密码
password: XXXXXX
#数据库,默认是0
database: 0
#超时时间
timeout: 1209600000ms
lettuce:
pool:
#最大链接数,默认8
max-active: 8
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接,默认8
max-idle: 200
#最小空闲连接,默认0
min-idle: 5
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.guet.APPshareimage.entity
logging:
level:
com.guet.shareimage.mapper: debug
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 加解密使用的密钥
secret: lzdwtl
# JWT的超期限时间(1000*60*60*24*14)14天,即两周
expiration: 1209600000
# JWT 负载中拿到开头
tokenHead: Bearer
role:
roleid: 1
5、其他配置、工具类
5.1、SpringSecurity配置类
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyOncePerRequestFilter myOncePerRequestFilter;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/doLogin").anonymous()
//其余url都要认证才能访问
.anyRequest().authenticated();
}
}
5.2、JSON格式返回配置类
public abstract class JSONAuthentication { /** * 输出JSON * * @param request * @param response * @param obj * @throws IOException * @throws ServletException */ protected void WriteJSON(HttpServletRequest request, HttpServletResponse response, Object obj) throws IOException, ServletException { //这里很重要,否则页面获取不到正常的JSON数据集 response.setContentType("application/json;charset=UTF-8"); //跨域设置 response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Method", "POST,GET"); //输出JSON PrintWriter out = response.getWriter(); out.write(JSON