目录
一、SpringSecurity01
1.2 什么是会话
1.2.1 基于session的认证
1.2.2 基于ToKen的认证
1.3 什么是授权
1.3.1 为什么要授权
1.3.2 SpringSecurity简介
1.4 SpringSecurity入门
1.4.1 pom文件
1.4.2 主启动类
1.4.3 创建控制层
1.4.4 启动项目进行测试
1.5 自定义配置用户
1.6 配置多用户登录
1.6.1 三种基于内存的密码总结
1.7 测试密码加密器
1.8 获取当前用户信息
1.9 Security的权限控制
1.9.1 创建一个UserController类
1.9.2 修改Security配置类
1.9.3 第二种方法(使用注解)
1.9.4 权限不足跳转页面(前后端不分离)
1.10 基于数据库的权限配置
1.10.1 创建项目并创建数据库表
1.10.2 pom文件
1.10.3 配置类
1.10.4 权限配置类
1.10.5 启动类
1.10.6 数据库服务类
1.10.7 dao层
1.10.8 controller层
1.10.9 封装后的数据格式
1.11 SpringSecurity集成thymeleaf
1.11.1 pom文件
1.11.2 权限配置类
1.11.3 登录页面
1.11.4 PageController(处理要跳转的页面)
1.11.5 登录成功处理页面
1.11.6 权限不足访问页面
1.12 SpringSecurity认证授权[源码分析]
1.12.1 结构总览
1.12.2 过滤器链中的几个主要过滤器
1.12.3 Spring security 认证工作流程
编辑
1.12.4 具体工作流程
一、SpringSecurity01
1.1 认证授权的概念
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条,抖音等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。
系统为什么要认证? 认证是为了保护系统的隐私数据与资源,用户的身份合法,方可访问该系统的资源。 认证︰用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法 方可继续访问,不合法则拒绝访问。 常见的用户身份认证方式有: 用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
1.2 什么是会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。
1.2.1 基于session的认证
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id存放到 cookie中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id 也就无效了。
1.2.2 基于ToKen的认证
它的交互流程是,用户认证成功后,服务端生成一个token【也就是uuid】发给客户端,客户端可以放到 cookie 或sessionStorage等存储中,每次请求时带上token,服务端收到token通过验证后即可确认用户身份。
基于session的认证方式由servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。
如今移动互联网时代更多类型的客户端[pC,android,IOS,]需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
1.3 什么是授权
还拿微信来举例子,微信登录成功后用户即可使用微信的功能,
比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以便用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
1.3.1 为什么要授权
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。 授权:授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
可以帮你完成认证授权的框架有哪些? 1. shiro框架。----入门简单,功能简单,可以整合web,javase,整合spring框架时比较麻烦。 2. springsecurity框架。---入门复杂,可以和spring或springboot无缝整合,因为他们都是spring全家桶的一部分。
1.3.2 SpringSecurity简介
官网介绍:Spring Security
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Sprirg应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection依赖主入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。 以上解释来源于百度白科。可以一句话来概括,SpringSecurity 是一个安全框架。可以帮我们完成认证,密码加密,授权,,rememberme的功能
1.4 SpringSecurity入门
创建SpringBoot项目 选中服务 web-->spring web Security-->spring Security
1.4.1 pom文件
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--使用低版本的SpringBoot框架-->
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>testsecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>testsecurity</name>
<description>testsecurity</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<!--springSecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.4.2 主启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication
public class TestsecurityApplication {
public static void main(String[] args) {
SpringApplication.run(TestsecurityApplication.class, args);
}
}
1.4.3 创建控制层
@RestController
public class TestSecurity {
/**
* 简单测试对应的Security的拦截功能
* @return
*/
@GetMapping("index")
public String testSecurity(){
System.out.println("1212121212");
return "hello Security";
}
}
1.4.4 启动项目进行测试
我们发现使用了security后再访问我们自己的接口,security会拦截并跳转到认证页面,认证后才可以访问。默认认证的账号user,密码在控制台。
输入用户名和密码后即可进入对应的页面
密码下面一行显示的是一些列的过滤器链:内容如下
2023-03-08 19:48:32.106 INFO 19272 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@47f08b81,
org.springframework.security.web.context.SecurityContextPersistenceFilter@5467eea4,
org.springframework.security.web.header.HeaderWriterFilter@726a17c4,
org.springframework.security.web.csrf.CsrfFilter@6075b2d3,
org.springframework.security.web.authentication.logout.LogoutFilter@56da52a7,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@21325036,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@368d5c00,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@b9dfc5a,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@8a62297,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7a799159,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@57b9e423,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@2787de58,
org.springframework.security.web.session.SessionManagementFilter@c4c0b41,
org.springframework.security.web.access.ExceptionTranslationFilter@3bcd426c,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4bb8855f]
1.5 自定义配置用户
在application.properties文件中进行配置用户名和密码
spring.security.user.name=admin
spring.security.user.password=123
然后再启动项目,这时控制台将不会再打印密码,能进行登录的用户只有在配置文件中配置的这个“admin”用户,当然也只有这一个用户
1.6 配置多用户登录
创建配置类config包下的MySpringSecurity类进行多用户配置,并对用户的密码进行密码加密
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
/**
* @Bean:创建对象并交给spring容器进行管理
* 密码加密器,自定义构建多个用户时,用户的密码必须使用密码加密器进行加密
* PasswordEncoder是一个接口,这里返回的是他的实现类 BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 重写父类的方法进行自定义构建多个用户名和密码,并进行密码加密
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin") //用户名
.password(passwordEncoder().encode("admin")) //调用上面的方法进行密码加密
.authorities("user:delete","user:query","user:insert") //用户具有的权限
.and()
.withUser("lwl")
.password(passwordEncoder().encode("lwl"))
.authorities("user:query","user:export");
}
}
1.6.1 三种基于内存的密码总结
/** * 简单验证Security的拦截功能 * 1、如果没有配置用户和密码,默认的用户名是user,默认的密码会在控制台输出,粘贴到页面即可 * 2、可以在application.properties配置一个用户名和密码(会自动加密) * 配置文件中配置了用户名和密码之后,初始默认的user用户不会再生效 * 3、如果要配置多个用户名和密码,可以在类中进行声明,声明时要对用户进行分配权限和密码加密 * 如果配置文件中和配置类中都定义了用户和密码时,配置文件中的用户将不能再使用 */
1.7 测试密码加密器
/**
* 测试PasswordEncoder加密
*/
public static void main(String[] args) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode1 = passwordEncoder.encode("123456");
System.out.println(encode1);
String encode2 = passwordEncoder.encode("123456");
System.out.println(encode2);
String encode3 = passwordEncoder.encode("123456");
System.out.println(encode3);
/** 结果打印
* $2a$10$qaPo.XRhOQVgbuPf1UzEHOC5lxcQ.xBf2dP57ZfRqLcQAGF1Ym52e
* $2a$10$ybk0lTgVQXEVvOuU8MeUY.8widuR10/NnQeEKwTch95rmbuohS.aS
* $2a$10$zDq28B6dMIMWxT/lL168nOmUFsS58jCBHa1A1AC5Kwu.jP1F7gP/.
*
* 我们发现加密器对同一个内容加密后结果不同。这样做的原因是安全性更高。
*/
//使用123456可以匹配每一个加密后的密码
boolean matches = passwordEncoder.matches("123456", "$2a$10$zDq28B6dMIMWxT/lL168nOmUFsS58jCBHa1A1AC5Kwu.jP1F7gP/.");
System.out.println("matches = " + matches); //matches = true
boolean matches1 = passwordEncoder.matches("123456", "$2a$10$ybk0lTgVQXEVvOuU8MeUY.8widuR10/NnQeEKwTch95rmbuohS.aS");
System.out.println("matches1 = " + matches1); //matches1 = true
}
注意:只要使用同一个密码加密器,解密也是一样的。
1.8 获取当前用户信息
/**
* 第一种:
* 容器会自动注入给参数为 Principal的参数
* @param principal
* @return
*/
@GetMapping("info")
public Principal info(Principal principal){
return principal;
}
/** 第二种
* 登录成功后springSecurity会把当前的用户信息保存到【SecurityContext】中,也就是类似于Session中
* 所有的用户信息都会封装到 Authentication 中
*/
@GetMapping("getInfo")
public Authentication getInfo(){
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
return authentication;
}
两种方法的结果是一样的,对应上面的多用户配置类
1.9 Security的权限控制
拥有的权限(权限表中的user:query)才可以访问对应的接口资源[query]
1. admin--->user:list user:insert user:delete user:update
2. test---->user:list user:export
1.9.1 创建一个UserController类
package com.example.testsecurity.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("query")
public String query(){
return "用户查询";
}
@GetMapping("insert")
public String insert(){
return "用户添加";
}
@GetMapping("update")
public String update(){
return "用户修改";
}
@GetMapping("delete")
public String delete(){
return "用户删除";
}
@GetMapping("export")
public String export(){
return "用户导出";
}
}
1.9.2 修改Security配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
/**
* @Bean:创建对象并交给spring容器进行管理
* 密码加密器,自定义构建多个用户时,用户的密码必须使用密码加密器进行加密
* PasswordEncoder是一个接口,这里返回的是他的实现类 BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 重写父类的方法进行自定义构建多个用户名和密码,并进行密码加密
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin") //用户名
.password(passwordEncoder().encode("admin")) //调用上面的方法进行密码加密
.authorities("user:delete","user:query","user:insert","user:update") //用户具有的权限
.and()
.withUser("lwl")
.password(passwordEncoder().encode("lwl"))
.authorities("user:query","user:export");
}
/**
* 权限管理:绑定用户和所拥有的权限
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//登录表单放行,不需要认证即可访问
http.formLogin().permitAll();
/**
* 资源和用户权限进行绑定
*/
http.authorizeRequests()
//想要访问/user/query资源,必须要拥有user:query权限
//也可以设置一个权限,访问多个资源
.antMatchers("/user/query").hasAnyAuthority("user:query")
.antMatchers("/user/update").hasAnyAuthority("user:update")
.antMatchers("/user/delete").hasAnyAuthority("user:delete")
.antMatchers("/user/insert").hasAnyAuthority("user:insert")
.antMatchers("/user/export").hasAnyAuthority("user:export");
/**
* admin的权限authorities("user:delete","user:query","user:insert","user:update")
* 所以admin用户登录时,可以访问[/user/query,/user/update,/user/delete,/user/insert]
* lwl的权限authorities("user:query","user:export")
* lwl用户登录后只可以访问[/user/query,/user/export]
* 如果访问的资源不在自己的权限内,那么就会报错[403:权限不够]
*/
//其他的请求,只需要认证过后都可以访问
http.authorizeRequests().anyRequest().authenticated();
}
}
1.9.3 第二种方法(使用注解)
1、启动类中添加注解
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启安全注解
public class TestsecurityApplication {
public static void main(String[] args) {
SpringApplication.run(TestsecurityApplication.class, args);
}
}
2、资源中配置注解
package com.example.testsecurity.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("query")
@PreAuthorize("hasAuthority('user:query')") //拥有这样的一个权限就可以访问上面的资源
public String query(){
return "用户查询";
}
@GetMapping("insert")
@PreAuthorize("hasAuthority('user:insert')")
public String insert(){
return "用户添加";
}
@GetMapping("update")
@PreAuthorize("hasAuthority('user:update')")
public String update(){
return "用户修改";
}
@GetMapping("delete")
@PreAuthorize("hasAuthority('user:delete')")
public String delete(){
return "用户删除";
}
@GetMapping("export")
@PreAuthorize("hasAuthority('user:export')")
public String export(){
return "用户导出";
}
}
3、权限配置类
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
/**
* @Bean:创建对象并交给spring容器进行管理
* 密码加密器,自定义构建多个用户时,用户的密码必须使用密码加密器进行加密
* PasswordEncoder是一个接口,这里返回的是他的实现类 BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 重写父类的方法进行自定义构建多个用户名和密码,并进行密码加密
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin") //用户名
.password(passwordEncoder().encode("admin")) //调用上面的方法进行密码加密
.authorities("user:delete","user:query","user:insert","user:update") //用户具有的权限
.and()
.withUser("lwl")
.password(passwordEncoder().encode("lwl"))
.authorities("user:query","user:export");
}
}
运行项目进行测试,可以实现第一种同样的效果
1.9.4 权限不足跳转页面(前后端不分离)
1、权限配置类
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
/**
* @Bean:创建对象并交给spring容器进行管理
* 密码加密器,自定义构建多个用户时,用户的密码必须使用密码加密器进行加密
* PasswordEncoder是一个接口,这里返回的是他的实现类 BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 重写父类的方法进行自定义构建多个用户名和密码,并进行密码加密
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin") //用户名
.password(passwordEncoder().encode("admin")) //调用上面的方法进行密码加密
.authorities("user:delete","user:query","user:insert","user:update") //用户具有的权限
.and()
.withUser("lwl")
.password(passwordEncoder().encode("lwl"))
.authorities("user:query","user:export");
}
/**
* 权限管理:绑定用户和所拥有的权限
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//登录表单放行,不需要认证即可访问
http.formLogin().permitAll();
//权限不足时跳转的页面
http.exceptionHandling().accessDeniedPage("/403.html");
//其他的请求,只需要认证过后都可以访问
http.authorizeRequests().anyRequest().authenticated();
}
}
2、前端页面
因为这里不想再配置视图解析器,所以直接把前端页面放置在static中
右键选择新建一个HTML文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
权限不足……请联系管理员进行操作
</body>
</html>
启动项目进行访问,当admin用户访问/user/export资源时,就会因为权限不足跳转到这个前端页面
1.10 基于数据库的权限配置
前面的用户都是基于内存的配置,配置一个基于数据库的用户权限配置
1.10.1 创建项目并创建数据库表
创建项目时选择 Developer Tools --> Lombok Web --> Spring web Security --> spring Security SQL --> MySQL Driver
创建数据库表
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80011
Source Host : localhost:3306
Source Schema : security-study
Target Server Type : MySQL
Target Server Version : 80011
File Encoding : 65001
Date: 14/02/2022 21:33:18
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`perid` int(11) NOT NULL AUTO_INCREMENT,
`pername` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`percode` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`perid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, '用户查询', 'user:query');
INSERT INTO `sys_permission` VALUES (2, '用户添加', 'user:insert');
INSERT INTO `sys_permission` VALUES (3, '用户修改', 'user:update');
INSERT INTO `sys_permission` VALUES (4, '用户删除', 'user:delete');
INSERT INTO `sys_permission` VALUES (5, '用户导出', 'user:export');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`roleid` int(11) NOT NULL AUTO_INCREMENT,
`rolename` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`roleid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '管理员');
INSERT INTO `sys_role` VALUES (2, '测试人员');
INSERT INTO `sys_role` VALUES (3, '普通用户');
-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`perid` int(11) NULL DEFAULT NULL,
`roleid` int(11) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES (2, 1);
INSERT INTO `sys_role_permission` VALUES (1, 1);
INSERT INTO `sys_role_permission` VALUES (3, 1);
INSERT INTO `sys_role_permission` VALUES (4, 1);
INSERT INTO `sys_role_permission` VALUES (2, 2);
INSERT INTO `sys_role_permission` VALUES (1, 2);
INSERT INTO `sys_role_permission` VALUES (3, 2);
INSERT INTO `sys_role_permission` VALUES (1, 3);
INSERT INTO `sys_role_permission` VALUES (5, 3);
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`userid` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`userpwd` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`sex` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`userid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, '张三', NULL, '男', '郑州');
INSERT INTO `sys_user` VALUES (2, '李四', NULL, '男', '北京');
INSERT INTO `sys_user` VALUES (3, '王五', NULL, '女', '杭州');
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`userid` int(11) NOT NULL,
`roleid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`userid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);
INSERT INTO `sys_user_role` VALUES (2, 2);
INSERT INTO `sys_user_role` VALUES (3, 3);
SET FOREIGN_KEY_CHECKS = 1;
1.10.2 pom文件
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>securitysql</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>securitysql</name>
<description>securitysql</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
<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>
<!--mp的代码生成器的依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<!--swagger2的坐标:版本过高会导致后面在排除错误的接口文档时发生错误-->
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.9.1.RELEASE</version>
</dependency>
<!--swagger图形化界面-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.7.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
mp代码生成器(旧)
package com.example.securitysql;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class GenerateTest {
/**
* <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.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");//代码生成位置
gc.setAuthor("L");//设置作者
gc.setOpen(false);
gc.setSwagger2(true); //实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);//是否设置为全局配置
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/security?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("lwl@123");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
//pc.setModuleName(scanner("模块名"));
pc.setParent("com.example.securitysql");//设置代码存放的包名
//pc.setXml("");
pc.setEntity("entity");//实体的包
pc.setMapper("dao");//dao的包
pc.setService("service");//service的包
pc.setServiceImpl("service.impl");//实现类的包
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
// String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
//不在java文件夹下面写入mapper文件
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);//设置将字段中的_省略,自动将下一个字母转换为大写字母
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
// 写于父类中的公共字段
// strategy.setSuperEntityColumns("id");//设置是否有公共的父类主键为id字段,不写这一句时,会给主键为id的字段一个@TableId注解
// strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));//输出仅需要的表名
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.execute();
}
}
或者也可以使用database生成代码
1.10.3 配置类
server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=lwl@123
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
1.10.4 权限配置类
package com.example.securitysql.config;
import com.example.securitysql.service.MyUserDetailService;
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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 如果SpringBoot的版本过高,WebSecurityConfigurerAdapter就过时了
*/
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密器,自定义构建多个用户时,用户的密码必须使用密码加密器进行加密
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private MyUserDetailService myUserDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* userDetailsService代表使用的是数据库
* 传递一个userDetailsService对象,查询数据库完成相应的功能
*/
auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//放行登录表单
http.formLogin().permitAll();
//其他资源认证即可访问
http.authorizeRequests().anyRequest().authenticated();
}
}
1.10.5 启动类
package com.example.securitysql;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication
@MapperScan(basePackages = "com.example.securitysql.dao")
//方便在后面进行注解资源绑定
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecuritysqlApplication {
public static void main(String[] args) {
SpringApplication.run(SecuritysqlApplication.class, args);
}
}
1.10.6 数据库服务类
package com.example.securitysql.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.securitysql.dao.SysUserMapper;
import com.example.securitysql.entity.SysPermission;
import com.example.securitysql.entity.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1、根据用户名username查找用户信息
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",username);
SysUser sysUser = userMapper.selectOne(queryWrapper);
if (sysUser!=null){
//2、如果用户不为空,查找用户对应的权限
List<SysPermission> permissionById = userMapper.findPermissionById(sysUser.getUserid());
//3、将权限转变为指定类型的权限集合
//3.1方法一:使用增强for循环
// Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
// for (SysPermission item: permissionById) {
// //这里权限数组中只要权限的权限码
// SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(item.getPercode());
// authorities.add(simpleGrantedAuthority);
// }
/**
* 3.2方法二:使用Stream流
* map:把集合中的元素变成另一种类型
* item -> new SimpleGrantedAuthority(item.getPercode()):将每一个permission类型的值变为SimpleGrantedAuthority类型
* collect(Collectors.toList()):重新收集为集合
*/
List<SimpleGrantedAuthority> authorities = permissionById.stream().map(item -> new SimpleGrantedAuthority(item.getPercode())).collect(Collectors.toList());
/**4、返回指定类型的数据(UserDetails)
* User(String username, String password, Collection<? extends GrantedAuthority> authorities)
* 用户名、密码、一个指定了泛型的集合(集合中是权限)
*/
UserDetails userDetails = new User(sysUser.getUsername(), sysUser.getUserpwd(),authorities);
return userDetails;
}
return null;
}
}
1.10.7 dao层
public interface SysUserMapper extends BaseMapper<SysUser> {
//根据用户id查询权限信息
public List<SysPermission> findPermissionById(Integer userid);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.securitysql.dao.SysUserMapper">
<!--根据用户id查找用户权限:需要三表联查,角色表(sys_role),角色权限表(sys_role_permission),权限表(sys_permission)-->
<select id="findPermissionById" resultType="com.example.securitysql.entity.SysPermission">
select p.* from sys_permission p,sys_role_permission rp,sys_user_role ur
where p.perid=rp.perid and rp.roleid=ur.roleid and ur.userid=#{userid}
</select>
</mapper>
1.10.8 controller层
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@RequestMapping("user")
public class UserController {
/**
* 第一种:
* 容器会自动注入给参数为 Principal的参数
* @param principal
* @return
*/
@GetMapping("info")
public Principal info(Principal principal){
return principal;
}
/** 第二种
* 登录成功后springSecurity会把当前的用户信息保存到【SecurityContext】中,也就是类似于Session中
* 所有的用户信息都会封装到 Authentication 中
*/
@GetMapping("getInfo")
public Authentication getInfo(){
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
return authentication;
}
@GetMapping("query")
@PreAuthorize("hasAuthority('user:query')")
//拥有这样的一个权限就可以访问上面的资源,使用这个注解需要在启动类中加上@EnableGlobalMethodSecurity(prePostEnabled = true)注解
public String query(){
return "用户查询";
}
@GetMapping("insert")
@PreAuthorize("hasAuthority('user:insert')")
public String insert(){
return "用户添加";
}
@GetMapping("update")
@PreAuthorize("hasAuthority('user:update')")
public String update(){
return "用户修改";
}
@GetMapping("delete")
@PreAuthorize("hasAuthority('user:delete')")
public String delete(){
return "用户删除";
}
@GetMapping("export")
@PreAuthorize("hasAuthority('user:export')")
public String export(){
return "用户导出";
}
}
1.10.9 封装后的数据格式
1.11 SpringSecurity集成thymeleaf
前面的项目已经实现了登录效果,在上面项目的基础上,添加或修改,这里想要实现一种,动态按钮效果[不同权限用户登陆后,看到的效果不同]
但是因为springboot内置了tomcat,tomcat不支持jsp模板引擎,在不排除tomcat的情况下,还想使用前端页面,所以这里选择集成thymeleaf实现一个前后端不分离的项目
1.11.1 pom文件
<!--thymeleaf模板引擎依赖
SpringBoot版本过高(2.7.9)时,thymeleaf依赖不兼容
前后端分离项目不使用thymeleaf依赖
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--token-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.2.1</version>
</dependency>
1.11.2 权限配置类
package com.example.securitysql.config;
import com.example.securitysql.service.MyUserDetailService;
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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 如果SpringBoot的版本过高,WebSecurityConfigurerAdapter就过时了
*/
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密器,自定义构建多个用户时,用户的密码必须使用密码加密器进行加密
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private MyUserDetailService myUserDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* userDetailsService代表使用的是数据库
* 传递一个userDetailsService对象,查询数据库完成相应的功能
*/
auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//放行登录表单
http.formLogin()
.loginPage("/login.html") //指定自定义的登录页面
.loginProcessingUrl("/login") //放行自己表单的登录处理路径[因为自己的表单提交路径为 /login]
.successForwardUrl("/success") //登陆成功要跳转的路径,这个请求的路径必须为post
.permitAll();
//权限不足时,跳转到权限不足界面
http.exceptionHandling().accessDeniedPage("/403.html");
//禁用csrf的校验
http.csrf().disable();
//其他资源认证即可访问
http.authorizeRequests().anyRequest().authenticated();
}
}
1.11.3 登录页面
创建在static下面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="/login" method="post">
账号:<input type="text" name="username"/><br>
密码:<input type="text" name="password"/><br>
<input type="submit" value="登录"/>
</form>
</body>
</html>
1.11.4 PageController(处理要跳转的页面)
/**
* 因为要使用视图解析器,所以这里就不使用RestController的风格
*/
@Controller
public class PageController {
@PostMapping("/success")
public String successPage(){
/**
* 如果要更改视图解析器的默认地址,可以在application.properties文件中进行配置
* spring.thymeleaf.prefix=前缀
* spring.thymeleaf.suffix=后缀
*/
return "index"; //视图解析器会解析视图,默认会找/templates/index.html
}
}
1.11.5 登录成功处理页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!--这里是引入了thymeleaf标签,名字定义为sec,否则下面的sec标签不能使用-->
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--这里的名字sec即是上面定义的名字,authorize就是权限-->
<p sec:authorize="hasAuthority('user:insert')"><button onclick="location.href='/user/insert'">增加</button></p >
<p sec:authorize="hasAuthority('user:delete')"> <button onclick="location.href='/user/delete'">删除</button></p >
<p sec:authorize="hasAuthority('user:update')"><button onclick="location.href='/user/update'">修改</button></p >
<p sec:authorize="hasAuthority('user:query')"><button onclick="location.href='/user/query'">查询</button></p >
<p sec:authorize="hasAuthority('user:export')"><button onclick="location.href='/user/export'">导出</button></p >
</body>
</html>
1.11.6 权限不足访问页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
权限不足……请联系管理员进行操作
</body>
</html>
这里的访问错误页面也可以同样编写
启动项目进行测试
随便访问一个资源会跳转到login.html页面,用户登录正确会跳转到index.html页面,因为已经查询到了权限,所以只会显示它拥有的权限,如果访问权限不足的资源,会跳转到403.html页面
1.12 SpringSecurity认证授权[源码分析]
在使用SpringSecurity时验证登录时,为什么自己没有做密码匹配?
1.12.1 结构总览
Spring security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。
根据前边知识的学习,可以通过Filter或AoP等技术来实现,SpringSecurity对web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security 原理。当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的 Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,
下图是 Spring Security过虑器链结构图:
上图说明
FilterchainProxy是一个代理,真正起作用的是FilterChainProxy 中securityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)
进行处理下图是 FilterChainProxy相关类的UML图示:
spring security 功能的实现主要是由一系列过滤器链相互配合完成
1.12.2 过滤器链中的几个主要过滤器
1、SecurityContextPersistenceFi1ter
这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepesitory 中获取SecurityContext,然后把它设置给securityContextHolder.在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository ,同时清除securityContextHolder所持有的SecurityContext;
2、UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和AuthenticationFailureHandler,这些都可以根据需求做相关改变;
3、FilterSecurityInterceptor
是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问;
4、ExceptionTranslationFilter
能够捕获来自Filterchain所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException和 AccessDeniedException,其它的异常它会继续抛出。
1.12.3 Spring security 认证工作流程
认证过程中用到的类(方法)
UsernamePasswordAuthenticationFilter (attemptAuthentication)
ProviderManager (authenticate)
DaoAuthenticationProvider (retrieveUser)
AbstractUserDetailsAuthenticationProvider (authenticate)
1.12.4 具体工作流程
当项目启动后请求资源,服务器一定会通过一系列过滤器链,其中有一个过滤器(UsernamePasswordAuthenticationFilter)一定会经过,所以就从这个类开始探究认证流程的源码
[双击shift,输入UsernamePasswordAuthenticationFilter进入此类]
[按着ctrl,点击authenticate()进入到接口,再进入到ProviderManager实现类中]
[按着ctrl,点击authenticate,进入到AuthenticationProvider接口,在进入到AbstractUserDetailsAuthenticationProvider实现类中]
[按着ctrl,点击retrieveUser,进入到retrieveUser接口,在进入到DaoAuthenticationProvider实现类中,这里会根据用户名,查询用户信息和用户权限]
如果没有没有自己写的类,会使用内存中的UserDetailsService类(通过实现UserDetailsService接口来识别)
查到对象后会返回上一级调用者类中,没有异常,向下执行check方法
[按着ctrl,点击check方法,进入UserDetailsChecker接口,进入AbstractUserDetailsAuthenticationProvider类(对,还是这个类)]
执行完check方法,会继续执行这个类中的additionalAuthenticationChecks方法
[按着ctrl,进入抽象方法additionalAuthenticationChecks,进入实现类DaoAuthenticationProvider]
这些判断都通过之后,会再回到AbstractUserDetailsAuthenticationProvider中继续向下执行createSuccessAuthentication