145.【JWT+SpringSecurity】

JWT+Seacher

  • 构造者模式
  • 可延长字符串 (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. 登入验证

1.启动项目我们发现:: 我们只添加了一个依赖,然后访问任何一个页面就会出现如下的登入信息,这个页面不是我们自己写的,是SpringSecurity生成的,我们只有验证了账户和密码我们才能成功访问到。 默认账户名为 user,密码在控制台

http://localhost:8080/login

在这里插入图片描述

  1. 密码账户验证

假如密码验证成功,我们会访问到正式的页面

在这里插入图片描述

我们也发现我们认证成功后并不是说我们只能访问这一个页面,而是全部的页面我们都能够进行访问。

(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
  1. 未在配置类中对密码进行加密操作

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

}

在这里插入图片描述

  1. 在配置中对密码进行加密
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).搭建环境
  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;
    }
}

  1. 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).搭建环境
  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;

  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>

  1. 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
  1. 创建分层

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

  1. 添加接口
  • 当@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);
    }
}

  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 控制层
  1. 出现问题
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);

    }
}

当我们使用注入的方式进行操作输出流的时候,因为注入
默认采用的是单实列,然后输出流又是只能有一个在运行
所以当我们二次调用刷新图片的时候会出现输出流已经被
使用的运行时异常信息。

在这里插入图片描述
在这里插入图片描述

  1. 解决问题 (未完全解决)

保留注入的方式,但是需要在方法的最后关闭输出流

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(); // ⭐关闭
    }
}

在这里插入图片描述

  1. 解决问题 (完全解决)
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加密实操
  1. 进行加密视频文件

对蛋仔游戏.mp4进行加密转换成1.txt

base64 蛋仔游戏.mp4>1.txt

在这里插入图片描述

  1. 进行解密视频文件
 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 的公钥/私钥对进行签名。

在这里插入图片描述

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).步骤
  1. 登入成功后jwt存放到redis中。
  2. 用户退出时,从redis中删除token。
  3. 用户每次访问时,先校验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).测试
  1. 登入

http://localhost:8080/login
在这里插入图片描述

输入密码后会有jwttoken生成
在这里插入图片描述
在这里插入图片描述

  1. 未退出前访问

http://localhost:8080/student/query
在这里插入图片描述

  1. 退出

我们携带着刚才的token添加消息头进行删除

在这里插入图片描述
redis也会删除

在这里插入图片描述

  1. 推出后访问

退出后,我们的jwt还生效,但是redis没有了 (携带token操作)

http://localhost:8080/student/query
在这里插入图片描述