145.【JWT+SpringSecurity】
JWT+Seacher
- (一)、未使用安全验证的SpringBoot程序
- (二)、认证授权等基本概念
- (三)、SpringSecurity
- (四)、SpringSecurity集成
- (五)、Base64编码 ⭐
- (六)、JWT学习
- (七)、JWT+SpringSecurity+Redis+MySQL 实现认证
- 构造者模式
- 可延长字符串 (string… s)
- !StringUtils.hasText(username),判断username是否为""
- @Param(“aaa”)设定之后的名字,需要和 #{aaa}对应
- assertNotNull(),我们使用断言进行认证操作
- 实体类上必须要加上@mapper,或者在启动类上添加@MapperScan(“com.jsxs”)
- 假如说类的成员变量是一个常量没有赋值,那么可以使用有参构造进行赋值,默认的无参构造不能写出来否则报错;如果非要使用无参构造进行赋值,那么可以在无参构造方法体内进行变量赋值
- 同名session不会被覆盖,因为sessionid。
(一)、未使用安全验证的SpringBoot程序
1.未使用安全验证
(1).依赖
父工程Jwt-SpringBoot的pom.xml文件
<?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.7.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modules>
<module>springBoot-01-hello</module>
</modules>
<!-- 更改打包方式-->
<packaging>pom</packaging>
<groupId>com.jsxs</groupId>
<artifactId>Jwt-SpringBoot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Jwt-SpringBoot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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>
</dependencies>
</project>
子工程springBoot-01-hello的pom.xml文件
<?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>com.jsxs</groupId>
<artifactId>Jwt-SpringBoot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<artifactId>springBoot-01-hello</artifactId>
<properties>
<java.version>1.8</java.version>
</properties>
</project>
(2).控制层
StudentController.java
package com.jsxs.springboot01hello.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author Jsxs
* @Date 2024/2/17 11:43
* @PackageName:com.jsxs.springboot01hello.controller
* @ClassName: StudentController
* @Description: TODO
* @Version 1.0
*/
@RestController
@RequestMapping("/student")
public class StudentController {
/**
* 查找全部信息
* @return I am a student,My name is liming!
*/
@GetMapping("/query")
public String queryInfo() {
return "I am a student,My name is liming!";
}
}
Teacher.java
package com.jsxs.springboot01hello.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author Jsxs
* @Date 2024/2/18 13:17
* @PackageName:com.jsxs.springboot01hello.controller
* @ClassName: Teacher
* @Description: TODO
* @Version 1.0
*/
@RestController
@RequestMapping("/teacher")
public class Teacher {
/**
* 查找全部信息
* @return I am a teacher,My name is teacher!
*/
@GetMapping("/query")
public String queryInfo() {
return "I am a teacher,My name is teacher!";
}
}
AdminController.java
package com.jsxs.springboot01hello.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author Jsxs
* @Date 2024/2/18 13:16
* @PackageName:com.jsxs.springboot01hello.controller
* @ClassName: AdminController
* @Description: TODO
* @Version 1.0
*/
@RestController
@RequestMapping("/admin")
public class AdminController {
/**
* 查找全部信息
* @return I am a student,My name is liming!
*/
@GetMapping("/query")
public String queryInfo() {
return "I am a admin,My name is admin!";
}
}
(3).测试
curl http://localhost:8080/student/query
可以查看到我们访问网址的返回结果
curl -XGET http://localhost:8080/student/query
可以指定请求的方式去访问
curl -v -XGET http://localhost:8080/student/query
查看请求的详细信息
(4).结论
我们未使用安全框架进行保护的时候,我们的所有路径都能够被访问到。
(二)、认证授权等基本概念
1.认证方式(authentlcation)
(1).认证(authentlcation)
(1.1).系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
(1.2).什么是认证 (登入)?
认证: 用户认证就是判断一个用户的身份是否合法的过程。
(1.3).常见的用户身份认证方式
- 用户密码登入
- 二维码登入
- 手机短信登入
- 指纹认证
- 人脸识别
- …
2.会话 (session)
(1).什么是会话?
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。会话就是系统为了保持当前用户的登入状态所提供的机制,常见的基于session方式、token方式。
(2).基于session的认证方式
它的交互流程是: “用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的 ‘session_id’ 存放在 ‘cookie’ 中,这样用户客户端请求时带上 ‘session_id’ 就可以验证服务器端是否存在 ‘session’ 数据,以此完成用户的合法校验,当用户退出系统或’session’过期进行销毁时,客户端的’session_id’也就无效了”。
(3).基于token的认证方式
它的交互流程是: "用户认证成功后,服务端生成一个 ‘token’ 发送给客户端,客户端可以放到 ‘cookie’ 或 ‘localstorage’ 等存储中,每次请求时带上 ‘token’,服务端收到 ‘token’ 通过验证后即可确认用户身份。可以使用 ‘redis’ 存储用户信息(分布式中共享 ‘session’ ) "。
基于session的认证方式由Servlet规范定制,服务端要存储 ‘session’ 信息需要占用内存资源,客户端需要支持 ‘cookie’,基于token的方式则一般不需要服务端存储 ‘token’,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
假如说客户端(浏览器)禁止使用了cookie,session也会受到影响,因为session依赖cookie, token不依赖与cookie,所以token不会受到影响。
cookie会自动携带进网站,但是localstorage不会自动携带到网站中去需要我们前端程序员进行编码实现。
3.授权(authorization)
(1). 为什么要授权 (控制资源被访问)?
因为不同的用户可以访问的资源是不一样的。
(2).什么是授权(给用户颁发权限)
授权: 授权是用户认证通过后,根据用户的权限来控制访问资源的过程。
拥有资源的访问权限则正常访问,没有权限则拒绝访问。
4.RBAC(基于角色的访问控制)
用户、角色、权限 本质: 就是把权限打包给角色(角色拥有一组权限),分配给用户(用户拥有多个角色)。
最少包括五张表 (用户表、角色表、用户角色表、权限表、角色权限表)。
5.Java的安全框架
主要有三种方式:
- shiro: 轻量级的安全框架,提供认证、授权、会话管理、密码管理、缓存管理等功能。
- spring security: 功能比shiro强大,更复杂,权限控制细粒度更高,对OAuth2支持更好,与spring框架无缝集合,使springBoot继承很快捷。
- 自己写: 基于过滤器(filter)和AOP来实现,难度大,没必要。
(三)、SpringSecurity
1.什么是 spring security
Spring Security 是一个能够为基于Spring的企业应用系统提供声明式(注解)的安全访问控制解决方案的安全框架。他提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring的 IOC、DI、AOP功能,减少了企业开发中控制编写大量重复代码的工作。
2.SpringSecurity 入门 (2⭐)
(1).添加依赖
新建模块: Springsecurity-02-hello
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
(2).登入系统
- 登入验证
1.启动项目我们发现:: 我们只添加了一个依赖,然后访问任何一个页面就会出现如下的登入信息,这个页面不是我们自己写的,是SpringSecurity生成的,我们只有验证了账户和密码我们才能成功访问到。 默认账户名为 user,密码在控制台
。
http://localhost:8080/login
- 密码账户验证
假如密码验证成功,我们会访问到正式的页面
我们也发现我们认证成功后并不是说我们只能访问这一个页面,而是全部的页面我们都能够进行访问。
(3).退出系统
http://localhost:8080/logout
(4).结论
引入SpringSecurity依赖后,项目除登入退出外所有资源都会被保护起来经过认证(登入)用户可以访问所有资源,不经过认证用户任何资源也访问不了。
(10).问题 (密码默认随机,如何定制密码?)
所有资源均已保护,但是用户只有一个,密码是随机的,只能在开发环境使用。
3.认证入门
3.1.使用配置文件配置用户名和密码 (3⭐)
(1).添加依赖
1.新建一个 Springsecurity-03-configfile
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
(2).配置文件
2.配置文件
application.yaml
spring:
security:
user:
name: admin
password: 121788
http://localhost:8080/login
登入成功:
(3).查看源码
默认的账户和密码如下:
(4).问题 (如何定制多用户的密码和账户?)
我们已经定制密码了,但是是单用户的。我们怎么对多用户进行定制我们的密码和账户呢?
3.2.基于内存的多用户管理 (4⭐)
(1).添加依赖
1.新建模块springsecuriy-04-inmemory
2.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
(2).配置文件
spring:
security:
user:
name: admin
password: 123456
(3).查看流程图
(4).编写用户配置类
我们这里的配置类中注入一个Bean,这个Bean的主要作用就是让我们自定义实现 用户名、密码和权限的设置
package comjsxs.springsecuriy04inmemory.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.jaas.memory.InMemoryConfiguration;
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.provisioning.InMemoryUserDetailsManager;
/**
* @Author Jsxs
* @Date 2024/3/4 21:12
* @PackageName:comjsxs.springsecuriy04inmemory.config
* @ClassName: MySecurityUserConfig
* @Description: TODO 自定义类实现用户详情服务接口
* @Version 1.0
*/
@Configuration
public class MySecurityUserConfig {
/**
* 我们自定义用户的账户名、密码和权限
* @return
*/
@Bean
public UserDetailsService userDetailsService(){
// 创建2个用户
UserDetails user1 = User.builder().username("aaa").password("123").build();
UserDetails user2 = User.builder().username("aaa").password("123").build();
// 存放到列表中
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
}
继承关系图
(5).运行系统
启动之后我们发现会报错,原因是因为我们不能使用一个明文的密码,不管我们需不需要进行加密,我们都需要配置一个加密器。
(6).配置加密器 (5⭐)
新建模块 springsecurity-05-encode
package com.jsxs.springsecurity05encode.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @Author Jsxs
* @Date 2024/3/4 21:12
* @PackageName:comjsxs.springsecuriy04inmemory.config
* @ClassName: MySecurityUserConfig
* @Description: TODO 自定义类实现用户详情服务接口
* @Version 1.0
*/
@Configuration
public class MySecurityUserConfig {
/**
* 我们自定义用户的账户名、密码和权限
*
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
// 创建2个用户
UserDetails user1 = User.builder().username("student").password(passwordEncoder().encode("123")).roles("student").build();
UserDetails user2 = User.builder().username("teacher").password(passwordEncoder().encode("123")).roles("teacher").build();
// 存放到列表中
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
/**
* 配置密码加密器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCryptPasswordEncoder进行加密
return NoOpPasswordEncoder.getInstance();
}
}
源码分析
(7).再次运行
使用我们在配置类中配置的能够正常访问
使用 application.yaml 中的角色登入不成功
因为产生了覆盖。
(8).问题
- 密码为什么加密? 加密的方式有哪些? 涉及到密码加密问题
- NoOpPasswordEncoder此类已经过期,而且还没有加密,如何解决?
- 以学生的身份登入,发现不但可以访问学生的信息,还可以访问教师的页面和管理员的页面,如何解决?
- 如何动态的创建用户?或者修改密码等(不是把密码写死在代码中)
4.密码处理
(1).为什么要加密?
csdn 密码泄露事件:https://www.chinaz.com/news/2011/1221/227512.shtml
明文可以加密成密文,但是密文不能解密成明文!!
问题来了: 既然密文不能解密成明文,那我们该进行如何处理?
(2).加密方案
密码加密一般采用散列函数,又称为散列算法,哈希函数,这些函数都是单向函数(从明文到密文,反之不行)
常用的散列算法有 MD5 和 SHA
SpringSecurity 提供多种密码加密方案,基本上都实现了 PasswordEncode接口,官方推荐使用BCryptPasswordEncoder。
(3).BCryptPasswordEncoder类初体验
package com.jsxs.springsecurity05encode.config;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import static org.junit.jupiter.api.Assertions.*;
/**
* @Author Jsxs
* @Date 2024/3/5 18:41
* @PackageName:com.jsxs.springsecurity05encode.config
* @ClassName: MySecurityUserConfigTest
* @Description: TODO
* @Version 1.0
*/
@SpringBootTest
class MySecurityUserConfigTest {
@Test
void testBcrypt(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode1 = bCryptPasswordEncoder.encode("123456");
String encode2 = bCryptPasswordEncoder.encode("123456");
String encode3 = bCryptPasswordEncoder.encode("123456");
System.out.println(encode1);
System.out.println(encode2);
System.out.println(encode3);
// 参数一: 明文 。 参数二: 密文
System.out.println(bCryptPasswordEncoder.matches("123456", encode1));
System.out.println(bCryptPasswordEncoder.matches("123456", encode2));
System.out.println(bCryptPasswordEncoder.matches("123456", encode3));
}
}
测试
- 禁止使用main方法进行操作,要使用junit。
- 禁止使用System.out.println(),要使用日志。
- 禁止使用System.out.print(),要使用断言。
assertTrue(flag): 假如为真就不输出,假如为假就输出异常信息。
(4).配置BCryptPasswordEncoder
- 未在配置类中对密码进行加密操作
MySecurityUserConfig.java
package com.jsxs.springsecurity05encode.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @Author Jsxs
* @Date 2024/3/4 21:12
* @PackageName:comjsxs.springsecuriy04inmemory.config
* @ClassName: MySecurityUserConfig
* @Description: TODO 自定义类实现用户详情服务接口
* @Version 1.0
*/
@Configuration
public class MySecurityUserConfig {
/**
* 我们自定义用户的账户名、密码和权限
*
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
// 创建2个用户 ⭐⭐ (我们这里的password()使用我们的明文)
UserDetails user1 = User.builder().
username("student").password("123").roles("student").build();
UserDetails user2 = User.builder().username("teacher").password("123").roles("teacher").build();
// 存放到列表中
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
/**
* 配置密码加密器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCryptPasswordEncoder进行加密 ⭐
return new BCryptPasswordEncoder();
}
}
- 在配置中对密码进行加密
package com.jsxs.springsecurity05encode.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @Author Jsxs
* @Date 2024/3/4 21:12
* @PackageName:comjsxs.springsecuriy04inmemory.config
* @ClassName: MySecurityUserConfig
* @Description: TODO 自定义类实现用户详情服务接口
* @Version 1.0
*/
@Configuration
public class MySecurityUserConfig {
/**
* 我们自定义用户的账户名、密码和权限
*
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
// 创建2个用户 ⭐⭐
UserDetails user1 = User.builder().username("student").password(passwordEncoder().encode("123")).roles("student").build();
UserDetails user2 = User.builder().username("teacher").password(passwordEncoder().encode("123")).roles("teacher").build();
// 存放到列表中
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
/**
* 配置密码加密器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCryptPasswordEncoder进行加密 ⭐
return new BCryptPasswordEncoder();
}
}
(5).问题 (如何查看用户的权限和配置用户权限)
- 我们已经配置了密码加密器,但是我们怎么进行配置用户的权限信息呢?
5. 查看当前登入用户信息及配置用户信息 (7⭐)
新建模块 springsecurity-07-loginuser
(1).获取当前登入用户的信息
package com.jsxs.springsecurity07loginuser.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.security.Principal;
/**
* @Author Jsxs
* @Date 2024/3/5 20:21
* @PackageName:com.jsxs.springsecrity06loginuser.controller
* @ClassName: CurrentLoginUserController
* @Description: TODO
* @Version 1.0
*/
@ResponseBody
@Controller
public class CurrentLoginUserController {
/**
* 使用子类进行认证
* @param authentication
* @return
*/
@GetMapping("/getLoginUser1")
public Authentication getLoginUser1(Authentication authentication){
return authentication;
}
/**
* 使用父类进行认证
* @param principal
* @return
*/
@GetMapping("/getLoginUser2")
public Principal getLoginUser2(Principal principal){
return principal;
}
/**
* 使用安全上下文进行认证
* @param principal
* @return
*/
@GetMapping("/getLoginUser3")
public Principal getLoginUser3(Principal principal){
// 通过安全上下文获取,再获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
}
父类和子类的操作
http://localhost:8080/getLoginUser1
(2).配置用户权限
配置用户权限的两种方式
- 配置 roles
- 配置 authorites
注意事项:
如果给一个用户同时配置 roles 和 authorities,哪个写在后面哪个起作用
。- 配置 roles 时, 权限名会加上 ROLE_
package com.jsxs.springsecurity07loginuser.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @Author Jsxs
* @Date 2024/3/4 21:12
* @PackageName:comjsxs.springsecuriy04inmemory.config
* @ClassName: MySecurityUserConfig
* @Description: TODO 自定义类实现用户详情服务接口
* @Version 1.0
*/
@Configuration
public class MySecurityUserConfig {
/**
* 我们自定义用户的账户名、密码和权限
*
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
// 创建2个用户 ⭐添加了权限
UserDetails user1 = User.builder().username("student").password(passwordEncoder().encode("123")).roles("student").authorities("student:delete","student:add").build();
UserDetails user2 = User.builder().username("teacher").password(passwordEncoder().encode("123")).roles("teacher").authorities("teacher:delete","teacher:add").build();
// 存放到列表中
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
/**
* 配置密码加密器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCryptPasswordEncoder进行加密
return new BCryptPasswordEncoder();
}
}
正常思路:因为我们即配置了 roles 也 配置了 authorities,应该是两个权限都会生效的,可是测试发现谁配置的在后面谁就生效。
(3).问题 (我们对用户进行了认证,但是依然没有多大的实际拦截效果)
- 权限已经配置成功,但是student依然能访问所有的页面,我们该怎么样对页面进行拦截呢
6.授权【URL级别的权限控制】(8⭐)
上面讲的是实现了认证功能,但是受保护的资源是默认的,默认所有认证(登入)用户均可以访问所有资源,不能根据实际情况进行角色管理,要实现授权功能,需要重写 WebSecurityConfigureAdapter
中的 configure
方法。
新建一个模块: springsecurity-08-url,并在模块7的基础上新增配置类
(1).所有请求都拒绝执行
package com.jsxs.springsecurity08url.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @Author Jsxs
* @Date 2024/3/6 13:22
* @PackageName:com.jsxs.springsecurity08url.config
* @ClassName: WebSecurityConfig
* @Description: TODO 我们需要继承一个抽象类
* @Version 1.0
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 重写抽象类中的configure方法,配置我们的权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权http请求
.anyRequest() //任何请求url
.denyAll(); // 执行拒绝操作
// super.configure(http);
}
}
(2).仅允许进入登入页面
我们只有在这里配置了支持所有登入页面才会有 登入页面,假如没有进行配置就不会有登入页面展现
package com.jsxs.springsecurity08url.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @Author Jsxs
* @Date 2024/3/6 13:22
* @PackageName:com.jsxs.springsecurity08url.config
* @ClassName: WebSecurityConfig
* @Description: TODO 我们需要继承一个抽象类
* @Version 1.0
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 重写抽象类中的configure方法,配置我们的权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权http请求
.anyRequest() //任何请求url
.denyAll(); // 执行拒绝操作
http.formLogin().permitAll(); // 允许表单登入页面 ⭐
}
}
就算输入密码正确之后,也会跳转到拦截页面
没有允许登入页面通行的登入页面
(3).允许所有请求操作
也就是不受springsecurity进行保护拦截
package com.jsxs.springsecurity08url.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @Author Jsxs
* @Date 2024/3/6 13:22
* @PackageName:com.jsxs.springsecurity08url.config
* @ClassName: WebSecurityConfig
* @Description: TODO 我们需要继承一个抽象类
* @Version 1.0
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 重写抽象类中的configure方法,配置我们的权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权http请求
.anyRequest() //任何请求url
// .denyAll() // 执行拒绝操作
.permitAll(); // 执行非拒绝操作
http.formLogin().permitAll(); // 允许表单登入
}
}
(4).mvcMatchers 具体化控制权限
没有在mvcMatchers方法中进行配置的都需要进行先等入
package com.jsxs.springsecurity08url.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @Author Jsxs
* @Date 2024/3/6 13:22
* @PackageName:com.jsxs.springsecurity08url.config
* @ClassName: WebSecurityConfig
* @Description: TODO 我们需要继承一个抽象类
* @Version 1.0
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 重写抽象类中的configure方法,配置我们的权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权http请求
.mvcMatchers("/student/**") // 下面的用户匹配这个url_a
.hasAnyAuthority("student:add","student:delete") // 任何拥有这个权限之一的用户可以访问上面的url_a
.mvcMatchers("/teacher/**") // 下面的用户匹配这个url_b
.hasAuthority("ROLE_teacher") // 只有这一个权限的用户才可以访问上面的url_b (因为是权限,在使用hasRole的时候给我们自动添加了Role_;所以我们这里要进行添加················)
.mvcMatchers("/admin/**") // 下面的用户匹配这个url_c
.hasRole("teacher") // 只有这一个权限的用户才可以访问上面的url_c (因为hasRole会给我们自动加Role_;所以我们这里不屑)
.anyRequest() // 剩余所有的请求 ⭐
.authenticated(); // 需要先被验证(登入) ⭐⭐
http.formLogin().permitAll(); // 允许表单登入
}
}
我们登入老师账户进行测试
为什么呢? 因为我们在内存中配置的时候hasRole()方法在上面,authorities()方法在后面,后面的会覆盖掉前面的认证,所以当前拥有的角色就是:
解决办法
package com.jsxs.springsecurity08url.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @Author Jsxs
* @Date 2024/3/6 13:22
* @PackageName:com.jsxs.springsecurity08url.config
* @ClassName: WebSecurityConfig
* @Description: TODO 我们需要继承一个抽象类
* @Version 1.0
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 重写抽象类中的configure方法,配置我们的权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权http请求
.mvcMatchers("/student/**") // 下面的用户匹配这个url_a
.hasAnyAuthority("student:add","student:delete") // 任何拥有这个权限之一的用户可以访问上面的url_a
.mvcMatchers("/teacher/**") // 下面的用户匹配这个url_b
.hasAuthority("teacher:add") // ⭐只有这一个权限的用户才可以访问上面的url_b (因为是权限,在使用hasRole的时候给我们自动添加了Role_;所以我们这里要进行添加················)
.mvcMatchers("/admin/**") // 下面的用户匹配这个url_c
.hasRole("teacher") // 只有这一个权限的用户才可以访问上面的url_c (因为hasRole会给我们自动加Role_;所以我们这里不屑)
.anyRequest() // 剩余所有的请求
.authenticated(); // 需要先被验证
http.formLogin().permitAll(); // 允许表单登入
}
}
老师的能访问、学生的不能访问(被拦截)、管理员不能访问(因为role被覆盖了)
7.授权【方法级别的权限控制】 (9⭐)
上面的认证和授权都是基于URL的,我们可以通过注解进行灵活的配置方法安全。
新建项目 springsecurity-09-method
(1).配置环境
1.添加TeacherService
package com.jsxs.springsecurity09method.service;
/**
* @Author Jsxs
* @Date 2024/3/6 17:39
* @PackageName:com.jsxs.springsecurity09method.service
* @ClassName: TeacherService
* @Description: TODO
* @Version 1.0
*/
public interface TeacherService {
String add();
String update();
String delete();
String query();
}
2.添加TeacherServiceImpl
package com.jsxs.springsecurity09method.service.impl;
import com.jsxs.springsecurity09method.service.TeacherService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @Author Jsxs
* @Date 2024/3/6 17:41
* @PackageName:com.jsxs.springsecurity09method.service.impl
* @ClassName: TeacherServiceImpl
* @Description: TODO
* @Version 1.0
*/
@Service
@Slf4j
public class TeacherServiceImpl implements TeacherService {
@Override
public String add() {
log.info("添加教师成功");
return "添加教师成功";
}
@Override
public String update() {
log.info("修改教师成功");
return "修改教师成功";
}
@Override
public String delete() {
log.info("删除教师成功");
return "删除教师成功";
}
@Override
public String query() {
log.info("查询教师成功");
return "查询教师成功";
}
}
3.添加TeacherController
package com.jsxs.springsecurity09method.controller;
import com.jsxs.springsecurity09method.service.TeacherService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @Author Jsxs
* @Date 2024/3/6 17:44
* @PackageName:com.jsxs.springsecurity09method.controller
* @ClassName: TeacherController
* @Description: TODO
* @Version 1.0
*/
@Slf4j
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@Resource
TeacherService teacherService;
@RequestMapping("/query")
public String queryInfo() {
log.info("进入query");
return teacherService.query();
}
@RequestMapping("/add")
public String addInfo() {
log.info("进入add");
return teacherService.add();
}
@RequestMapping("/update")
public String updateInfo() {
log.info("进入update");
return teacherService.update();
}
@RequestMapping("/delete")
public String deleteInfo() {
log.info("进入delete");
return teacherService.delete();
}
}
4.添加CurrentLoginUserController ,获取安全上下文
package com.jsxs.springsecurity09method.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.security.Principal;
/**
* @Author Jsxs
* @Date 2024/3/5 20:21
* @PackageName:com.jsxs.springsecrity06loginuser.controller
* @ClassName: CurrentLoginUserController
* @Description: TODO
* @Version 1.0
*/
@ResponseBody
@Controller
public class CurrentLoginUserController {
/**
* 使用子类进行认证
* @param authentication
* @return
*/
@GetMapping("/getLoginUser1")
public Authentication getLoginUser1(Authentication authentication){
return authentication;
}
/**
* 使用父类进行认证
* @param principal
* @return
*/
@GetMapping("/getLoginUser2")
public Principal getLoginUser2(Principal principal){
return principal;
}
/**
* 使用安全上下文进行认证
* @param principal
* @return
*/
@GetMapping("/getLoginUser3")
public Principal getLoginUser3(Principal principal){
// 通过安全上下文获取,再获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
}
5.添加用户信息 MySecurityUserConfig 在内存
package com.jsxs.springsecurity08url.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @Author Jsxs
* @Date 2024/3/4 21:12
* @PackageName:comjsxs.springsecuriy04inmemory.config
* @ClassName: MySecurityUserConfig
* @Description: TODO 自定义类实现用户详情服务接口
* @Version 1.0
*/
@Configuration
public class MySecurityUserConfig {
/**
* 我们自定义用户的账户名、密码和权限
*
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
// 创建3个用户,用户1是学生、用户2是老师(拥有查询的权限)、用户三是管理员(拥有增加/删除的权限)
UserDetails user1 = User.builder().username("student").password(passwordEncoder().encode("0")).roles("student").build();
UserDetails user2 = User.builder().username("teacher").password(passwordEncoder().encode("0")).authorities("teacher:query").build();
UserDetails user3 = User.builder().username("admin").password(passwordEncoder().encode("0")).authorities("teacher:add","teacher:delete","teacher:query","teacher:update").build();
// 存放到列表中
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(user1);
userDetailsService.createUser(user2);
userDetailsService.createUser(user3);
return userDetailsService;
}
/**
* 配置密码加密器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCryptPasswordEncoder进行加密
return new BCryptPasswordEncoder();
}
}
6. WebSecurityConfig授权类
我们在这里需要开启 方法安全注解
package com.jsxs.springsecurity09method.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @Author Jsxs
* @Date 2024/3/6 13:22
* @PackageName:com.jsxs.springsecurity08url.config
* @ClassName: WebSecurityConfig
* @Description: TODO 我们需要继承一个抽象类
* @Version 1.0
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // ⭐开启全局方法安全,启用预授权注解和后授权注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 针对URL进行授权
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 1.所有接口都需要认证
http.authorizeRequests().anyRequest().authenticated();
// 2.允许表单登入
http.formLogin().permitAll();
}
}
(2).在serviceImpl上添加注解
package com.jsxs.springsecurity09method.service.impl;
import com.jsxs.springsecurity09method.service.TeacherService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
/**
* @Author Jsxs
* @Date 2024/3/6 17:41
* @PackageName:com.jsxs.springsecurity09method.service.impl
* @ClassName: TeacherServiceImpl
* @Description: TODO
* @Version 1.0
*/
@Service
@Slf4j
public class TeacherServiceImpl implements TeacherService {
@Override
@PreAuthorize("hasAnyAuthority('teacher:add')") // 假如用户有teacher:add权限就可以访问
public String add() {
log.info("添加教师成功");
return "添加教师成功";
}
@Override
@PreAuthorize("hasAnyAuthority('teacher:update')") // 假如用户有teacher:update权限就可以访问
public String update() {
log.info("修改教师成功");
return "修改教师成功";
}
@Override
@PreAuthorize("hasAnyAuthority('teacher:delete')") // 假如用户有teacher:delete权限就可以访问
public String delete() {
log.info("删除教师成功");
return "删除教师成功";
}
@Override
@PreAuthorize("hasAnyAuthority('teacher:query')") // 假如用户有teacher:query权限就可以访问
public String query() {
log.info("查询教师成功");
return "查询教师成功";
}
}
(3). 测试
老师账户进行测试
我们发现我们虽然没有执行到我们的add方法,但是除了add()这个方法外的所有局部变量等都被执行了,由此得出我们 方法级别的权限控制成功
8.SpringSecurity 返回json (10⭐)
新建模块:springsecurity-10-json
(1).搭建环境
- controller包
CurrentLoginUserController 和 teacher、student、admin四个控制层。
package com.jsxs.springsecurity10json.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.security.Principal;
/**
* @Author Jsxs
* @Date 2024/3/5 20:21
* @PackageName:com.jsxs.springsecrity06loginuser.controller
* @ClassName: CurrentLoginUserController
* @Description: TODO
* @Version 1.0
*/
@ResponseBody
@Controller
public class CurrentLoginUserController {
/**
* 使用子类进行认证
* @param authentication
* @return
*/
@GetMapping("/getLoginUser1")
public Authentication getLoginUser1(Authentication authentication){
return authentication;
}
/**
* 使用父类进行认证
* @param principal
* @return
*/
@GetMapping("/getLoginUser2")
public Principal getLoginUser2(Principal principal){
return principal;
}
/**
* 使用安全上下文进行认证
* @param principal
* @return
*/
@GetMapping("/getLoginUser3")
public Principal getLoginUser3(Principal principal){
// 通过安全上下文获取,再获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
}
- vo包 (value_object就是值对象,主要用来存放缓存对象)
我们这里使用构造者模式
package com.jsxs.springsecurity10json.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author Jsxs
* @Date 2024/3/6 19:52
* @PackageName:com.jsxs.springsecurity10json.vo
* @ClassName: HttpResult
* @Description: TODO
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder //使用建造者模式创建对象 ⭐⭐(构造者模式我们可以不用进行)
public class HttpResult {
// 返回给前端的自定义响应码
private Integer code;
// 返回给前端的消息
private String msg;
// 返回给前端的数据
private Object data;
}
(2).用户详情配置类 (MySecurityUserConfig )
1.创建用户详情信息 MySecurityUserConfig
package com.jsxs.springsecurity10json.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @Author Jsxs
* @Date 2024/3/4 21:12
* @PackageName:comjsxs.springsecuriy04inmemory.config
* @ClassName: MySecurityUserConfig
* @Description: TODO 自定义类实现用户详情服务接口
* @Version 1.0
*/
@Configuration
public class MySecurityUserConfig {
/**
* 我们自定义用户的账户名、密码和权限
*
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
// 创建2个用户
UserDetails user1 = User.builder().username("student").password(passwordEncoder().encode("0")).roles("student").build();
UserDetails user2 = User.builder().username("teacher").password(passwordEncoder().encode("0")).roles("teacher").build();
// 存放到列表中
InMemoryUserDetailsManager userDetails = new InMemoryUserDetailsManager();
userDetails.createUser(user1);
userDetails.createUser(user2);
return userDetails;
}
/**
* 配置密码加密器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCryptPasswordEncoder进行加密
return new BCryptPasswordEncoder();
}
}
(3).认证成功处理器配置类(AppAuthenticationSuccessHandler )
2.编写认证成功处理器配置类 AppAuthenticationSuccessHandler ⭐
package com.jsxs.springsecurity10json.config;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.jsxs.springsecurity10json.vo.HttpResult;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Author Jsxs
* @Date 2024/3/6 19:46
* @PackageName:com.jsxs.springsecurity10json.config
* @ClassName: AppAuthenticationSuccessHandler
* @Description: TODO 如果说 认证成功就会调用认证成功处理器
* @Version 1.0
*/
@Configuration
public class AppAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 1.设置响应编码、设置响应类型
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
// 2.使用构造器存储我们的缓存信息
HttpResult result = HttpResult.builder()
.code(1)
.msg("登入成功")
.build();
// 3.将对象转换为Json字符串
String s = JSON.toJSONString(result);
// 4.输出到前端
PrintWriter writer = response.getWriter();
writer.write(s);
writer.flush();
}
}
(4).认证失败处理器配置类(AppAuthenticationFailHandler )
3.编写认证失败处理器类 AppAuthenticationFailHandler ⭐
package com.jsxs.springsecurity10json.config;
import com.alibaba.fastjson2.JSON;
import com.jsxs.springsecurity10json.vo.HttpResult;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Author Jsxs
* @Date 2024/3/6 21:08
* @PackageName:com.jsxs.springsecurity10json.config
* @ClassName: AppAuthenticationFailHandler
* @Description: TODO
* @Version 1.0
*/
@Configuration
public class AppAuthenticationFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 1.设置编码
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("UTF-8");
// 2.编写信息
HttpResult result = HttpResult.builder()
.code(2)
.msg("登入失败")
.build();
// 3.转成JSON
String s = JSON.toJSONString(result);
// 4.输出
PrintWriter writer = response.getWriter();
writer.write(s);
writer.flush();
}
}
(5).用户授权配置类(WebSecurityConfig )
4.编写授权类 WebSecurityConfig
package com.jsxs.springsecurity10json.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import javax.annotation.Resource;
/**
* @Author Jsxs
* @Date 2024/3/6 20:40
* @PackageName:com.jsxs.springsecurity10json.config
* @ClassName: WebSecurityConfig
* @Description: TODO 授权适配器
* @Version 1.0
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
AppAuthenticationSuccessHandler appAuthenticationSuccessHandler;
@Resource
AppAuthenticationFailHandler appAuthenticationFailHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 1.所有页面均需要认证
http.authorizeRequests().anyRequest().authenticated();
// 2.登入被运行。假如登入成功的话那么我们就跳转到下面的appAuthenticationSuccessHandler认证成功处理器
http.formLogin().successHandler(appAuthenticationSuccessHandler).failureHandler(appAuthenticationFailHandler).permitAll();
}
}
- 当然也有退出成功处理器
- 退出失败处理器
9. 使用UserDetailsService获取用户认证信息 (11⭐)
(1).新建一个用户信息 (SecurityUser实现UserDetails接口)
新建子模块: springsecurity-10-userdetailservice
复制09子模块的。
在vo下新建SecurityUser类,该类实现接口UserDetails。(主要是为了帮助我们在内存中创建一个用户)
删除掉配置类 MySecurityUserConfig.java
(添加用户信息的) 和 两个处理器类(认证成功和认证失败)
。
package com.jsxs.springsecurity11userdetailservice.vo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.Collection;
/**
* @Author Jsxs
* @Date 2024/3/7 18:52
* @PackageName:com.jsxs.springsecurity11userdetailservice.vo
* @ClassName: SecurityUser
* @Description: TODO
* @Version 1.0
*/
public class SecurityUser implements UserDetails {
// 用户的权限 ⭐⭐⭐
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
// 使用加密器进行加密密码
@Override
public String getPassword() {
return new BCryptPasswordEncoder().encode("0");
}
// 用户名
@Override
public String getUsername() {
return "admin";
}
// 账户是否未过期? 改成true
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账户是否被锁定? 改成true
@Override
public boolean isAccountNonLocked() {
return true;
}
// 凭据是否过期? 改成true
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 账户是否启用? 改成true
@Override
public boolean isEnabled() {
return true;
}
}
(2).新建一个UserServiceImpl实现UserDetailsService接口
主要作用是实现UserDetailsService接口,并且验证是否输入正确的账户名
- 在SpringSecurity框架中我们不进行密码的处理,SpringSecurity框架自动帮我们处理。
package com.jsxs.springsecurity11userdetailservice.service.impl;
import com.jsxs.springsecurity11userdetailservice.vo.SecurityUser;
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 org.springframework.util.StringUtils;
/**
* @Author Jsxs
* @Date 2024/3/7 19:04
* @PackageName:com.jsxs.springsecurity11userdetailservice.service.impl
* @ClassName: UserServiceImpl
* @Description: TODO 继续UserDetailsService接口
* @Version 1.0
*/
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 假如说 username为""
if (!StringUtils.hasText(username)) {
throw new UsernameNotFoundException("用户名为NULL");
}
// 假如说 用户名输入错误
if (!username.equals("admin")){
throw new UsernameNotFoundException("Not Found the User!");
}
// 执行到这里说明用户名不为" 并且 用户名输入正确
// 返回我们刚才在vo包中创建的信息即可,我们只管用户名就行、密码验证是框架解决的
return new SecurityUser();
}
}
(3).修改WebSecurityConfig(授权配置类)
- 因为我们在SecurityUser类中使用了BCryptPasswordEncoder进行密码加密,所以我们要在唯一的配置类中注入这个组件BCryptPasswordEncoder。
package com.jsxs.springsecurity11userdetailservice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
import javax.annotation.Resource;
/**
* @Author Jsxs
* @Date 2024/3/6 20:40
* @PackageName:com.jsxs.springsecurity10json.config
* @ClassName: WebSecurityConfig
* @Description: TODO 授权适配器
* @Version 1.0
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 1.所有页面均需要认证
http.authorizeRequests().anyRequest().authenticated();
// 2.登入页面所有人都能访问
http.formLogin().permitAll();
}
// 主入我们的PasswordEncoder,目的是为了让框架使用这个BCryptPasswordEncoder进行解密操作 ⭐
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(4).测试
- 因为我们没有设置权限,所以都能访问。
10.基于数据库的认证 (12⭐)
新建模块 springsecurity-12-database-authentication
(1).搭建环境
- 创建数据库
创建数据库 security_study
/*
Navicat Premium Data Transfer
Source Server : myhost
Source Server Type : MySQL
Source Server Version : 80031
Source Host : localhost:3306
Source Schema : security_study
Target Server Type : MySQL
Target Server Version : 80031
File Encoding : 65001
Date: 22/11/2022 09:11:07
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '编号',
`pid` int NULL DEFAULT NULL COMMENT '父级编号',
`name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '名称',
`code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '权限编码',
`type` int NULL DEFAULT NULL COMMENT '0代表菜单1权限2 url',
`delete_flag` tinyint NULL DEFAULT 0 COMMENT '0代表未删除,1 代表已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, 0, '学生管理', '/student/**', 0, 0);
INSERT INTO `sys_menu` VALUES (2, 1, '学生查询', 'student:query', 1, 0);
INSERT INTO `sys_menu` VALUES (3, 1, '学生添加', 'student:add', 1, 0);
INSERT INTO `sys_menu` VALUES (4, 1, '学生修改', 'student:update', 1, 0);
INSERT INTO `sys_menu` VALUES (5, 1, '学生删除', 'student:delete', 1, 0);
INSERT INTO `sys_menu` VALUES (6, 1, '导出学生信息', 'student:export', 1, 0);
INSERT INTO `sys_menu` VALUES (7, 0, '教师管理', '/teacher/**', 0, 0);
INSERT INTO `sys_menu` VALUES (9, 7, '教师查询', 'teacher:query', 1, 0);
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`rolename` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '角色名称,英文名称',
`remark` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ROLE_ADMIN', '管理员');
INSERT INTO `sys_role` VALUES (2, 'ROLE_TEACHER', '老师');
INSERT INTO `sys_role` VALUES (3, 'ROLE_STUDENT', '学生');
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`rid` int NOT NULL COMMENT '角色表的编号',
`mid` int NOT NULL COMMENT '菜单表的编号',
PRIMARY KEY (`mid`, `rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (1, 1);
INSERT INTO `sys_role_menu` VALUES (3, 1);
INSERT INTO `sys_role_menu` VALUES (2, 2);
INSERT INTO `sys_role_menu` VALUES (3, 2);
INSERT INTO `sys_role_menu` VALUES (1, 3);
INSERT INTO `sys_role_menu` VALUES (2, 3);
INSERT INTO `sys_role_menu` VALUES (1, 4);
INSERT INTO `sys_role_menu` VALUES (2, 4);
INSERT INTO `sys_role_menu` VALUES (1, 5);
INSERT INTO `sys_role_menu` VALUES (2, 5);
INSERT INTO `sys_role_menu` VALUES (3, 6);
INSERT INTO `sys_role_menu` VALUES (1, 9);
INSERT INTO `sys_role_menu` VALUES (2, 9);
INSERT INTO `sys_role_menu` VALUES (3, 9);
INSERT INTO `sys_role_menu` VALUES (1, 10);
INSERT INTO `sys_role_menu` VALUES (1, 17);
-- ----------------------------
-- Table structure for sys_role_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_user`;
CREATE TABLE `sys_role_user` (
`uid` int NOT NULL COMMENT '用户编号',
`rid` int NOT NULL COMMENT '角色编号',
PRIMARY KEY (`uid`, `rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_role_user
-- ----------------------------
INSERT INTO `sys_role_user` VALUES (1, 1);
INSERT INTO `sys_role_user` VALUES (2, 2);
INSERT INTO `sys_role_user` VALUES (3, 3);
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` int NOT NULL AUTO_INCREMENT COMMENT '编号',
`username` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '登陆名',
`password` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '密码',
`sex` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '性别',
`address` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '地址',
`enabled` int NULL DEFAULT 1 COMMENT '是否启动账户0禁用 1启用',
`account_no_expired` int NULL DEFAULT 1 COMMENT '账户是否没有过期0已过期 1 正常',
`credentials_no_expired` int NULL DEFAULT 1 COMMENT '密码是否没有过期0已过期 1 正常',
`account_no_locked` int NULL DEFAULT 1 COMMENT '账户是否没有锁定0已锁定 1 正常',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'obama', '$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq', '男', '武汉', 1, 1, 1, 1);
INSERT INTO `sys_user` VALUES (2, 'thomas', '$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq', '男', '北京', 1, 1, 1, 1);
INSERT INTO `sys_user` VALUES (3, 'eric', '$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq', '男', '成都', 1, 1, 1, 1);
SET FOREIGN_KEY_CHECKS = 1;
- 导入依赖
<?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>com.jsxs</groupId>
<artifactId>Jwt-SpringBoot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<artifactId>springsecurity-12-database-authentication</artifactId>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--自动生成代码依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.22</version>
</dependency>
</dependencies>
</project>
- application.properties
# 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security_study?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=121788
# mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# mapper.xml的映射文件
mybatis-plus.mapper-locations=classpath:com/jsxs/springsecurity12databaseauthentication/mapper/xml/*.xml
# 别名映射
mybatis-plus.type-aliases-package=com.jsxs.pojo
# 下划线自动改成驼峰
mybatis-plus.configuration.map-underscore-to-camel-case=true
- 创建分层
使用MyBatis-Plus的逆向工程
package com.jsxs.AutoCode;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.ArrayList;
/**
* @Author Jsxs
* @Date 2023/5/17 15:24
* @PackageName:com.jsxs.SmartCampus
* @ClassName: AutoCode
* @Description: TODO
* @Version 1.0
*/
// 代码自动生成
public class AutoCode {
public static void main(String[] args) {
// 需要构建一个代码自动生成器 对象
AutoGenerator mpg = new AutoGenerator();
// 配置策略
// 1. 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java"); // 输出目录
gc.setAuthor("吉士先生"); //作者
gc.setOpen(false); //是否弹出文件夹在桌面 -否
gc.setFileOverride(false); //是否覆盖原来生成的
gc.setServiceName("%sService"); //去Service的I前缀 正则表达式
gc.setIdType(IdType.ID_WORKER); //主键生成策略
gc.setDateType(DateType.ONLY_DATE); //日期类型
gc.setSwagger2(true); //设置swagger
// 2.丢到容器中
mpg.setGlobalConfig(gc);
//3.设置数据源
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUsername("root");
dsc.setPassword("121788");
dsc.setUrl("jdbc:mysql://localhost:3306/security_study?userUnicode=true&characterEncoding=utf-8");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setDbType(DbType.MYSQL); //数据库类型
mpg.setDataSource(dsc);
//4.包的配置
PackageConfig pc = new PackageConfig();
// pc.setModuleName(""); //模块名字************
pc.setParent("com.jsxs.springsecurity12databaseauthentication"); //代码放在这里
pc.setEntity("pojo"); //实体类的包名
pc.setMapper("mapper"); // 名字
pc.setService("service"); //名字
pc.setController("controller"); //名字
mpg.setPackageInfo(pc);
// 5.策略配置->插件
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("sys_menu", "sys_role", "sys_role_menu", "sys_role_user", "sys_user"); //********映射的表名*********
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true); //自动Lombok
// 逻辑删除
strategy.setLogicDeleteFieldName("deleted");
// 自动填充配置
TableFill gmtCreate = new TableFill("gmt_create", FieldFill.INSERT); //插入
TableFill gmtModified = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE); //修改
ArrayList<TableFill> tableFills = new ArrayList<>();
tableFills.add(gmtCreate);
tableFills.add(gmtModified); //添加到策略
strategy.setTableFillList(tableFills);
// 乐观锁
strategy.setVersionFieldName("version");
strategy.setRestControllerStyle(true);
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);
mpg.execute();
;
}
}
- 添加接口
- 当@Param(“userName”) String userName。一个接口中就一个参数的时候,在xml映射文件中的占位符的变量可以随意写。
- 当参数多的时候,占位符的变量不能随意写了,一定要严格按照@Param(“userName”) String userName注解中的userName写。
entity层
package com.jsxs.springsecurity12databaseauthentication.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.Version;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
/**
* <p>
*
* </p>
*
* @author 吉士先生
* @since 2024-03-07
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="SysUser对象", description="")
public class SysUser implements Serializable {
private static final long serialVersionUID=1L;
@ApiModelProperty(value = "编号")
@TableId(value = "user_id", type = IdType.AUTO)
private Integer userId;
@ApiModelProperty(value = "登陆名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "性别")
private String sex;
@ApiModelProperty(value = "地址")
private String address;
@ApiModelProperty(value = "是否启动账户0禁用 1启用")
private Integer enabled;
@ApiModelProperty(value = "账户是否没有过期0已过期 1 正常")
private Integer accountNoExpired;
@ApiModelProperty(value = "密码是否没有过期0已过期 1 正常")
private Integer credentialsNoExpired;
@ApiModelProperty(value = "账户是否没有锁定0已锁定 1 正常")
private Integer accountNoLocked;
}
Mapper层
package com.jsxs.springsecurity12databaseauthentication.mapper;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 吉士先生
* @since 2024-03-07
*/
@Repository
public interface SysUserMapper extends BaseMapper<SysUser> {
SysUser getByUserName(@Param("userName") String userName);
}
<?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.jsxs.springsecurity12databaseauthentication.mapper.SysUserMapper">
<select id="getByUserName" resultType="com.jsxs.springsecurity12databaseauthentication.pojo.SysUser" parameterType="string">
select *from security_study where username=#{userName};
</select>
</mapper>
Service层
package com.jsxs.springsecurity12databaseauthentication.service;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import com.baomidou.mybatisplus.extension.service.IService;
import org.apache.ibatis.annotations.Param;
/**
* <p>
* 服务类
* </p>
*
* @author 吉士先生
* @since 2024-03-07
*/
public interface SysUserService extends IService<SysUser> {
SysUser getByUserName( String userName);
}
package com.jsxs.springsecurity12databaseauthentication.service.impl;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import com.jsxs.springsecurity12databaseauthentication.mapper.SysUserMapper;
import com.jsxs.springsecurity12databaseauthentication.service.SysUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* <p>
* 服务实现类
* </p>
*
* @author 吉士先生
* @since 2024-03-07
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Resource
SysUserMapper sysUserMapper;
@Override
public SysUser getByUserName(String userName) {
return sysUserMapper.getByUserName(userName);
}
}
(2).新建一个用户信息 (SecurityUser实现UserDetails接口)
在我们的vo内存包中进行设置。
切记我们这里的成员变量是引用对象,所以我们要使用有参构造进行赋值,也就意味着不能使用无参进行创建对象了。
package com.jsxs.springsecurity12databaseauthentication.vo;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @Author Jsxs
* @Date 2024/3/7 21:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.vo
* @ClassName: SecurityUser
* @Description: TODO
* @Version 1.0
*/
public class SecurityUser implements UserDetails {
// 因为是常量需要先赋值,我们这里只能使用有参构造进行赋初始值,不能使用无参构造进行实列化对象
private final SysUser sysUser;
public SecurityUser(SysUser sysUser) {
this.sysUser = sysUser;
}
// 权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
// 数据库的密码是BCry的密码编译器
@Override
public String getPassword() {
return sysUser.getPassword();
}
//
@Override
public String getUsername() {
return sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled() {
return sysUser.getEnabled().equals(1);
}
}
(3).新建一个UserServiceImpl实现UserDetailsService接口
在impl包中
package com.jsxs.springsecurity12databaseauthentication.service.impl;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import com.jsxs.springsecurity12databaseauthentication.service.SysUserService;
import com.jsxs.springsecurity12databaseauthentication.vo.SecurityUser;
import com.sun.org.apache.bcel.internal.generic.ExceptionThrower;
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 org.springframework.util.StringUtils;
import javax.annotation.Resource;
/**
* @Author Jsxs
* @Date 2024/3/8 16:22
* @PackageName:com.jsxs.springsecurity12databaseauthentication.service.impl
* @ClassName: SecurityUserDetaislsService
* @Description: TODO 实现
* @Version 1.0
*/
@Service
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
// 依赖注入Bean
@Resource
SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.getByUserName(username);
if (user==null){
throw new UsernameNotFoundException("该用户不存在");
}
if (!StringUtils.hasText(user.getUsername())) {
throw new UsernameNotFoundException("未根据用户名找到信息");
}
return new SecurityUser(user);
}
}
(4).新建WebSecurityConfig(授权配置类)
在config包中进行创建
进行授权和密码解密器
package com.jsxs.springsecurity12databaseauthentication.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
/**
* @Author Jsxs
* @Date 2024/3/8 16:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.config
* @ClassName: WebSecurityConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启方法授权
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
http.formLogin().permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(5).控制层(Controller)
StudentController.java
package com.jsxs.springsecurity12databaseauthentication.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author Jsxs
* @Date 2024/3/8 17:02
* @PackageName:com.jsxs.springsecurity12databaseauthentication.controller
* @ClassName: StudentController
* @Description: TODO
* @Version 1.0
*/
@RestController
@Slf4j
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
public String queryInfo() {
return "query student";
}
@GetMapping("/add")
public String addInfo() {
return "add student!";
}
@GetMapping("/update")
public String updateInfo() {
return "update student";
}
@GetMapping("/delete")
public String deleteInfo() {
return "delete student!";
}
@GetMapping("/export")
public String exportInfo() {
return "export student!";
}
}
CurrentLoginUserController.java:
查看权限用的
package com.jsxs.springsecurity12databaseauthentication.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.security.Principal;
/**
* @Author Jsxs
* @Date 2024/3/5 20:21
* @PackageName:com.jsxs.springsecrity06loginuser.controller
* @ClassName: CurrentLoginUserController
* @Description: TODO
* @Version 1.0
*/
@ResponseBody
@Controller
public class CurrentLoginUserController {
/**
* 使用子类进行认证
* @param authentication
* @return
*/
@GetMapping("/getLoginUser1")
public Authentication getLoginUser1(Authentication authentication){
return authentication;
}
/**
* 使用父类进行认证
* @param principal
* @return
*/
@GetMapping("/getLoginUser2")
public Principal getLoginUser2(Principal principal){
return principal;
}
/**
* 使用安全上下文进行认证
* @param principal
* @return
*/
@GetMapping("/getLoginUser3")
public Principal getLoginUser3(Principal principal){
// 通过安全上下文获取,再获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
}
TeacherController .java
: 方法授权
package com.jsxs.springsecurity12databaseauthentication.controller;
import lombok.extern.slf4j.Slf4j;
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;
/**
* @Author Jsxs
* @Date 2024/3/8 17:04
* @PackageName:com.jsxs.springsecurity12databaseauthentication.controller
* @ClassName: TeacherController
* @Description: TODO
* @Version 1.0
*/
@RestController
@Slf4j
@RequestMapping("/teacher")
public class TeacherController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('teacher:query')") //预授权
public String queryInfo() {
return "I am a teacher!";
}
}
(6).测试
账户:student 密码:123456
11.基于数据库的授权 (12的基础上二次开发⭐)
(1).查找数据库的权限
从数据库中查询我们的权限数据
select distinct code
from sys_menu INNER JOIN sys_role_menu
ON sys_menu.id=sys_role_menu.mid
INNER JOIN sys_role
ON sys_role.id=sys_role_menu.rid;
(2).编写Mapper层接口
package com.jsxs.springsecurity12databaseauthentication.mapper;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysMenu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 吉士先生
* @since 2024-03-07
*/
@Repository
@Mapper
public interface SysMenuMapper extends BaseMapper<SysMenu> {
List<String> queryPermissionByUserId(@Param("userId") 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.jsxs.springsecurity12databaseauthentication.mapper.SysMenuMapper">
<select id="queryPermissionByUserId" resultType="string">
select distinct code
from sys_menu
INNER JOIN sys_role_menu
ON sys_menu.id = sys_role_menu.mid
INNER JOIN sys_role
ON #{userId} = sys_role_menu.rid;
</select>
</mapper>
(3).编写service层接口
package com.jsxs.springsecurity12databaseauthentication.service;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysMenu;
import com.baomidou.mybatisplus.extension.service.IService;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
* 服务类
* </p>
*
* @author 吉士先生
* @since 2024-03-07
*/
public interface SysMenuService extends IService<SysMenu> {
List<String> queryPermissionByUserId(Integer userId);
}
package com.jsxs.springsecurity12databaseauthentication.service.impl;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysMenu;
import com.jsxs.springsecurity12databaseauthentication.mapper.SysMenuMapper;
import com.jsxs.springsecurity12databaseauthentication.service.SysMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author 吉士先生
* @since 2024-03-07
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
@Resource
SysMenuMapper sysMenuMapper;
@Override
public List<String> queryPermissionByUserId(Integer userId) {
return sysMenuMapper.queryPermissionByUserId(userId);
}
}
(4).修改一个用户信息 (SecurityUser实现UserDetails接口)
在vo包中
(1).写死权限的版本
package com.jsxs.springsecurity12databaseauthentication.vo;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @Author Jsxs
* @Date 2024/3/7 21:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.vo
* @ClassName: SecurityUser
* @Description: TODO
* @Version 1.0
*/
public class SecurityUser implements UserDetails {
// 因为是常量需要先赋值,我们这里只能使用有参构造进行赋初始值,不能使用无参构造进行实列化对象
private final SysUser sysUser;
public SecurityUser(SysUser sysUser) {
this.sysUser = sysUser;
}
// 权限: 这里的返回类型是Collection并且里面的泛型需要继承GrantedAuthority这个接口
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
GrantedAuthority grantedAuthority = new GrantedAuthority() {
@Override
public String getAuthority() {
return "teacher:query";
}
};
authorities.add(grantedAuthority);
return authorities;
}
// 密码
@Override
public String getPassword() {
return sysUser.getPassword();
}
//
@Override
public String getUsername() {
return sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled() {
return sysUser.getEnabled().equals(1);
}
}
- 读取数据库(写活)的版本 ⭐⭐
package com.jsxs.springsecurity12databaseauthentication.vo;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @Author Jsxs
* @Date 2024/3/7 21:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.vo
* @ClassName: SecurityUser
* @Description: TODO
* @Version 1.0
*/
public class SecurityUser implements UserDetails {
// 因为是常量需要先赋值,我们这里只能使用有参构造进行赋初始值,不能使用无参构造进行实列化对象
private final SysUser sysUser;
// 我们这里是GrantedAuthority的子类 ⭐
private List<SimpleGrantedAuthority> authorityList;
// 外界使用set方法进行操作 ⭐⭐
public void setAuthorityList(List<SimpleGrantedAuthority> authorityList) {
this.authorityList = authorityList;
}
public SecurityUser(SysUser sysUser) {
this.sysUser = sysUser;
}
// 权限: 这里的返回类型是Collection并且里面的泛型需要继承GrantedAuthority这个接口,可以使用匿名内部类和SimpleGrantedAuthority ⭐⭐⭐
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityList;
}
// 密码 ⭐⭐⭐
@Override
public String getPassword() {
String myPassword = sysUser.getPassword();
// 进行擦除我们的密码,防止会被传递到前端
sysUser.setPassword(null);
return myPassword;
}
//
@Override
public String getUsername() {
return sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled() {
return sysUser.getEnabled().equals(1);
}
}
(5).修改一个用户信息 (SecurityUser实现UserDetails接口)
package com.jsxs.springsecurity12databaseauthentication.service.impl;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import com.jsxs.springsecurity12databaseauthentication.service.SysMenuService;
import com.jsxs.springsecurity12databaseauthentication.service.SysUserService;
import com.jsxs.springsecurity12databaseauthentication.vo.SecurityUser;
import com.sun.org.apache.bcel.internal.generic.ExceptionThrower;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author Jsxs
* @Date 2024/3/8 16:22
* @PackageName:com.jsxs.springsecurity12databaseauthentication.service.impl
* @ClassName: SecurityUserDetaislsService
* @Description: TODO 实现
* @Version 1.0
*/
@Service
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
// 依赖注入Bean
@Resource
SysUserService sysUserService;
@Resource
SysMenuService sysMenuService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.getByUserName(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
if (!StringUtils.hasText(user.getUsername())) {
throw new UsernameNotFoundException("未根据用户名找到信息");
}
// 1.获取到用户的权限信息
List<String> userPermission = sysMenuService.queryPermissionByUserId(user.getUserId());
// 2.存储用户的权限
ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 3.遍历用户的权限信息,并进行存储
for (String permission : userPermission) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
authorities.add(simpleGrantedAuthority);
}
// 相等于上侧遍历的结果
// List<SimpleGrantedAuthority> authorities = userPermission.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
// 5.存储到我们的权限类中
SecurityUser securityUser = new SecurityUser(user);
securityUser.setAuthorityList(authorities);
return securityUser;
}
}
(6).测试
从数据库取出权限
(四)、SpringSecurity集成
1.SpringSecurity集成Thymeleaf (12的基础上三次开发⭐)
(1).添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
(2).添加静态页面
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登陆</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/login/doLogin" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="uname" value="admin"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="pwd"></td>
<span th:if="${param.error}">用户名或者密码错误</span>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
main.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>系统首页</title>
</head>
<body>
<h1 align="center">系统首页</h1>
<a href="/student/query" sec:authorize="hasAuthority('student:query')">查询学生</a>
<br>
<a href="/student/add" sec:authorize="hasAuthority('student:add')">添加学生</a>
<br>
<a href="/student/update" sec:authorize="hasAuthority('student:update')">更新学生</a>
<br>
<a href="/student/delete" sec:authorize="hasAuthority('student:delete')">删除学生</a>
<br>
<a href="/student/export" sec:authorize="hasAuthority('student:export')">导出学生</a>
<br>
<br><br><br>
<h2><a href="/logout">退出</a></h2>
<br>
</body>
</html>
CRUDE
增删改查导出的页面都是这个模板的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-新增</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
(3).添加控制层
IndexController.java
package com.jsxs.springsecurity12databaseauthentication.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @Author Jsxs
* @Date 2024/3/9 16:49
* @PackageName:com.jsxs.springsecurity12databaseauthentication.controller
* @ClassName: IndexController
* @Description: TODO
* @Version 1.0
*/
@Controller
@RequestMapping("/index")
public class IndexController {
@RequestMapping("/toIndex")
public String toIndex(){
return "main";
}
}
LoginController.java
package com.jsxs.springsecurity12databaseauthentication.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @Author Jsxs
* @Date 2024/3/9 15:47
* @PackageName:com.jsxs.springsecurity12databaseauthentication.controller
* @ClassName: LoginController
* @Description: TODO
* @Version 1.0
*/
@Controller
public class LoginController {
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
}
StudentController.java
package com.jsxs.springsecurity12databaseauthentication.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @Author Jsxs
* @Date 2024/3/8 17:02
* @PackageName:com.jsxs.springsecurity12databaseauthentication.controller
* @ClassName: StudentController
* @Description: TODO
* @Version 1.0
*/
@Controller
@Slf4j
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
public String queryInfo() {
return "student/query";
}
@GetMapping("/add")
public String addInfo() {
return "student/query";
}
@GetMapping("/update")
public String updateInfo() {
return "student/update";
}
@GetMapping("/delete")
public String deleteInfo() {
return "student/delete";
}
@GetMapping("/export")
public String exportInfo() {
return "student/export";
}
}
(4).修改WebSecurityConfig配置类
package com.jsxs.springsecurity12databaseauthentication.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
/**
* @Author Jsxs
* @Date 2024/3/8 16:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.config
* @ClassName: WebSecurityConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
// 自定义我们的登入配置
http.formLogin()
.loginPage("/toLogin") // 配置登入页面
.usernameParameter("uname") // 自定义我们的账户变量名,需要和login一样 ⭐
.passwordParameter("pwd") // 自定义我们的密码变量名,需要和login一样 ⭐
.loginProcessingUrl("/login/doLogin") //点击登入的时候跳到哪里,(⭐这个不需要我们操作框架解决,也就是说不用在controller层编写⭐)
.successForwardUrl("/index/toIndex") // 框架判断成功之后,跳转到 /toIndex
.failureForwardUrl("/toLogin") // 框架判断失败之后,跳转到 /toLogin
.permitAll();
// 自定义我们的退出配置
http.logout().logoutSuccessUrl("/toLogin"); //假如退出成功的话,我们继续跳转到登入页面
http.csrf().disable(); //关闭我们的跨域保护,关闭意味着会被黑客攻击
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(5).启动测试
不管什么角色都能访问,因为我们没有对方法进行授权
2.SpringSecurity优化Thymeleaf
(1).当用户没有某权限时,页面不展示按钮
假如Student没有权限去删除学生,那么我登入账号的时候就不应该展示删除学生的这个超链接。
(2).导入依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
(3).在页面添加标签
1.添加这个标签
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
2.添加权限
sec:authorize="hasAuthority('student:query')"
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>系统首页</title>
</head>
<body>
<h1 align="center">系统首页</h1>
<a href="/student/query" sec:authorize="hasAuthority('student:query')">查询学生</a>
<br>
<a href="/student/add" sec:authorize="hasAuthority('student:add')">添加学生</a>
<br>
<a href="/student/update" sec:authorize="hasAuthority('student:update')">更新学生</a>
<br>
<a href="/student/delete" sec:authorize="hasAuthority('student:delete')">删除学生</a>
<br>
<a href="/student/export" sec:authorize="hasAuthority('student:export')">导出学生</a>
<br>
<br><br><br>
<h2><a href="/logout">退出</a></h2>
<br>
</body>
</html>
3.Springsecurity 集成图片验证码 (12的基础上四次开发)
(1).概述
实际开发中,我们可能需要使用到验证码的功能怎么实现呢?
(2).原理、存在问题、解决思路
我们知道SpringSecurity是通过过滤器来完成的,所以他的解决方案是创建一个过滤器放到 Security 的过滤器链中,在自定义的过滤器中比较验证码,当然我们在进行验证码比对的时候一定要放在密码验证的前面,达到验证码的真实作用。
(3).添加依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.5</version>
</dependency>
(4).静态页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登陆</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/login/doLogin" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="uname" value="thomas"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="pwd"></td>
<span th:if="${param.error}">用户名或者密码错误</span>
</tr>
<tr>
<td>验证码:</td>
<td><input type="text" name="code"> <img src="/code/image" style="height:33px;cursor:pointer;" onclick="this.src=this.src">
<span th:text="${session.captcha_code_error}" style="color: #FF0000;" >username</span>
</td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
(5).修改WebSecurityConfig 配置类
package com.jsxs.springsecurity12databaseauthentication.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
/**
* @Author Jsxs
* @Date 2024/3/8 16:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.config
* @ClassName: WebSecurityConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/code/image").permitAll()
.anyRequest().authenticated();
// 自定义我们的登入配置
http.formLogin()
.loginPage("/toLogin") // 配置登入页面
.usernameParameter("uname") // 自定义我们的账户变量名,需要和login一样
.passwordParameter("pwd") // 自定义我们的密码变量名,需要和login一样
.loginProcessingUrl("/login/doLogin") //点击登入的时候跳到哪里,(⭐这个不需要我们操作框架解决,也就是说不用在controller层编写⭐)
.successForwardUrl("/index/toIndex") // 框架判断成功之后,跳转到 /toIndex
.failureForwardUrl("/toLogin") // 框架判断失败之后,跳转到 /toLogin
.permitAll();
// 自定义我们的退出配置
http.logout().logoutSuccessUrl("/toLogin"); //假如退出成功的话,我们继续跳转到登入页面
http.csrf().disable(); //关闭我们的跨域保护,关闭意味着会被黑客攻击
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(6).编写CaptchaController 控制层
- 出现问题
package com.jsxs.springsecurity12databaseauthentication.controller;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* @Author Jsxs
* @Date 2024/3/9 21:51
* @PackageName:com.jsxs.springsecurity12databaseauthentication.controller
* @ClassName: CaptchaController
* @Description: TODO
* @Version 1.0
*/
@Controller
@Log4j2
public class CaptchaController {
@Resource
HttpServletRequest request;
// ⭐
@Resource
HttpServletResponse response;
@GetMapping("/code/image")
public void getCaptchaCode() throws IOException {
// 1.创建一个验证码,宽度为200、长度为100、代码数为2、干扰元素圆的个数为20
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 2, 20);
String code = circleCaptcha.getCode();
log.info("生成的验证码为:{}",code);
// 2.将验证码存储到Session中
HttpSession session = request.getSession();
session.setAttribute("CAPTCHA_CODE",code);
// 3.利用工具放到输出流中,第一个参数是图片、第二个参数是格式、第三个参数是输出流。
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(circleCaptcha.getImage(),"jpeg",outputStream);
}
}
当我们使用注入的方式进行操作输出流的时候,因为注入
默认采用的是单实列,然后输出流又是只能有一个在运行
所以当我们二次调用刷新图片的时候会出现输出流已经被
使用的运行时异常信息。
- 解决问题 (未完全解决)
保留注入的方式,但是需要在方法的最后关闭输出流
package com.jsxs.springsecurity12databaseauthentication.controller;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* @Author Jsxs
* @Date 2024/3/9 21:51
* @PackageName:com.jsxs.springsecurity12databaseauthentication.controller
* @ClassName: CaptchaController
* @Description: TODO
* @Version 1.0
*/
@Controller
@Log4j2
public class CaptchaController {
@Resource
HttpServletRequest request;
@Resource
HttpServletResponse response;
@GetMapping("/code/image")
public void getCaptchaCode() throws IOException, InterruptedException {
// 1.创建一个验证码,宽度为200、长度为100、代码数为2、干扰元素圆的个数为20
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 2, 20);
String code = circleCaptcha.getCode();
log.info("生成的验证码为:{}",code);
// 2.将验证码存储到Session中
HttpSession session = request.getSession();
session.setAttribute("CAPTCHA_CODE",code);
// 3.利用工具放到输出流中,第一个参数是图片、第二个参数是格式、第三个参数是输出流。
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(circleCaptcha.getImage(),"jpeg",outputStream);
outputStream.close(); // ⭐关闭
}
}
- 解决问题 (完全解决)
package com.jsxs.springsecurity12databaseauthentication.controller;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* @Author Jsxs
* @Date 2024/3/9 21:51
* @PackageName:com.jsxs.springsecurity12databaseauthentication.controller
* @ClassName: CaptchaController
* @Description: TODO
* @Version 1.0
*/
@Controller
@Log4j2
public class CaptchaController {
@Resource
HttpServletRequest request;
@GetMapping("/code/image")
public void getCaptchaCode(HttpServletResponse response) throws IOException, InterruptedException {
// 1.创建一个验证码,宽度为200、长度为100、代码数为2、干扰元素圆的个数为20
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 2, 20);
String code = circleCaptcha.getCode();
log.info("生成的验证码为:{}",code);
// 2.将验证码存储到Session中
HttpSession session = request.getSession();
session.setAttribute("CAPTCHA_CODE",code);
// 3.利用工具放到输出流中,第一个参数是图片、第二个参数是格式、第三个参数是输出流。
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(circleCaptcha.getImage(),"jpeg",outputStream);
outputStream.close();
}
}
(7).创建验证码过滤器
所有的过滤器默认都是在请求的前面,所以我们要进行判断
package com.jsxs.springsecurity12databaseauthentication.filter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author Jsxs
* @Date 2024/3/9 22:50
* @PackageName:com.jsxs.springsecurity12databaseauthentication.filter
* @ClassName: ValidateCodeFilter
* @Description: TODO
* @Version 1.0
*/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 1.判断是否是/login/doLogin路径 (因为其他的路径不需要进行验证码的比较)
if (!request.getRequestURI().equals("/login/doLogin")) {
doFilter(request,response,filterChain); // 不是登入的请求直接放行
return;
}
validateCode(request,response,filterChain);
}
private void validateCode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// 2.获取前端的验证码
String code = request.getParameter("code").trim();
// 3.获取session中的验证码
String captcha_code = (String) request.getSession().getAttribute("CAPTCHA_CODE");
// 4.假如用户没有输入
if (!StringUtils.hasText(code)) {
request.getSession().setAttribute("captcha_code_error","验证码不能为空");
request.getSession().removeAttribute("captcha_code_error"); // 清除session减少服务器压力
response.sendRedirect("/toLogin");
return;
}
// 5.假如session没有传递进来
if (!StringUtils.hasText(captcha_code)) {
request.getSession().setAttribute("captcha_code_error","获取验证码错误");
request.getSession().removeAttribute("captcha_code_error"); // 清除session减少服务器压力
response.sendRedirect("/toLogin");
return;
}
// 6.假如验证码错误
if (!code.equals(captcha_code)) {
request.getSession().setAttribute("captcha_code_error","验证码输入错误,请重新输入");
request.getSession().removeAttribute("captcha_code_error"); // 清除session减少服务器压力
response.sendRedirect("/toLogin");
return;
}
request.getSession().removeAttribute("CAPTCHA_CODE"); // 清除session减少服务器压力
// 7.比较成功
this.doFilter(request,response,filterChain);
}
}
(8).将验证码过滤器配置到SpringSecurity执行连
在密码和账户验证之前进行执行我们的过滤器
package com.jsxs.springsecurity12databaseauthentication.config;
import com.jsxs.springsecurity12databaseauthentication.filter.ValidateCodeFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
/**
* @Author Jsxs
* @Date 2024/3/8 16:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.config
* @ClassName: WebSecurityConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
ValidateCodeFilter validateCodeFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 添加到security的密码用户认证执行链前 ⭐⭐
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
// 将验证码页面进行方向
http.authorizeRequests()
.mvcMatchers("/code/image").permitAll()
.anyRequest().authenticated();
// 自定义我们的登入配置
http.formLogin()
.loginPage("/toLogin") // 配置登入页面
.usernameParameter("uname") // 自定义我们的账户变量名,需要和login一样
.passwordParameter("pwd") // 自定义我们的密码变量名,需要和login一样
.loginProcessingUrl("/login/doLogin") //点击登入的时候跳到哪里,(⭐这个不需要我们操作框架解决,也就是说不用在controller层编写⭐)
.successForwardUrl("/index/toIndex") // 框架判断成功之后,跳转到 /toIndex
.failureForwardUrl("/toLogin") // 框架判断失败之后,跳转到 /toLogin
.permitAll();
// 自定义我们的退出配置
http.logout().logoutSuccessUrl("/toLogin"); //假如退出成功的话,我们继续跳转到登入页面
http.csrf().disable(); //关闭我们的跨域保护,关闭意味着会被黑客攻击
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(9).测试
(五)、Base64编码 ⭐
1.什么是Base64
所谓Base64,就是说64个字符: “小写字母a-z、大写字母A-Z、阿拉伯数字0-9、+号、/号”,作为一个基本的字符集。
(1).进行base64加密
echo "hello world"
echo -n "hello world"
echo -n "hello world" | base64
在base编码中 “=” 只能出现在最后面,且最多为2个"=“,最少为0个”="。
(2).是否可以使用base64进行密码加密?
进行解密的操作
echo -n "base64的编码" | base64 -d
不可以,因为是可逆的。
(3).对文件进行base64加密实操
- 进行加密视频文件
对蛋仔游戏.mp4进行加密转换成1.txt
base64 蛋仔游戏.mp4>1.txt
- 进行解密视频文件
base64 1.txt>1.mp4 -d
2.Base64和BaseUrl的区别
Base64Url是一种在Base64的基础上形成的新的编码方式,为了编码能在网络安全中顺畅传输,需要对Base64进行的编码,特别是互联网中的。
Base64Url基本步骤:
- 明文使用Base64编码
- 在Base64编码的基础上进行以下处理
- (1).去除尾部的"="
- (2).把"+“替换成”-"
- (3).斜线"/“替换成下划线”_"。
3.跨域认证问题和JWT实现登入原理
(1).跨域认证问题
互联网服务离不开用户认证。一般流程是下面这样
- 用户向服务器发送用户名和密码
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登入时间等等。
- 服务器向用户返回一个jsession_id,写入用户的Cookie。
- 用户随后的每一次请求,都会通过Cookie,将 session_id 传回服务器。
- 服务器收到session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。举例来说,A 网站和 B网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
- 一种解决方案是
session
数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。 - 另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。 服务器不存数据,客户端存,服务器解析就行了。
(2).JWT实现登入原理
说明: JWT只通过算法实现对Token合法性的验证,不依赖数据库,缓存等存储系统,因此可以做到跨服务器验证,只要密钥和算法相同,不同服务器生成的Token可以进行互相验证。
(六)、JWT学习
1.简介
SON Web Token(JWT)是一个开放标准(RFC7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。 JWT可以使用密钥(使用 HMAE 算法)或使用 RSA或ECDSA 的公钥/私钥对进行签名。
- 官方网址:https://jwt.io/
- 调试页面:https://iwt.io/
- 学习文档:https://iwt.io/introduction/
2.用途
- 授权: 这是我们使用JWT最广泛的应用场景。一次用户登录,后续请求将会包含JWT对于那些合法的token,允许用户连接路由,服务和资源。目前JWT广泛应用在SSO(single Sign On)(
单点登录
)上, 因为他们开销很小并且可以在不同领域轻松使用。 - 信息交换: JSON Web Token 是一种在各方面之间安全信息传输的好的方式 因为JWT 可以签名。例如,使用公钥/私钥对-您可以确定发件人是他们所说的人。此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。
3.JWT的组成
一个JWT有三部分组成,他们之间用 “.” 分开。
- Header(头部)-----base64Ur1 编码的 Json字符串
- Payload(载荷)—base64ur1编码的 Json字符串
- Signature(签名)—使用指定算法,通过 Header 和 Playload加盐计算的字符串。
一个JWT看起来类似于下面这样: XXXX.YYYY.ZZZZ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
(1).Header
此部分由两部分组成:
- 一部分是token的类型,目前只能是Jwt
- 另一部分是签名算法,比如 HMAC、SHA256、RSA
{
"alg": "HS256",
"typ": "JWT"
}
示列
echo -n '{"alg":"HS256","typ":"JWT"}' | base64
(2).Payload (负载)
token 的第二部分是 payload(有效负载),其中包含 claims(声明)。Claims 是关于一个实体(通常是用户) 和 其他数据类型的声明。
claims有三种类型: registered
,public
,and private
claims.
- Registered(已注册的声明): 这些是一组预定义声明,不是强制性的,但建议使用,以提供一组有用的,可互操作的声明。其中一些是:
iss
(发行人),exp
(到期时间),sub
(主题),aud
(观众),and others。(请注意,声明名称只有三个字符,因为JWT意味着紧凑。)
JWT 规定了7个官方字段,供选用。
- iss (issuer):签发人
- exp(expiration time):过期时间
- sub(subject):主题
- aud(audience):受众
- nbf(Not Before):生效时间
- iat(lssued At):签发时间
- jti (JWT ID):编号
除了官方字段,你还可以在这部分定义私有字段
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
注意: JWT默认是不加密的,任何人都可以读到,所以不要把私密信息(密码、手机号)等放在这个位置。 这个JSON字符串也要采用Base64Url算法转换成字符串。
- Public(公开声明): 这些可以由使用 JWT的人随意定义。但为避免冲突,应在 IANAJSONWeb Token Registry 中定义它们,或者将其定义为包含防冲突命名空间的 URI。
- private (私人声明): 这些声明是为了在同意使用它们的各方之间共享信息而创建的,并且既不是注册声明也不是公开声明。
(3).Signature (保证数据安全的)
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后使用 Header 里面指定的签名算法(默认是 HMACSHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
) secret base64 encoded
算出签名以后,把 Header、Payload、signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
4.JWT的使用方式【重点】
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务器通信,都要带上这个JWT。你可以把它放在 Cookie 里面自动发送(不用我们手动),但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面。
5.JWT的特点
(1).JWT优点
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- JWT 不加密的情况下,不能将秘密数据写入JWT。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用JWT,可以降低服务器查询数据库的次数。
(2).Jwt缺点
JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑(JWT的登出问题)。
就是因为服务端无状态了正常情况下,修改了密码后就会跳转到登录页面 :修改成功后清空浏览器保存的 token 了。
如何解决?
因为服务端不保留 token 我用之前的 token 还是可以继续访问的,从有状态(后端也会存一个)的变成无状态的了,我们就要把它从无状态再变成有状态了。结合Redis缓存进行操作。
6.Java中使用JWT
新建模块 jwt-learn
(1).添加依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
(2).工具类
package com.jsxs.springsecurity13jwtlearn1.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.log4j.Log4j2;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author Jsxs
* @Date 2024/3/10 14:30
* @PackageName:com.jsxs.springsecurity13jwtlearn1.util
* @ClassName: JwtUtils
* @Description: TODO
* @Version 1.0
*/
@Log4j2
public class JwtUtils {
private static final String SECRET = "secret888"; //密钥
// 创建Token
public String createJwt(Integer userId, String userName, List<String> authList) throws UnsupportedEncodingException {
// 1.签发时间
Date issuedDate = new Date();
// 2.过期时间
Date expireDate = new Date(issuedDate.getTime() + 1000 * 10);
// Date expireDate = new Date(issuedDate.getTime() + 1000 * 60 * 60 * 2);
// 3.Header 信息
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("alg", "HS256");
headerClaims.put("typ", "JWT");
// 4.身体信息
return JWT.create().withHeader(headerClaims)
.withIssuer("thomas") //签发人 (谁生成的这个JWT)
.withIssuedAt(issuedDate) //签发时间
.withExpiresAt(expireDate) //过期时间
.withClaim("userId", userId) // 自定义
.withClaim("userName", userName) // 自定义
.withClaim("userAuth", authList.toString()) //自定义
.sign(Algorithm.HMAC256(SECRET)); // 使用HS256进行签名,使用SECRET作为密钥
}
//
public boolean verifyToken(String jwtToken) {
try {
// 1.创建校验器
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
return true;
} catch (Exception e) {
log.error("token验证有误");
return false;
}
}
public void getMyClaim(String jwt) throws UnsupportedEncodingException {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
if (!this.verifyToken(jwt)) {
System.out.println("JWT过期或失效!");
return;
}
DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
Integer userId = decodedJWT.getClaim("userId").asInt();
String userName = decodedJWT.getClaim("userName").asString();
String userAuth = decodedJWT.getClaim("userAuth").asString();
System.out.println(userId+" "+userName+" "+userAuth);
}
}
(3).测试JWT
package com.jsxs.springsecurity13jwtlearn1.util;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* @Author Jsxs
* @Date 2024/3/10 14:57
* @PackageName:com.jsxs.springsecurity13jwtlearn1.util
* @ClassName: JwtUtilsTest
* @Description: TODO
* @Version 1.0
*/
@SpringBootTest
class JwtUtilsTest {
@Test
void createJwt() throws UnsupportedEncodingException, InterruptedException {
JwtUtils jwtUtils = new JwtUtils();
List<String> authList = Arrays.asList("student:query", "student:add", "student:update");
// 1.创建JWT
String jwt = jwtUtils.createJwt(19, "obama", authList);
System.out.println(jwt);
// 2.验证JWT
System.out.println(jwtUtils.verifyToken(jwt));
System.out.println("过期前获得的数据: ");
jwtUtils.getMyClaim(jwt);
Thread.sleep(12000);
// 3.过期后_再次验证过期后的JWT
System.out.println(jwtUtils.verifyToken(jwt));
System.out.println("过期后获得的数据: ");
jwtUtils.getMyClaim(jwt);
}
}
(4).JWT的总结
JWT 就是一个加密的带用户信息的字符串,没学习JWT之前,我们在项目中都是返回一个基本的字符串,然后请求时带上这个字符事,再从 session
或者 redis
_中(共享 session)获取当前用户,学过JWT以后我们可以把用户信息直接放在字符申返回给前端,然后用户请求时带过来,我们是在服务器进行解析拿到当前用户,这就是两种登录方式,这两种方式有各自的优缺点。
(七)、JWT+SpringSecurity+Redis+MySQL 实现认证
1.JWT+SpringSecurity (12的基础上五次开发)
(1).添加依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
(2).application.properties中配置密钥
# 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security_study?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=121788
#thymeleaf
#开发的时候我们不使用缓存,我们只需要点击小锤子就能同步部署
spring.thymeleaf.cache=false
# 检查模板
spring.thymeleaf.check-template=true
# mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# mapper.xml的映射文件
mybatis-plus.mapper-locations=classpath:com/jsxs/springsecurity12databaseauthentication/mapper/xml/*.xml
# 别名映射
mybatis-plus.type-aliases-package=com.jsxs.pojo
# 下划线自动改成驼峰
mybatis-plus.configuration.map-underscore-to-camel-case=true
# 密钥
my.secret="secret888"
(3).jwt工具类
package com.jsxs.springsecurity12databaseauthentication.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.util.*;
/**
* @Author Jsxs
* @Date 2024/3/10 14:30
* @PackageName:com.jsxs.springsecurity13jwtlearn1.util
* @ClassName: JwtUtils
* @Description: TODO
* @Version 1.0
*/
@Log4j2
@Component
@PropertySource(value = "classpath:application.properties")
public class JwtUtils {
@Value("${my.secret}")
private String SECRET; //密钥
// 创建Token
public String createJwt(String userInfo, List<String> authList) throws UnsupportedEncodingException {
// 1.签发时间
Date issuedDate = new Date();
// 2.过期时间
Date expireDate = new Date(issuedDate.getTime() + 1000 * 60*60 *24);
// Date expireDate = new Date(issuedDate.getTime() + 1000 * 60 * 60 * 24);
// 3.Header 信息
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("alg", "HS256");
headerClaims.put("typ", "JWT");
// 4.身体信息
return JWT.create().withHeader(headerClaims)
.withIssuer("thomas") //签发人 (谁生成的这个JWT)
.withIssuedAt(issuedDate) //签发时间
.withExpiresAt(expireDate) //过期时间
.withClaim("user_info", userInfo) // 自定义链 (用户信息)
.withClaim("userAuth", authList.toString()) //自定义链 (权限)
.sign(Algorithm.HMAC256(SECRET)); // 使用HS256进行签名,使用SECRET作为密钥
}
// 验证Token
public boolean verifyToken(String jwtToken) {
try {
// 1.创建校验器
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
log.info("token验证成功");
return true;
} catch (Exception e) {
log.error("token验证失败,失败的原因可能是token服务过期");
return false;
}
}
// 获得用户信息
public String getUserInfoFromToken(String jwt) throws UnsupportedEncodingException {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
if (!this.verifyToken(jwt)) {
log.error("JWT过期或失效!");
return "";
}
DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
String userInfo = decodedJWT.getClaim("user_info").asString();
return userInfo;
}
// 获得权限列表
public List<String> getUserAuthFromToken(String jwt) throws UnsupportedEncodingException {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
if (!this.verifyToken(jwt)) {
log.error("JWT过期或失效!");
return null;
}
DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
String userInfo = decodedJWT.getClaim("userAuth").asString();
ArrayList<String> authList = new ArrayList<>();
String[] auths = (((userInfo.replaceAll(" ", "")).replace("[", "")).replace("]", "")).split(",");
for (String auth : auths) {
authList.add(auth);
}
return authList;
}
}
(4).添加响应类
package com.jsxs.springsecurity12databaseauthentication.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author Jsxs
* @Date 2024/3/6 19:52
* @PackageName:com.jsxs.springsecurity10json.vo
* @ClassName: HttpResult
* @Description: TODO
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder //使用建造者模式创建对象
public class HttpResult {
// 返回给前端的自定义响应码
private Integer code;
// 返回给前端的消息
private String msg;
// 返回给前端的数据
private Object data;
}
(5).修改SecurityUser类
我们这里对这个类进行获取类对象
public SysUser getSysUser() {
return sysUser;
}
package com.jsxs.springsecurity12databaseauthentication.vo;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @Author Jsxs
* @Date 2024/3/7 21:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.vo
* @ClassName: SecurityUser
* @Description: TODO
* @Version 1.0
*/
public class SecurityUser implements UserDetails {
// 因为是常量需要先赋值,我们这里只能使用有参构造进行赋初始值,不能使用无参构造进行实列化对象
private final SysUser sysUser;
public SysUser getSysUser() {
return sysUser;
}
// 我们这里是GrantedAuthority的子类
private List<SimpleGrantedAuthority> authorityList;
// 外界使用set方法进行操作
public void setAuthorityList(List<SimpleGrantedAuthority> authorityList) {
this.authorityList = authorityList;
}
public SecurityUser(SysUser sysUser) {
this.sysUser = sysUser;
}
// 权限: 这里的返回类型是Collection并且里面的泛型需要继承GrantedAuthority这个接口
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityList;
}
// 密码
@Override
public String getPassword() {
String myPassword = sysUser.getPassword();
// 进行擦除我们的密码,防止会被传递到前端
sysUser.setPassword(null);
return myPassword;
}
//
@Override
public String getUsername() {
return sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled() {
return sysUser.getEnabled().equals(1);
}
}
(6).添加JWT过滤器
package com.jsxs.springsecurity12databaseauthentication.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import com.jsxs.springsecurity12databaseauthentication.util.JwtUtils;
import com.jsxs.springsecurity12databaseauthentication.vo.HttpResult;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author Jsxs
* @Date 2024/3/11 16:05
* @PackageName:com.jsxs.springsecurity12databaseauthentication.filter
* @ClassName: JwtCheckFilter
* @Description: TODO
* @Version 1.0
*/
@Component
@Log4j2
public class JwtCheckFilter extends OncePerRequestFilter {
@Resource
JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 如果是登入的请求直接放行
if (request.getRequestURI().equals("/toLogin") || request.getRequestURI().equals("/code/image")) {
doFilter(request, response, filterChain);
return;
}
String jwt = request.getHeader("Authorization");
if (!StringUtils.hasText(jwt)) {
log.error("服务端不存在消息头是Authorization的token");
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt生成失败")
.data(null)
.build();
printToken(request, response, JSON.toJSONString(httpResult));
return;
}
// 1.因为在token的消息头中我们就要对其进行添加 "bearer "所以我们验证的时候要先删除
jwt = jwt.replace("bearer ", "");
if (!jwtUtils.verifyToken(jwt)) {
log.error("获取失败,从请求头中获取的token是:{}", jwt);
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt生成失败")
.data(null)
.build();
printToken(request, response, JSON.toJSONString(httpResult));
return;
}else {
// HttpResult httpResult = HttpResult.builder()
// .code(0)
// .msg("jwt生成成功")
// .data(jwt)
// .build();
// printToken(request, response, JSON.toJSONString(httpResult));
String userInfo = jwtUtils.getUserInfoFromToken(jwt);
List<String> userAuthFromToken = jwtUtils.getUserAuthFromToken(jwt);
// 将用户信息再转换成sysUser
SysUser sysUser = JSONObject.parseObject(userInfo,SysUser.class);
// 将权限再次转换为SimpleGrantedAuthority
List<SimpleGrantedAuthority> grantedAuthorityList = userAuthFromToken.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
// SpringSecurity接管 -> 用户名密码认证token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(sysUser,null,grantedAuthorityList);
// 把usernamePasswordAuthenticationToken放入SpringSecurity安全上下文中
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
doFilter(request, response, filterChain);
}
}
private void printToken(HttpServletRequest request, HttpServletResponse response, String result) throws IOException {
// 1.设置编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(result);
writer.flush();
writer.close();
}
}
(7).添加认证成功处理器类
package com.jsxs.springsecurity12databaseauthentication.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import com.jsxs.springsecurity12databaseauthentication.util.JwtUtils;
import com.jsxs.springsecurity12databaseauthentication.vo.HttpResult;
import com.jsxs.springsecurity12databaseauthentication.vo.SecurityUser;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author Jsxs
* @Date 2024/3/10 21:06
* @PackageName:com.jsxs.springsecurity12databaseauthentication.config
* @ClassName: MyAuthenticationSuccessHandler
* @Description: TODO
* @Version 1.0
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//从认证信息中获取登入用户信息
SecurityUser securityUser = (SecurityUser)authentication.getPrincipal();
SysUser sysUser = securityUser.getSysUser();
String userinfo = JSONObject.toJSONString(sysUser);
// 2.获取认证信息
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) securityUser.getAuthorities();
// 3. SimpleGrantedAuthority::getAuthority 调用get方法
List<String> list = authorities.stream().map(SimpleGrantedAuthority::getAuthority).collect(Collectors.toList());
// 生成Jwt
String jwt = jwtUtils.createJwt(userinfo, list);
if (jwtUtils.verifyToken(jwt)){
response.setHeader("Authorization","bearer "+jwt);
HttpResult httpResult = HttpResult.builder()
.code(1)
.msg("jwt生成成功")
.data(jwt)
.build();
printToken(request,response,JSON.toJSONString(httpResult));
return;
}
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt生成失败")
.data(null)
.build();
printToken(request,response,JSON.toJSONString(httpResult));
}
private void printToken(HttpServletRequest request, HttpServletResponse response,String result) throws IOException {
// 1.设置编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(result);
writer.flush();
writer.close();
}
}
(8).WebSecurityConfig 配置类
package com.jsxs.springsecurity12databaseauthentication.config;
import com.jsxs.springsecurity12databaseauthentication.filter.JwtCheckFilter;
import com.jsxs.springsecurity12databaseauthentication.filter.ValidateCodeFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
/**
* @Author Jsxs
* @Date 2024/3/8 16:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.config
* @ClassName: WebSecurityConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
ValidateCodeFilter validateCodeFilter;
@Resource
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
JwtCheckFilter jwtCheckFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 放到用户认证之前 (如果想要获取token,先注释掉这句,因为登入成功后才会有token)
http.addFilterBefore(jwtCheckFilter,UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests().anyRequest().authenticated();
// 登入成功添加认证成功处理器类
http.formLogin().successHandler(myAuthenticationSuccessHandler).permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(9).测试
我们在postman添加一个响应头: 命名为->“Authentication”,值为: "bearer "+token。
2.添加Redis处理token过期问题
(1).Token弊端
Token一旦生成除非到了过期时间他才会失效,否则一旦生成就撤回不了。用户注销后照样可以进行token登入,这就是最大的弊端!!!
(2).步骤
- 登入成功后jwt存放到redis中。
- 用户退出时,从redis中删除token。
- 用户每次访问时,先校验jwt是否合法,如果合法再从 redis 里面取出
logintoken:jwt
判断这个jwt还存不存在,如果不存在就说是用户已经退出来,就返回未登陆。
(3).添加依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(3).启动redis
(4).在用户登入成功处理器上进行添加redis
package com.jsxs.springsecurity12databaseauthentication.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import com.jsxs.springsecurity12databaseauthentication.util.JwtUtils;
import com.jsxs.springsecurity12databaseauthentication.vo.HttpResult;
import com.jsxs.springsecurity12databaseauthentication.vo.SecurityUser;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @Author Jsxs
* @Date 2024/3/10 21:06
* @PackageName:com.jsxs.springsecurity12databaseauthentication.config
* @ClassName: MyAuthenticationSuccessHandler
* @Description: TODO 登入成功处理器
* @Version 1.0
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
JwtUtils jwtUtils;
@Resource
StringRedisTemplate stringRedisTemplate;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//从认证信息中获取登入用户信息
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
SysUser sysUser = securityUser.getSysUser();
String userinfo = JSONObject.toJSONString(sysUser);
// 2.获取认证信息
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) securityUser.getAuthorities();
// 3. SimpleGrantedAuthority::getAuthority 调用get方法
List<String> list = authorities.stream().map(SimpleGrantedAuthority::getAuthority).collect(Collectors.toList());
// 生成Jwt
String jwt = jwtUtils.createJwt(userinfo, list);
if (jwtUtils.verifyToken(jwt)) {
response.setHeader("Authorization", "bearer " + jwt);
// ⭐ 以logintoken+jwt为key将authentication值放入到我们的redis缓存中,并设置我们的过期时间
stringRedisTemplate.opsForValue().set("logintoken:"+jwt,JSONObject.toJSONString(authentication),2, TimeUnit.HOURS);
HttpResult httpResult = HttpResult.builder()
.code(1)
.msg("jwt生成成功")
.data(jwt)
.build();
printToken(request, response, JSON.toJSONString(httpResult));
return;
}
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt生成失败")
.data(null)
.build();
printToken(request, response, JSON.toJSONString(httpResult));
}
private void printToken(HttpServletRequest request, HttpServletResponse response, String result) throws IOException {
// 1.设置编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(result);
writer.flush();
writer.close();
}
}
(5).在jwt过滤器上进行校验jwt和redis
package com.jsxs.springsecurity12databaseauthentication.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jsxs.springsecurity12databaseauthentication.pojo.SysUser;
import com.jsxs.springsecurity12databaseauthentication.util.JwtUtils;
import com.jsxs.springsecurity12databaseauthentication.vo.HttpResult;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author Jsxs
* @Date 2024/3/11 16:05
* @PackageName:com.jsxs.springsecurity12databaseauthentication.filter
* @ClassName: JwtCheckFilter
* @Description: TODO
* @Version 1.0
*/
@Component
@Log4j2
public class JwtCheckFilter extends OncePerRequestFilter {
@Resource
JwtUtils jwtUtils;
@Resource
StringRedisTemplate stringRedisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 如果是登入的请求直接放行
if (request.getRequestURI().equals("/login") || request.getRequestURI().equals("/code/image")) {
doFilter(request, response, filterChain);
return;
}
// 如果jwt没有值
String jwt = request.getHeader("Authorization");
if (!StringUtils.hasText(jwt)) {
log.error("服务端不存在消息头是Authorization的token");
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt生成失败")
.data(null)
.build();
printToken(request, response, JSON.toJSONString(httpResult));
return;
}
// 1.因为在token的消息头中我们就要对其进行添加 "bearer "所以我们验证的时候要先删除
jwt = jwt.replace("bearer ", "");
// ⭐如果redis中没有jwt的信息直接打印删除了 ⭐
if (!StringUtils.hasText(stringRedisTemplate.opsForValue().get("logintoken:"+jwt))) {
log.info("redis的信息已经被删除或不存在:logintoken:{}",jwt);
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt非法、redis已经删除jwt,请你重新登入")
.data(null)
.build();
printToken(request, response, JSON.toJSONString(httpResult));
return;
}
if (!jwtUtils.verifyToken(jwt)) {
log.error("获取失败,从请求头中获取的token是:{}", jwt);
HttpResult httpResult = HttpResult.builder()
.code(0)
.msg("jwt生成失败")
.data(null)
.build();
printToken(request, response, JSON.toJSONString(httpResult));
return;
}
String userInfo = jwtUtils.getUserInfoFromToken(jwt);
List<String> userAuthFromToken = jwtUtils.getUserAuthFromToken(jwt);
// 将用户信息再转换成sysUser
SysUser sysUser = JSONObject.parseObject(userInfo,SysUser.class);
// 将权限再次转换为SimpleGrantedAuthority
List<SimpleGrantedAuthority> grantedAuthorityList = userAuthFromToken.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
// SpringSecurity接管 -> 用户名密码认证token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(sysUser,null,grantedAuthorityList);
// 把usernamePasswordAuthenticationToken放入SpringSecurity安全上下文中
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
doFilter(request, response, filterChain);
}
private void printToken(HttpServletRequest request, HttpServletResponse response, String result) throws IOException {
// 1.设置编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(result);
writer.flush();
writer.close();
}
}
(6).在退出成功处理器上删除redis
package com.jsxs.springsecurity12databaseauthentication.config;
import com.alibaba.fastjson2.JSON;
import com.jsxs.springsecurity12databaseauthentication.util.JwtUtils;
import com.jsxs.springsecurity12databaseauthentication.vo.HttpResult;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Author Jsxs
* @Date 2024/3/12 11:30
* @PackageName:com.jsxs.springsecurity12databaseauthentication.config
* @ClassName: MyLogoutSuccessHandler
* @Description: TODO
* @Version 1.0
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Resource
StringRedisTemplate stringRedisTemplate;
@Resource
JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 1.获取请求头
String auth = request.getHeader("Authorization");
// 2.假如没有jwt或者已经过期
if (!StringUtils.hasText(auth)) {
HttpResult result = HttpResult.builder()
.code(0)
.msg("执行退出时,jwt不存在或者已经过期")
.build();
printToken(request,response, JSON.toJSONString(response));
}
String jwtToken = auth.replace("bearer ", "");
boolean flag = jwtUtils.verifyToken(jwtToken);
// 从redis中删除我们的jwtToken
Boolean isDelete = stringRedisTemplate.delete("logintoken:" + jwtToken);
}
private void printToken(HttpServletRequest request, HttpServletResponse response, String result) throws IOException {
// 1.设置编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(result);
writer.flush();
writer.close();
}
}
(7).配置核心文件
# 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security_study?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=121788
#thymeleaf
#开发的时候我们不使用缓存,我们只需要点击小锤子就能同步部署
spring.thymeleaf.cache=false
# 检查模板
spring.thymeleaf.check-template=true
# mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# mapper.xml的映射文件
mybatis-plus.mapper-locations=classpath:com/jsxs/springsecurity12databaseauthentication/mapper/xml/*.xml
# 别名映射
mybatis-plus.type-aliases-package=com.jsxs.pojo
# 下划线自动改成驼峰
mybatis-plus.configuration.map-underscore-to-camel-case=true
#密钥
my.secret="secret888"
# 配置redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=121788
spring.redis.database=0
(8).SpringSecurity配置类
package com.jsxs.springsecurity12databaseauthentication.config;
import com.jsxs.springsecurity12databaseauthentication.filter.JwtCheckFilter;
import com.jsxs.springsecurity12databaseauthentication.filter.ValidateCodeFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
/**
* @Author Jsxs
* @Date 2024/3/8 16:52
* @PackageName:com.jsxs.springsecurity12databaseauthentication.config
* @ClassName: WebSecurityConfig
* @Description: TODO
* @Version 1.0
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
ValidateCodeFilter validateCodeFilter;
@Resource
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
MyLogoutSuccessHandler myLogoutSuccessHandler;
@Resource
JwtCheckFilter jwtCheckFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); //关闭我们的跨域保护,关闭意味着会被黑客攻击。 一定要开启使用redis
// 放到用户认证之前
http.addFilterBefore(jwtCheckFilter,UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests().anyRequest().authenticated();
http.formLogin().successHandler(myAuthenticationSuccessHandler).permitAll();
http.logout().logoutSuccessHandler(myLogoutSuccessHandler).permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(5).测试
- 登入
http://localhost:8080/login
输入密码后会有jwttoken生成
- 未退出前访问
http://localhost:8080/student/query
- 退出
我们携带着刚才的token添加消息头进行删除
redis也会删除
- 推出后访问
退出后,我们的jwt还生效,但是redis没有了 (携带token操作)
http://localhost:8080/student/query