快速学习springsecurity最新版 (版本6.2)---用户认证

简介

Spring Security 是 Spring 家族中的一个安全管理框架。目前比较主流的是另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富,但是shiro并不简便,这里轻量级安全框架更推荐国产安全框架satokensatoken官网
​ 一般大型的项目都是使用SpringSecurity 来做安全框架。这些安全框架主要的内容包含以下功能模块

​ 一般Web应用的需要进行认证授权

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
比如:购买东西前需要登录 ,预约前需要登录认证个人信息

授权:经过认证后判断当前用户是否有权限进行某个操作
比如;一般某些管理后台系统对应用户的角色(管理员,审核,测试)等不同角色提供不同服务,某些接口功能只对某些特定角色开启

本章主要讲认证模块 看完本章是实现前后端分离场景–>看这个Spring
security用户授权

​ 而认证和授权也是SpringSecurity作为安全框架的核心功能。(官方中文文档截图)springsecurity官方中文文档
在这里插入图片描述
当然,这些功能也可以自己通过拦截器和jwt等实现认证,通过访问接口前多表联查实现授权等自定义实现,比如自己实现前后端分离安全认证

身份认证模块

官方代码示例:GitHub - spring-projects/spring-security-samples
目前主流的还是mvc架构 以及采用servlet的web项目
在这里插入图片描述
该目录地址就是一个推荐的springsecurity官方案列
在这里插入图片描述
项目名:security-demo

JDK:17

SpringBoot:3.2.0(依赖了Spring Security 6.2.0)

Dependencies:Spring Web、Spring Security、Thymeleaf

入门案列–静态密码登录 内存用户器

创建IndexController
package com.atguigu.securitydemo.controller;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index() {
        return "index";
    }
}
创建index.html

在路径resources/templates中创建index.html,这个th:href=“@{/logout}” 是thymeleaf的模板语法和vue和相似

<html xmlns:th="https://www.thymeleaf.org">
<head>
  <title>Hello Security!</title>
</head>
<body>
<h1>Hello Security</h1>
<!--通过使用@{/logout},Thymeleaf将自动处理生成正确的URL,以适应当前的上下文路径。
这样,无论应用程序部署在哪个上下文路径下,生成的URL都能正确地指向注销功能。-->
<a th:href="@{/logout}">Log Out</a>
</body>
</html>

启动项目测试Controller

浏览器中访问:http://localhost:8080/

**浏览器自动跳转到登录页面:**http://localhost:8080/login

这个时候如果没有认证访问controller的路由就会跳转默认生成的登录页面
在这里插入图片描述

输入用户名:user

输入密码:在控制台的启动日志中查找初始的默认密码

点击"Sign in"进行登录,浏览器就跳转到了index页面

在这里插入图片描述

@{/logout}的作用

就是类似vue的动态语法 这里了解即可
通过使用@{/logout},Thymeleaf将自动处理生成正确的URL,以适应当前的上下文路径。这样,无论应用程序部署在哪个上下文路径下,生成的URL都能正确地指向注销功能。

例如:如果我们在配置文件中添加如下内容

server.servlet.context-path=/demo

那么@{/logout}可以自动处理url为正确的相对路径(此时为demo 这里是演示路径无需添加)

但是如果是普通的/logout,路径就会不正确

页面样式无法加载的问题

页面样式bootstrap.min.css是一个CDN地址,由于在国外需要通过科学上网的方式访问
当然由于我们写项目都是客户端和服务端分离的方式,这里无需在意这个问题

Spring Security默认做了什么

我们并没有写一个登录接口,但是框架就已经

  • 保护应用程序URL,要求对应用程序的任何交互进行身份验证。
  • 程序启动时生成一个默认用户“user”。
  • 生成一个默认的随机密码,并将此密码记录在控制台上。
  • 生成默认的登录表单和注销页面。
  • 提供基于表单的登录和注销流程。
  • 对于Web请求,重定向到登录页面;
  • 对于服务请求,返回401未经授权。
  • 处理跨站请求伪造(CSRF)攻击。
  • 处理会话劫持攻击。
  • 写入Strict-Transport-Security以确保HTTPS。
  • 写入X-Content-Type-Options以处理嗅探攻击。
  • 写入Cache Control头来保护经过身份验证的资源。
  • 写入X-Frame-Options以处理点击劫持攻击。

Spring Security 的底层架构

官方文档中写得清楚
在这里插入图片描述
客户端向应用程序发送一个请求,容器创建一个 FilterChain,其中包含 Filter 实例和 Servlet,应该根据请求URI的路径来处理 HttpServletRequest。在Spring MVC应用程序中,Servlet是 DispatcherServlet 的一个实例。一个 Servlet 最多可以处理一个 HttpServletRequest 和 HttpServletResponse。然而,可以使用多个 Filter 来完成如下工作。

防止下游的 Filter 实例或 Servlet 被调用。在这种情况下,Filter 通常会使用 HttpServletResponse 对客户端写入响应。

修改下游的 Filter 实例和 Servlet 所使用的 HttpServletRequest 或 HttpServletResponse。

过滤器的力量来自于传入它的 FilterChain。

DelegatingFilterProxy

DelegatingFilterProxy 是 Spring Security 提供的一个 Filter 实现,可以在 Servlet 容器和 Spring 容器之间建立桥梁。通过使用 DelegatingFilterProxy,这样就可以将Servlet容器中的 Filter 实例放在 Spring 容器中管理。

在这里插入图片描述

FilterChainProxy

复杂的业务中不可能只有一个过滤器。因此FilterChainProxy是Spring Security提供的一个特殊的Filter,它允许通过SecurityFilterChain将过滤器的工作委托给多个Bean Filter实例。

在这里插入图片描述

SecurityFilterChain

SecurityFilterChain 被 FilterChainProxy 使用,负责查找当前的请求需要执行的Security Filter列表。

在这里插入图片描述

Multiple SecurityFilterChain

多个过滤器的使用可以完成各个样子的业务

可以有多个SecurityFilterChain的配置,FilterChainProxy决定使用哪个SecurityFilterChain。如果请求的URL是/api/messages/,它首先匹配SecurityFilterChain0的模式/api/**,因此只调用SecurityFilterChain 0。假设没有其他SecurityFilterChain实例匹配,那么将调用SecurityFilterChain n。

在这里插入图片描述

所以看下来可以发现springsecurity中包含了很多过滤器,组成过滤器链完成认证操作

SecurityFilterChain接口的实现,加载了默认的16个Filter
在这里插入图片描述
在这里插入图片描述

SecurityProperties 配置静态用户名,密码

了解完securtiy的底层架构,在解决一个问题,那么就是登录时候的账户名和密码如何出现的,在依赖中搜索该类SecurityProperties,发现会出现一个内部类user,账户名为user,密码则是uuid,很明显这个就是启动项目时候默认生成的密码,既然这个类是属性类
在这里插入图片描述
,那么我们也可以进行指定,在security的配置文件文档中
在这里插入图片描述

编写配置
import org.springframework.beans.factory.annotation.Autowired;

import org.sprinramework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;

@Configuration
@EnableWebSecurity//SPRINGBOOT项目忽略
public class WebSecurityConfig {

	@Bean
	public UserDetailsService userDetailsService() {
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//			在内存中创建用户作用登录时候作为对比的数据源
manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
		return manager;
	}
}

发现可以通过注册一个UserDetailsService的bean进行实现
其中返回的是内存管理对象实列,管理用户users
在这里插入图片描述
UserDetailsService 包含了根据用户名加载对象方法,该方法就是springsecurity进行用户比对的用户源,(这里是进行静态写在内存,那么后期可以自己实现这个接口来比对用户变成数据库)
在这里插入图片描述
其中的maner则是提供了对系统用户crud的方法
在这里插入图片描述

回到官网提供的demo配置案列,那么就可以将用户静态写在内存中

//见名知意 这里创建在内存的时候,还对这个用户密码进行了加密(withDefaultPasswordEncoder)
manager.createUser(User.withDefaultPasswordEncoder()
.username("user").password("password").roles("USER").build());

此时重启配置,就可以根据user ,password进行登录

既然是静态配置,那么就可以写在配置文件

spring.security.user.name=user
spring.security.user.password=123

Spring Security自定义配置

需要对于接口UserDetailsService有个初步映像,理解为管理认证需要的数据源管理器
Springscurity主要的是配置过滤器链

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 忽略授权的地址列表
        List<String> permitList = permitResource.getPermitList();
        String[] permits = permitList.toArray(new String[0]);

        http
                .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(permits).permitAll()
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
                        .anyRequest().authenticated()
                )
                .exceptionHandling(exception -> exception.authenticationEntryPoint(new SecurityAuthenticationEntryPoint()))
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                .csrf(AbstractHttpConfigurer::disable)
        ;

        return http.build();
    }

官方文档

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
		.authorizeRequests(authorize -> authorize
			.anyRequest().authenticated()
		)
		.formLogin(withDefaults())
		.httpBasic(withDefaults());
	return http.build();
}`
基于内存配置

实际开发的过程中,我们需要应用程序更加灵活,可以在SpringSecurity中创建自定义配置文件

官方文档:Java自定义配置

刚才实现了官方的实列配置,可以发现大致是相同,那么我们可以通过了解基于内存的管理器来更一步了解security

从刚才的实列中可知.UserDetailsService用来管理用户信息,InMemoryUserDetailsManager是UserDetailsService的一个实现,用来管理基于内存的用户信息。其中createuser就是在内存中创建系统用户,用于登录时候进行比对
实现就是在其封装在内存中map,
在这里插入图片描述
manner的其他方法则是对用户进行crud 不做过多阐述
在这里插入图片描述那么顶级接口userdetailsService呢

loadUserByUsername 这个方法主要用于从后端系统(比如数据库,官方演示配置是内存)加载用户的详细信息。当用户尝试登录时,他们会提供自己的用户名(或其他标识)和密码。Spring Security 需要使用这个用户名来获取用户的详细信息,包括他们的密码、权限等。这就是在 UserDetailsService 接口中定义的 loadUserByUsername 方法的目的。也就是说在进行比对认证时候需要调用这个方法

loadUserByUsername 就是在尝试进行用户认证的过程中,从后端系统加载用户详细信息的关键步骤。这个步骤通常发生在 Spring Security 处理登录请求的过程中的 DaoAuthenticationProvider 中。

这里查看内存用户管理器的实现 通过用户名返回用户
在这里插入图片描述

UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法中将用户输入的用户名密码和从内存中获取到的用户信息进行比较,进行用户认证
认证的大概逻辑
在这里插入图片描述

而这个用户我没并没有定义,这个用户是官方定义的用户类型
在这里插入图片描述
在这里插入图片描述

通过源码可以看到官方定义的用户 包含了用户基本信息外,还有权限列表账户是否过期等消息,所以在创建用户的时候大致可以参考一下其中user,而userdetials则是定影了用户的一些细节信息

实现security登录接口基于数据库

所以接下来通过改造重写userdetialsuservice实现数据库认证

准备工作

创建数据并且创建三个用户

-- 创建数据库
CREATE DATABASE `security-demo`;
USE `security-demo`;

-- 创建用户表
CREATE TABLE `user`(
	`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
	`username` VARCHAR(50) DEFAULT NULL ,
	`password` VARCHAR(500) DEFAULT NULL,
	`enabled` BOOLEAN NOT NULL
);
-- 唯一索引
CREATE UNIQUE INDEX `user_username_uindex` ON `user`(`username`); 

-- 插入用户数据(密码是 "abc" )
INSERT INTO `user` (`username`, `password`, `enabled`) VALUES
('admin', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Helen', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Tom', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE);

字段也只有三个,是很基本的用户表,其中数据密码是采用springsecurity的默认加密方式
在这里插入图片描述
引入依赖

<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.30</version>
		</dependency>

		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
			<version>3.5.5</version>
			<exclusions>
				<exclusion>
					<groupId>org.mybatis</groupId>
					<artifactId>mybatis-spring</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<!--swagger测试-->
		<dependency>
			<groupId>com.github.xiaoymin</groupId>
			<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
			<version>4.1.0</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-spring</artifactId>
			<version>3.0.3</version>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

配置数据源

#MySQL数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security-demo
spring.datasource.username=root
spring.datasource.password=123456
#SQL日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

安装myvatis-x 一键生成各个层
在这里插入图片描述

写一个controller验证orm框架是否操作数据库可行


@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    public UserService userService;

    @GetMapping("/list")
    public List<User> getList(){
        return userService.list();
    }
}

目录
在这里插入图片描述

将对比用户数据源定义为自己的数据库用户管理器

创建认证管理器有内存和jdbc俩个,但是jdbc的认证是基于springtemplate 所以需要自己更改
在这里插入图片描述
通过上面的认识,认证过程中主要是通过loadbyusername 取出用户进行和前端输入的用户比对,所以要做的就是模仿InMemoryUserDetailsManager类
在这里插入图片描述

基于数据库的用户认证实现

认证流程

  • 程序启动时:
    模仿InMemoryUserDetailsManager
    • 创建DBUserDetailsManager类,实现接口 UserDetailsManager, UserDetailsPasswordService
    • 在应用程序中初始化这个类的对象
  • 校验用户时:
    • SpringSecurity自动使用DBUserDetailsManagerloadUserByUsername方法从数据库中获取User对象
    • UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法中将用户输入的用户名密码和从数据库中获取到的用户信息进行比较,进行用户认证
@Component//注入ioc 或者在配置文件中用@Bean的方式注入
@Slf4j
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {


//    这样就可以按照security的规范来使用用户的管理
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }
//原来的内存管理器是在用户在添加到内存的map,实现这个方法这里插入数据库
    @Override
    public void createUser(UserDetails userDetails) {
//        在sql中插入信息
        User user = new User();
        user.setUsername(userDetails.getUsername());
        user.setPassword(userDetails.getPassword());
        user.setEnabled(1);
        userMapper.insert(user);
    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }
    @Resource
    private UserMapper userMapper;

    //security底层会根据这个方法来对比用户
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery().eq(User::getUsername, username);
//        这里用户账户是唯一的
    User user = userMapper.selectOne(wrapper);
       if (user == null){
           throw new UsernameNotFoundException("系统用户不存在");
       }else{
//           1表示可用
           boolean isenabled = user.getEnabled() == 1;
/**
 * ,任何非零的整数值都会被视为 true,而 0 会被视为 false。
 */
//模拟系统权限列表
           Collection<GrantedAuthority> authorities = new ArrayList<>();
           return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),isenabled ,
                   true,
                   true,
                  true,
//               权限列表
               authorities);
       }
    }
}

重启的时候把配置类中基于内存的用户管理器注入bean的代码注释,避免有俩个userdetailsService的实现,认证时调用loaduserBYusername冲突,重启即可,此时登录认证,输入admin,password 即可登录,则说明是从数据库中获取用户进行比对且成功
此时已经是实现基于数据库实现,但是还是不够灵活,为此我们需要了解默认配置(引入security即实现的配置)
在这里插入图片描述

//经过过滤器的请求
   http
//                lambda表达式对其中请求进行遍历
                .authorizeRequests(authorize -> authorize
                        .anyRequest()
                        .authenticated()//已认证的请求自动授权
                )
//                如果没有登录认证的请求默认使用表单登录api 跳转表单进行登录
             .formLogin(withDefaults())//自动生成表单 
                .httpBasic(withDefaults());//然后给在使用基本授权方式(游览器默认表单)

httpBasic(withDefaults())采用游览器默认认证方式,在过滤器链中注释.formLogin(withDefaults()) 那么重启
在这里插入图片描述
默认配置的另一个配置表单登录,如果我们觉得,这个登录页面丑呢,那么需要修改这个配置来实现自定义登录界面(这里都是前后i端一体的,分离在后面)

自定义security的登录界面

在templates新建login页面


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>登录页面</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
    html {
      height: 100%;
    }
    body {
      height: 100%;
    }
    .container {
      height: 100%;
      background-image: linear-gradient(to right, #fbc2eb, #a6c1ee);
    }
    .login-wrapper {
      background-color: #fff;
      width: 358px;
      height: 588px;
      border-radius: 15px;
      padding: 0 50px;
      position: relative;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
    }
    .header {
      font-size: 38px;
      font-weight: bold;
      text-align: center;
      line-height: 200px;
    }
    .input-item {
      display: block;
      width: 100%;
      margin-bottom: 20px;
      border: 0;
      padding: 10px;
      border-bottom: 1px solid rgb(128, 125, 125);
      font-size: 15px;
      outline: none;
    }
    .input-item:placeholder {
      text-transform: uppercase;
    }
    .btn {
      text-align: center;
      padding: 10px;
      width: 100%;
      margin-top: 40px;
      background-image: linear-gradient(to right, #a6c1ee, #fbc2eb);
      color: #fff;
    }
    .msg {
      text-align: center;
      line-height: 88px;
    }
    a {
      text-decoration-line: none;
      color: #abc1ee;
    }
  </style>
</head>
<body>
<div class="container">
  <div class="login-wrapper">
    <div class="header">Login</div>
    <!--method必须为"post"-->
    <!--th:action="@{/login}" ,
    使用动态参数,表单中会自动生成_csrf隐藏字段,用于防止csrf攻击
    login: 和登录页面保持一致即可,SpringSecurity自动进行登录认证-->
    <form th:action="@{/login}" method="post" class="form-wrapper">
      <div>
        <!--name必须为"username"-->
        <input type="text" name="username" placeholder="用户名" class="input-item"/>
      </div>
      <div>
        <!--name必须为"password"-->
        <input type="password" name="password" placeholder="密码" class="input-item"/>
      </div>
      <input type="submit" value="登录" class="btn" />
    </form>

    <div th:if="${param.error}">
      错误的用户名和密码.</div>


    <div class="msg">
      Don't have account?
      <a href="#">Sign up</a>
    </div>
  </div>
</div>
</body>
</html>

新添加一个controller负责跳转路由

@Controller
public class LoginController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

修改配置文件

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

//        返回的是安全过滤器链所以是依次执行的
//关闭csrf攻击防御

        http.csrf(AbstractHttpConfigurer::disable).formLogin( form -> {
            form
                    .loginPage("/login").permitAll() //登录页面无需授权即可访问
                    .usernameParameter("username") //自定义表单用户名参数,默认是username
                    .passwordParameter("password") //自定义表单密码参数,默认是password
                    .failureUrl("/login?error") //登录失败的返回地址
            ;
        }); //使用表单授权方式;
        http
//                lambda表达式对其中请求进行遍历
                .authorizeRequests(authorize -> authorize
                        .anyRequest()
                        .authenticated()//已认证的请求自动授权
                )
//                如果没有登录认证的请求默认使用表单登录api 跳转表单进行登录
//                .formLogin(withDefaults())//自动生成表单 不使用自动生成的表单
                .httpBasic(withDefaults());//然后给在使用基本授权方式(游览器默认表单)
        return http.build();
    }

一定记得关闭默认表达认证避免重涂,重启项目
在这里插入图片描述

那么实现完成了自定义登录了,接下来探讨的是密码安全部分

密码安全

security在密码安全部分做了很好的加密算法
先回忆内存用户管理类中添加用户的代码,其中对用户进行了加密
在这里插入图片描述
为此我们根据官方实列来进行仿造数据中添加符合security定义的用户
usercontroller中写一个添加用户接口

@PostMapping("/add")
public void add(@RequestBody User user){    userService.saveUserDetails(user);}

UserService接口中添加方法

void saveUserDetails(User user);

UserServiceImpl实现中添加方法

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
    implements UserService{
    @Autowired
DBUserDetailsManager userDetailsManager;
    @Override
    public void adduser(User user) {
        log.info("最开始接收到的密码"+user.getPassword());
//            security的user
        UserDetails details = org.springframework.security.core.userdetails.User
                .withDefaultPasswordEncoder().username(user.getUsername()).password(user.getPassword())
                .roles("USER")//当前数据还没有角色一说
                .build();
        log.info("构造为userdetials的密码"+details.getPassword());
userDetailsManager.createUser(details);
    }
}

DBUserDetailsManager中之前就添加的插入数据库方法

@Override
public void createUser(UserDetails userDetails) {

    User user = new User();
    user.setUsername(userDetails.getUsername());
    user.setPassword(userDetails.getPassword());
    user.setEnabled(true);
    userMapper.insert(user);
}

使用Swagger测试

pom中添加配置用于测试

<!--swagger测试-->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.1.0</version>
</dependency>

**Swagger测试地址:**http://localhost:8080/demo/doc.html

在测试接口前需要关闭csrf保护

默认情况下SpringSecurity开启了csrf攻击防御的功能,这要求请求参数中必须有一个隐藏的**_csrf**字段,如下:

在这里插入图片描述

在filterChain方法中添加如下代码,关闭csrf攻击防御

//关闭csrf攻击防御
http.csrf((csrf) -> {
    csrf.disable();
});

访问swagger接口测试
http://localhost:8080/doc.html
在这里插入图片描述

输出
在这里插入图片描述
此时就可以清楚的看到密码是进行加密了的
密码加密算法

参考文档:Password Storage :: Spring Security

密码加密方式

明文密码:

最初,密码以明文形式存储在数据库中。但是恶意用户可能会通过SQL注入等手段获取到明文密码,或者程序员将数据库数据泄露的情况也可能发生。

Hash算法:

Spring Security的PasswordEncoder接口用于对密码进行单向转换,从而将密码安全地存储。对密码单向转换需要用到哈希算法,例如MD5、SHA-256、SHA-512等,哈希算法是单向的,只能加密,不能解密

因此,数据库中存储的是单向转换后的密码,Spring Security在进行用户身份验证时需要将用户输入的密码进行单向转换,然后与数据库的密码进行比较。

因此,如果发生数据泄露,只有密码的单向哈希会被暴露。由于哈希是单向的,并且在给定哈希的情况下只能通过暴力破解的方式猜测密码

彩虹表:

恶意用户创建称为彩虹表的查找表。

彩虹表就是一个庞大的、针对各种可能的字母组合预先生成的哈希值集合,有了它可以快速破解各类密码。越是复杂的密码,需要的彩虹表就越大,主流的彩虹表都是100G以上,目前主要的算法有LM, NTLM, MD5, SHA1, MYSQLSHA1, HALFLMCHALL, NTLMCHALL, ORACLE-SYSTEM, MD5-HALF。

加盐密码:

为了减轻彩虹表的效果,开发人员开始使用加盐密码。不再只使用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为盐)。盐和用户的密码将一起经过哈希函数运算,生成一个唯一的哈希。盐将以明文形式与用户的密码一起存储。然后,当用户尝试进行身份验证时,盐和用户输入的密码一起经过哈希函数运算,再与存储的密码进行比较。唯一的盐意味着彩虹表不再有效,因为对于每个盐和密码的组合,哈希都是不同的。

自适应单向函数:

随着硬件的不断发展,加盐哈希也不再安全。原因是,计算机可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地破解每个密码。

现在,开发人员开始使用自适应单向函数来存储密码。使用自适应单向函数验证密码时,故意占用资源(故意使用大量的CPU、内存或其他资源)。自适应单向函数允许配置一个“工作因子”,随着硬件的改进而增加。我们建议将“工作因子”调整到系统中验证密码需要约一秒钟的时间。这种权衡是为了让攻击者难以破解密码

自适应单向函数包括bcrypt、PBKDF2、scrypt和argon2

PasswordEncoder

BCryptPasswordEncoder

使用广泛支持的bcrypt算法来对密码进行哈希。为了增加对密码破解的抵抗力,bcrypt故意设计得较慢。和其他自适应单向函数一样,应该调整其参数,使其在您的系统上验证一个密码大约需要1秒的时间。BCryptPasswordEncoder的默认实现使用强度10。建议您在自己的系统上调整和测试强度参数,以便验证密码时大约需要1秒的时间。

在这里插入图片描述

Argon2PasswordEncoder

使用Argon2算法对密码进行哈希处理。Argon2是密码哈希比赛的获胜者。为了防止在自定义硬件上进行密码破解,Argon2是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当前的Argon2PasswordEncoder实现需要使用BouncyCastle库。

Pbkdf2PasswordEncoder

使用PBKDF2算法对密码进行哈希处理。为了防止密码破解,PBKDF2是一种故意缓慢的算法。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当需要FIPS认证时,这种算法是一个很好的选择。

在这里插入图片描述

SCryptPasswordEncoder

使用scrypt算法对密码进行哈希处理。为了防止在自定义硬件上进行密码破解,scrypt是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。

密码加密测试

在测试类中编写一个测试方法

@Test
void testPassword() {

    // 工作因子,默认值是10,最小值是4,最大值是31,值越大运算速度越慢
    PasswordEncoder encoder = new BCryptPasswordEncoder(4);
    //明文:"password"
    //密文:result,即使明文密码相同,每次生成的密文也不一致
    String result = encoder.encode("password");
    System.out.println(result);

    //密码校验
    Assert.isTrue(encoder.matches("password", result), "密码不一致");
}
DelegatingPasswordEncoder
  • 表中存储的密码形式:{bcrypt}$2a 10 10 10GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW
  • 通过如下源码可以知道:可以通过{bcrypt}前缀动态获取和密码的形式类型一致的PasswordEncoder对象
  • 目的:方便随时做密码策略的升级,兼容数据库中的老版本密码策略生成的密码

在这里插入图片描述

根据密码前缀进行比对

在这里插入图片描述

前缀是为了判断根据哪个算法进行加密,对用户密码进行比对时候会判断前缀 不同用户不同加密方式

在这里插入图片描述

身份认证模块-前后端分离架构

在项目开发中前端和后端应该是分开的特别是服务器端应该专注于数据的返回,而页面跳转等前端工作由前端完成,所以这里需要对认证功能进行前后端分离开发的i情况下定制化
先来了解登录流程

登录流程

下面讨论的都是security的内置部分

  • 登录成功后调用:AuthenticationSuccessHandler
  • 登录失败后调用:AuthenticationFailureHandler

官网的认证架构

在这里插入图片描述
所以我们需要做的就是- 前端传递的用户密码生成认证token 然后提交给认证manager,所以我们的登录接口返回的不应该是跳转路由
参考我们正常的前后端分离开发过程,我们需要自己封装一个json

首先关闭下面的配置
//   只要关闭了这几个配置,就不会采用security的登录方案,就可以自己写登录逻辑
  http
            .csrf().disable()  // 如果使用前后端分离,可能需要禁用CSRF保护
            .authorizeRequests()
                .anyRequest().authenticated() // 要求所有访问都必须通过认证
                .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不创建会话 

自定义返回结果的话就需要重写 认证处理成功的处理器的抽象方法

同理认证失败的话就要重写认证失败处理器中的处理方法

认证返回结果为json

引入fastjson

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.37</version>
</dependency>

写一个通用返回结果类(这里随便写的实际开发不可能这么简单)

@Data
@AllArgsConstructor
public class Result {
    public static final int SUCCESS_CODE=0;//成功
    public static final int Nologin_CODE=401;//成功
    public static final int fail_CODE=500;//失败
    private int code;//错误码
private String msg;//返回信息
    private Object data;//返回数据


    public static Result success(Object data){
        return new Result(SUCCESS_CODE,"操作成功",data);
    }
    public static Result fail(String Errormsg) {
        return new Result(fail_CODE,Errormsg,null);
    }
    public static Result nologin(String Errormsg) {
        return new Result(Nologin_CODE,Errormsg,null);
    }
}


认证成功的响应

写一个响应成功类
@Slf4j
public class myAUthensuccessHandler implements AuthenticationSuccessHandler
{


@Resource
    UserMapper userMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
    //        当应用程序认证成功时候触发

        User user = new User();
        Object principal = authentication.getPrincipal();    //获取用户身份信息
        log.info("用户信息"+principal);
//        authentication.getCredentials();//登录凭证信息 账户密码登录 时里包含用户密码等信息
//        Collection<? extends GrantedAuthority> collection = authentication.getAuthorities();//包含的权限信息
        response.setContentType("application/json;charset=UTF-8");//响应头
            //认证成功
        String jsonString = JSON.toJSONString(Result.success(principal));
        response.getWriter().println(jsonString);//响应体内容输出

}
}

SecurityFilterChain配置

form.successHandler(new MyAuthenticationSuccessHandler()) //认证成功时的处理

输出的principal中不包含密码
在这里插入图片描述

返回结果

在这里插入图片描述

认证失败响应

失败结果处理
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException, IOException {

        //获取错误信息
        String localizedMessage = exception.getLocalizedMessage();



        //转换成json字符串
        String json = JSON.toJSONString(Result.fail("登录失败哦"));

        //返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

SecurityFilterChain配置

form.failureHandler(new MyAuthenticationFailureHandler()) //认证失败时的处理

在这里插入图片描述

注销响应
注销结果处理
package com.atguigu.securitydemo.config;

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", 0);
        result.put("message", "注销成功");

        //转换成json字符串
        String json = JSON.toJSONString(result);

        //返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

SecurityFilterChain配置

http.logout(logout -> {
    logout.logoutSuccessHandler(new MyLogoutSuccessHandler()); //注销成功时的处理
});

在这里插入图片描述

但是这样写处理器是有一点麻烦的 Springsecurity 有事件监听机制

可以通过事件监听器(Event Listener)来处理登录成功和失败的事件,而无需单独实现 AuthenticationSuccessHandler 和 AbstractAuthenticationFailureEvent。事件监听器可以更为灵活地处理认证成功和失败的情况,并且可以集中处理多种事件,包括但不限于认证成功和失败。

使用事件监听器处理认证事件
Spring Security提供了几个事件,其中包括 AuthenticationSuccessEvent 和 AbstractAuthenticationFailureEvent,它们可以用来监听认证成功和认证失败的事件。通过创建事件监听器,你可以在这些事件发生时执行自定义逻辑。 只需要注入ioc ,无需写配置文件

@Component
@AllArgsConstructor
public class AuthenticationEvents {
    private final SysLogLoginService sysLogLoginService;

    @EventListener
    public void onSuccess(AuthenticationSuccessEvent event) {
        // 用户信息
        UserDetail user = (UserDetail) event.getAuthentication().getPrincipal();

        // 保存登录日志
        sysLogLoginService.save(user.getUsername(), Constant.SUCCESS, LoginOperationEnum.LOGIN_SUCCESS.getValue());
    }

    @EventListener
    public void onFailure(AbstractAuthenticationFailureEvent event) {
        // 用户名
        String username = (String) event.getAuthentication().getPrincipal();

        // 保存登录日志
        sysLogLoginService.save(username, Constant.FAIL, LoginOperationEnum.ACCOUNT_FAIL.getValue());
    }

}

请求未认证的接口

实现AuthenticationEntryPoint接口

Servlet Authentication Architecture :: Spring Security

当访问一个需要认证之后才能访问的接口的时候,Spring Security会使用AuthenticationEntryPoint将用户请求跳转到登录页面,要求用户提供登录凭证。所以如果是前后端分类使用Springsecurity,一定要重写这个接口并用配置文件加入过滤器链

这里我们也希望系统返回json结果方便前端根据状态码进行跳转客户端的登录页面,因此我们定义类实现AuthenticationEntryPoint接口

package com.atguigu.securitydemo.config;

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        //获取错误信息
        //String localizedMessage = authException.getLocalizedMessage();

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", -1);
        result.put("message", "需要登录");

        //转换成json字符串
        String json = JSON.toJSONString(result);

        //返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

SecurityFilterChain配置

//错误处理
http.exceptionHandling(exception  -> {
    exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());//请求未认证的接口
});
用户认证信息封装
基本概念

官网解析
在这里插入图片描述

在Spring Security框架中,SecurityContextHolder、SecurityContext、Authentication、Principal和Credential是一些与身份验证和授权相关的重要概念。它们之间的关系如下:

  1. SecurityContextHolder:SecurityContextHolder 是 Spring Security 存储已认证用户详细信息的地方。
  2. SecurityContext:SecurityContext 是从 SecurityContextHolder 获取的内容,包含当前已认证用户的 Authentication 信息。
  3. Authentication:Authentication 表示用户的身份认证信息。它包含了用户的Principal、Credential和Authority信息。
  4. Principal:表示用户的身份标识。它通常是一个表示用户的实体对象,例如用户名。Principal可以通过Authentication对象的getPrincipal()方法获取。
  5. Credentials:表示用户的凭证信息,例如密码、证书或其他认证凭据。Credential可以通过Authentication对象的getCredentials()方法获取。
  6. GrantedAuthority:表示用户被授予的权限

总结起来,SecurityContextHolder用于管理当前线程的安全上下文,存储已认证用户的详细信息,其中包含了SecurityContext对象,该对象包含了Authentication对象,后者表示用户的身份验证信息,包括Principal(用户的身份标识)和Credential(用户的凭证信息)。

自己实现前后端登录安全校验,这篇文档中我使用的就是localtrhread 线程池作为上下文对象

在Controller中获取用户信息

之前实在官方的接口中获取配置信息,那么如果在controller的环境呢
其实就是类似上下文,在最开始的哪个
IndexController:

    @GetMapping("/")
    public Result index() {

        SecurityContext context = SecurityContextHolder.getContext();//存储认证对象的上下文
        Authentication authentication = context.getAuthentication();//认证对象
        String username = authentication.getName();//用户名
        Object principal =authentication.getPrincipal();//身份
        Object credentials = authentication.getCredentials();//凭证(脱敏)
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();//权限

        System.out.println(username);
        System.out.println(principal);
        System.out.println(credentials);
        System.out.println(authorities);

        HashMap<String, Object> map = new HashMap<>();

        map.put("认证对象", authentication);
        map.put("身份信息", principal);
        map.put("creden", credentials);
        return Result.success(map);
    }

并且

如果把cookie删除,找不到对应的session,那么就会登录失效 说明security默认是采用会话登录,如果在分布式的环境中,session无法共享是不能完成我们需要的需求的,后续我们需要对该功能模块进行更细一步的定制化

在这里插入图片描述

会话并发处理

后登录的账号会使先登录的账号cookie失效

实现处理器接口

实现接口SessionInformationExpiredStrategy

package com.atguigu.securitydemo.config;

public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    //当session失效
    
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", -1);
        result.put("message", "该账号已从其他设备登录");

        //转换成json字符串
        String json = JSON.toJSONString(result);

        HttpServletResponse response = event.getResponse();
        //返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

SecurityFilterChain配置

//会话管理
http.sessionManagement(session -> {
    session
        只允许客户端匹配session 的token数量
        .maximumSessions(1)
        .expiredSessionStrategy(new MySessionInformationExpiredStrategy());
});

此时前一个登录的客户端cookie就过期了在这里插入图片描述

跨域

跨域全称是跨域资源共享(Cross-Origin Resources Sharing,CORS),它是浏览器的保护机制,只允许网页请求统一域名下的服务,同一域名指=>协议、域名、端口号都要保持一致,如果有一项不同,那么就是跨域请求。在前后端分离的项目中,需要解决跨域的问题。

在SpringSecurity中解决跨域很简单,在配置文件中添加如下配置即可

//跨域
http.cors(withDefaults());

但是上诉提供实现的接口配置都是在原来security提供的login logout 等方法上,显然我们自己的登录接口需要更自由的定制

定制化jwt,redis等定制化需求

刚才的接口实现就可以发现,官方默认的login接口逻辑无法满足我们需求,所以我们需要进一步定制化目前github有个低代码平台(maku-boot)官网对security的实现也很优秀可以下载学习

首先梳理登录流程,完成定制化登录
  1. 接收登录请求:用户的登录请求通常是一个包含用户名和密码的 HTTP POST 请求。这个请求被 Spring Security 的 UsernamePasswordAuthenticationFilter 捕获。如果你不想使用 Spring Security 默认的登录端点(如 /login),你可以自定义登录接口。

生成 Authentication Token:UsernamePasswordAuthenticationFilter 会根据请求中的用户名和密码创建一个 UsernamePasswordAuthenticationToken(未认证状态 用于在过滤链中进行认证)。

  1. 认证过程:这个未认证的 UsernamePasswordAuthenticationToken 会被传递给 AuthenticationManager 进行认证。AuthenticationManager 会调用配置的 AuthenticationProvider,通常是 DaoAuthenticationProvider,来验证用户名和密码。

  2. 加载用户详情:AuthenticationProvider 会使用配置的 UserDetailsService 来加载用户的详细信息(如权限),并进行密码的验证。

  3. 认证成功:如果认证成功,AuthenticationManager 会返回一个已认证的 Authentication 对象(包含用户的权限信息)给 UsernamePasswordAuthenticationFilter。

安全上下文:SecurityContextHolder 的 SecurityContext 会被更新为包含已认证 Authentication 对象,表示当前用户已经通过认证。

在次还是建议看一遍实现前后端分离登录,逻辑和springsecurity大致一样的

登录成功后->手动生成token返回前端,过滤器用于比对当前用户是否携带token,以及把用户信息保存localthread作为上下文对象,登出->删除该token,让其无法通过过滤器,而security自带的登录登出则是将用户信息保存到SecurityContext作为上下文

可以把token放在redis 可以通过删除redis的数据让用户的token手动失效

具体实现

   <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
	<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.0</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api   javax.xml.bind.DatatypeConverter类,这是 Java 6 和 Java 7 中的 JAXB (Java Architecture for XML Binding) API 的一部分。在 Java 8 及更高版本中,JAXB 已经被移动到 java.xml.bind 模块。  jwt工具类中会用到-->
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.4.0-b180830.0359</version>
		</dependency>

	<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
jwt 工具类
@Component
@Data
@Slf4j
public class JwtUtil {

    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "iat";

    @Value("${jwt.data.SECRET}")
    private String secret;

    @Value("${jwt.data.expiration}")
    private Long expiration;

    private final DefaultClock clock = (DefaultClock) DefaultClock.INSTANCE;

    public String createToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, clock.now());
        return generateToken(claims);
    }

    public String getUsernameFromToken(String token) {
        String username;
        try {
            final Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
            log.error("Error getting username from token: {}", e.getMessage());
        }
        return username;
    }

    private Date getExpirationDateFromToken(String token) {
        final Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    private boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(clock.now());
    }

    private Claims getClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    private Date generateExpirationDate() {
        return new Date(clock.now().getTime() + expiration * 1000);
    }

    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    public boolean canTokenBeRefreshed(String token) {
        return !isTokenExpired(token);
    }

    public String refreshToken(String token) {
        final Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, clock.now());
        return generateToken(claims);
    }

    // 添加测试方法
    public  void  testJwtUtil() {

        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_USER")); // 添加一个角色
        User userDetails = new User(
                "test",
                "test",
                true,
                true, //用户账号是否过期
                true, //用户凭证是否过期
                true, //用户是否未被锁定
                authorities); // 设置权限列表


        // 创建token
        String token = createToken(userDetails);
        log.info("生成Token: {}", token);

        // 从token中获取用户名
        String usernameFromToken = getUsernameFromToken(token);
        log.info("解析出来的用户名: {}", usernameFromToken);

        // 验证token
        boolean isValid = validateToken(token, userDetails);
        log.info("是否有效? {}", isValid);

        // 刷新token
        String refreshedToken = refreshToken(token);
        log.info("刷新 Token: {}", refreshedToken);

        // 验证刷新后的token
        boolean isValidRefreshedToken = validateToken(refreshedToken, userDetails);
        log.info("验证刷新后的token? {}", isValidRefreshedToken);
    }
}


测试是否可以正常生成token

	@Autowired
	JwtUtil jwtUtil;



	@Test
	@DisplayName("测试jwtUtil")
	public void testJwtUtil() {
		jwtUtil.testJwtUtil();
	}
redis 工具类 这里用来做过滤器的时候进行请求头比对,并且用于手动让用户下线
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}`

配置redis和你的jwt属性

spring:
  data:
    redis:
      database: 1
      host: 192.168.249.133 
      port: 6379
      password: redis
      #timeout: 6000ms  # 连接超时时长(毫秒)
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security-demo
    username: root
    password: 111111
  mybatis-plus:
    configuration:
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

jwt:
  data:
    #    jwt加密密钥
    SECRET: sahksaklsjaasa
    #    jwt储存的请求头Authorization固定写法
    tokenHeader: Authorization
    #      jwt的过期时间(60s*60min*24h*7day)
    expiration: 604800
    #    jwt负载中拿到的头信息
    tokenHead: Bearer

jwt 引入后我们思考需要做的事情 之前是security服务于web默认有登录接口和登出,但是服务于表单登录的,为此我们如果只是后端开发,返回给前端一些数据的话,这里就需要自定义的登陆接口,主要是实现流程中的第三步

首先重写loaduserByusername的类,安全校验时候就会根据这个方法取出数据比较之前是使用默认案列的方法实列化userdetails

User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build()

现在自己写一个实现该接口的类,让我们定义的用户符合security的规范

@Data
public class UserDetail implements UserDetails {

//直接包含我们自己数据库的对象 这样我们自己系统的用户对象可以携带任意数据 又是符合security规范的

    private User user;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

或者给原有的User用户添加不存在的权限集合字段


@Data
@TableName("sys_user")
public class   SysUser implements Serializable, UserDetails {
    @TableId(value = "id",type = IdType.AUTO)
        private Long id;
        private String username;
        private String password;
        private String realName;
        private String avatar;
        private Integer gender;
        private String email;
        private String mobile;
        private Integer superAdmin;
        private Integer status;
        private Integer deleted;
        private Long creator;
        private LocalDateTime createTime;
        private Long updater;
        private LocalDateTime updateTime;
        @TableField(exist = false)
    private Collection<String> Authorities;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
    }


    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {

//        return false;
        return getStatus().equals(1)  ;
    }
}

登录接口 login

梳理逻辑: 获取前端发送的用户数据后,我们不自己进行比对,依靠 AuthenticationManager进行认证(考察我写的步骤三),然后生成token,返回给前端,并且保存用户数据到redis,便于进行主动删除用户的token

为此第一步就是先实际化AuthenticationManager 在配置文件中进行详细配置
配置文件

/**
 * 第一步是创建我们的Spring Security Java配置。该配置创建了一个被称为 springSecurityFilterChain 的 Servlet 过滤器,
 * 它负责应用程序中的所有安全问题(保护应用程序的URL,
 * 验证提交的用户名和密码,重定向到登录表单,等等)。下面的例子显示了Spring Security Java配置的最基本例子。
 */

@EnableWebSecurity//申明是security配置类,要么加在启动类上
@Configuration
@AllArgsConstructor
public class WebSecurityConfig {
    private final ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private RedisCache redisCache;
    @Autowired
    private JwtUtil jwtUtil;
//    从配置文件注入  ioc先扫描配置文件
@Resource
    UserMapper userMapper;

    /**
     * 是Spring Security用于处理基于数据库的用户认证的提供者。
     * DaoAuthenticationProvider需要一个UserDetailsService对象来获取用户的详细信息进行认证,
     * 所以通过setUserDetailsService()方法设置了我们之前设置的manager。
     * @return
     */
    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder( passwordEncoder());
        daoAuthenticationProvider.setUserDetailsService(new DBUserDetailsManager(userMapper));

        return daoAuthenticationProvider;
    }

    /**
     * 把默认的密码加密器换成我们自定义的加密器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
      return new BCryptPasswordEncoder();
    }

    /**
     * 这个Bean创建了一个认证管理器对象,它是Spring Security认证的核心组件之一。
     * 认证管理器负责协调和管理认证流程,并委托给一个或多个认证提供者(在这里,使用了daoAuthenticationProvider)来进行具体的认证操作。
     * 这里通过创建一个ProviderManager对象,将之前配置的daoAuthenticationProvider添加到认证管理器中。
     * 还通过setAuthenticationEventPublisher()方法设置了一个事件发布器,用于在认证事件发生时发布相关的事件,
     * 这里使用了DefaultAuthenticationEventPublisher,并传入了一个applicationEventPublisher对象,可能用于发布认证事件到Spring的事件机制中。
     * @return
     */
    @Bean
    public AuthenticationManager authenticationManager() {
        List<AuthenticationProvider> providerList = new ArrayList<>();
        providerList.add(daoAuthenticationProvider());
        ProviderManager providerManager = new ProviderManager(providerList);
        //在成功或失败的认证事件上发布相应的事件。所以,你可能并不需要显式地创建AuthenticationManager Bean 
        providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher));

        return providerManager;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {




        http.csrf(AbstractHttpConfigurer::disable)//前后端分离提供接口需要关闭
        //添加过滤器并且指定在用户密码认证过滤器前
                .addFilterBefore(new JwtAuthenticationTokenFilter(redisCache,jwtUtil), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(AbstractHttpConfigurer::disable)//无状态 这里使用的jwt代替session
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.POST,"/auth/login").permitAll() // 对登录接口允许匿名访问
                        .requestMatchers(HttpMethod.POST,"/user/add").permitAll() // 对登录接口允许匿名访问
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
//                        .requestMatchers("**").permitAll()
                        .anyRequest().authenticated())
                .exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()))
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))

            ;

        return http.build();


    }

}

优化配置

这样写的配置文件太繁琐了
直观性不强 所以进行拆解

过滤器链路配置

@AllArgsConstructor
@Configuration
public class SecurityFilterChainConfig {

    private final OncePerRequestFilter authenticationTokenFilter;
//  注入加密方式
@Bean
public PasswordEncoder passwordEncoder() {
    // 使用国密SM3加密
    return new Sm3PasswordEncoder();

}
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
        //jwt过滤器 
                .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth.antMatchers( "/auth/**").permitAll()
                        .antMatchers(HttpMethod.OPTIONS).permitAll()
                        .anyRequest().authenticated()
                )
                //登录失败异常处理器
                .exceptionHandling(exception -> exception.authenticationEntryPoint(new SecurityAuthenticationEntryPoint()))
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                .csrf(AbstractHttpConfigurer::disable)
        ;

        return http.build();
    }


}

Security其他配置配置
@Configuration
@AllArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
    private final DBUserDetailsManager userDetailsService;
    private final PasswordEncoder passwordEncoder;
    private final ApplicationEventPublisher applicationEventPublisher;

    /**
     * 定义 DaoAuthenticationProvider bean,这是一个认证提供者,专门处理基于数据库的用户信息验证。
     * 此提供者使用 passwordEncoder 来加密和验证密码,使用 userDetailsService 来加载用户详情。
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        return daoAuthenticationProvider;
    }

    /**
     * 定义 AuthenticationManager bean,它是 Spring Security 认证的核心接口。
     * 这里通过组合不同的 AuthenticationProvider 实现,可以灵活地支持多种认证方式。
     * 本示例中仅添加了 DaoAuthenticationProvider,它使用数据库用户详情进行用户名和密码的验证。
     * 如果需要支持其他认证方式,可以继承 AbstractUserDetailsAuthenticationProvider 并添加到这个列表中。
     */
    @Bean
    public AuthenticationManager authenticationManager() {
        List<AuthenticationProvider> providerList = new ArrayList<>();
        providerList.add(daoAuthenticationProvider());
        ProviderManager providerManager = new ProviderManager(providerList);
        providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher));

        return providerManager;
    }
}

DBUserDetailsManager(userMapper)这里我对之前的代码进行了修改,因为配置文件注入ioc比@compoent 优先级一些,所以采用传递参数的形式,并且loaduserByusername 返回我们封装的符合security规范的用户对象
改造后的DBUserDetailsManage : 回忆这个管理器的作用 主要是实现userdetailsService 实现loadbyusername,如果需要做加强对用户信息管理,那么可以实现UserDetailsManager, UserDetailsPasswordService 这俩个接口

如果只是需要自己实现登录逻辑那么实现userdetailsService 就可以,(对应的新增用户时候记得实用同样的加密器加密密码)

@Component
@Slf4j
@AllArgsConstructor
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {


    private UserMapper userMapper;
//    这样就可以按照security的规范来使用用户的管理
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }

    @Override
    public void createUser(UserDetails userDetails) {
//        在sql中插入信息
        User user = new User();
        user.setUsername(userDetails.getUsername());
        user.setPassword(     // 密码加密
        vo.setPassword(passwordEncoder.encode(vo.getPassword()));
        user.setEnabled(1);
        userMapper.insert(user);
    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }


    //security底层会根据这个方法来对比用户
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//        这里用户账户是唯一的
    User user = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username));
       if (user == null){
           throw new UsernameNotFoundException("系统用户不存在");
       }else{
//           1表示可用
           boolean isenabled = user.getEnabled() == 1;
/**
 * ,任何非零的整数值都会被视为 true,而 0 会被视为 false。
 */

log.info("数据库个根据用户名获取用户"+user);
        //模拟系统权限列表
//           Collection<GrantedAuthority> authorities = new ArrayList<>();
           UserDetail detail = new UserDetail();
           detail.setUser(user);
           return detail;
       }
    }
}

只重写loadbyusername接口
@Component//注入ioc 或者在配置文件中用@Bean的方式注入
@Slf4j
@AllArgsConstructor
//public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
public class DBUserDetailsManager implements UserDetailsService {

    private  final  SysUserMapper userMapper;

    //security底层会根据这个方法来对比用户
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.debug("触发方法 username={}", username);
        LambdaQueryWrapper<SysUser> wrapper = Wrappers.<SysUser>lambdaQuery()
                .eq(SysUser::getUsername, username);
//        这里用户账户是唯一的
        SysUser user = userMapper.selectOne(wrapper);
        System.out.println(user);
        if (user == null) {
            throw new UsernameNotFoundException("系统用户不存在");
        } else {
            log.debug("系统用户{}加载成功", user);
            Collection<String> authorities = new HashSet<>();//权限列表 现在数据库为空
            user.setAuthorities(authorities);
            return user;
        }
    }
}


ProviderManager:自定义事件发布器。然而,在这个情况下,可能是由于Spring Security的默认配置,它仍然会发布认证成功和失败的事件,即使你并没有显式地配置。

所以这里添加一个认证成功和失败的处理事件(一般用于记录日志)

@Component
@AllArgsConstructor
@Slf4j
public class AuthenticationEvents {


    @EventListener
    public void onSuccess(AuthenticationSuccessEvent event) {
        // 用户信息
        UserDetail user = (UserDetail) event.getAuthentication().getPrincipal();

     log.info("用户 {} 登录成功", user.getUsername());
    }

    @EventListener
    public void onFailure(AbstractAuthenticationFailureEvent event) {
        // 用户名
        String username = (String) event.getAuthentication().getPrincipal();

     log.info("用户 {} 登录失败", username);
    }

}
系统未认证用户访问异常处理器
/**
 * 匿名用户(token不存在、错误),异常处理器
 *

 */
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setContentType("application/json; charset=utf-8");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
        response.getWriter().print(JsonUtils.toJsonString(Result.error(ErrorCode.UNAUTHORIZED)));
    }
}
过滤器:用于过滤token,然后在上下文对象中存放用户信息,底层也是localthread 和文章中我们自己实现的也是一样的效果

SecurityContextHolder的底层实现是通过ThreadLocal来存储SecurityContext对象的。ThreadLocal是一个线程本地变量,它提供了线程级别的数据隔离,使得每个线程都可以独立地访问自己的数据副本,从而避免了线程安全问题。

/**
 * OncePerRequestFilter是Spring Security框架提供的一个过滤器基类,
 * 它确保在一次请求中只被调用一次。这个过滤器可以用来执行一些针对每个请求的操作,例如身份验证、授权、日志记录等。
 *
 * 类标记为@AllArgsConstructor,这意味着在创建该类的实例时,Spring 将通过构造函数注入所有已声明的依赖项(RedisCache和JwtUtil)。在使用构造函数注入时,Spring 使用类型匹配来确定哪些 bean 应该注入到构造函数中。

 */
@Slf4j
@AllArgsConstructor
@Component
@AllArgsConstructor
public class AuthenticationTokenFilter extends OncePerRequestFilter {
    private final TokenStoreCache tokenStoreCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String accessToken = TokenUtils.getAccessToken(request);
        // accessToken为空,表示未登录
        if (StringUtils.isBlank(accessToken)) {
            chain.doFilter(request, response);
            return;
        }

        // 获取登录用户信息
        UserDetail user = tokenStoreCache.getUser(accessToken);
        if (user == null) {
            chain.doFilter(request, response);
            return;
        }

        // 用户存在
        Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());

        // 新建 SecurityContext
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);

        chain.doFilter(request, response);
    }
}

过滤器对于没有携带token,或者token不匹配的各种情况做了判断和返回这样的效果和配置中的

   .exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()))

是一样的效果
准备工作完成,现在些登录接口

@Autowired
    JwtUtil jwtUtil;
@Autowired
    RedisCache redisCache;
    private final AuthenticationManager authenticationManager;
    @PostMapping("login")
    public Result login(@RequestBody User uservo) throws ServerException {
log.info("接收的参数"+uservo);
        Authentication authentication;
        try {
            // 用户认证
            authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(uservo.getUsername(), uservo.getPassword()));
        } catch (BadCredentialsException e) {
           return Result.fail("账户或密码错误");
        }
        log.info("认证通过的信息"+ authentication.getPrincipal());
        // 用户信息
        UserDetail user = (UserDetail) authentication.getPrincipal();

        // 生成 accessToken
        String token= jwtUtil.createToken(user);
        //保存到redis  前缀加用户名
        redisCache.setCacheObject("logintoken:"+user.getUser().getUsername(), JSON.toJSONString(user),8, TimeUnit.HOURS);
        return Result.success(token);
    }

其中SecurityContext和一般手动实现的ThreadLocal一样也是作为上下文对象的,并且框架自动实现了拦截器,请求过后删除,无需手动实现下面是gpt回答

是的,Spring Security 内部确实使用了 ThreadLocal 来维护 SecurityContext,实现线程安全的上下文管理。每个线程都有自己的 SecurityContext 实例,确保在处理请求时每个线程的安全上下文是独立的。

关键点
ThreadLocal: SecurityContextHolder 使用 ThreadLocal 来存储当前线程的 SecurityContext,这样每个线程都有自己独立的上下文。

自动管理: Spring Security 自动处理上下文的创建和清理。请求开始时,过滤器链会设置 SecurityContext,请求结束后,Spring Security 会自动清理上下文。通常,SecurityContext 在过滤器链中由 SecurityContextPersistenceFilter 处理。

示例
在 Spring Security 中,通常不需要手动管理 SecurityContext 的生命周期,因为框架已经处理了这个部分。你可以在 Controller 或 Service 层直接使用 SecurityContextHolder 来访问当前的 SecurityContext:

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

@RestController
public class MyController {

    @GetMapping("/user-info")
    public ResponseEntity<String> getUserInfo() {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        String username = authentication.getName(); // 获取当前用户的用户名
        return ResponseEntity.ok("User: " + username);
    }
}

清理
在正常情况下,Spring Security 会在请求处理结束后自动清理 SecurityContext

测试
请求头没有携带token字段时
在这里插入图片描述
登录获取token
在这里插入图片描述
控制台成功输出认证成功的事件,这里可以用来做登录记录保存在这里插入图片描述
携带token 访问我们上文定义好的一个读取上下文对象数据的接口 成功
在这里插入图片描述
那么同理登出就是删除reids的数据,并且可以添加登出监听,一样的逻辑不做复述

小细节

这里过滤器的操作都是全放行,但是之前自己实现判断携带token才可以访问受限资源是需要自己根据条件判断是否放行的·

@Component
@AllArgsConstructor
public class AuthenticationTokenFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(AuthenticationTokenFilter.class);


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // Log the request URI
        logger.debug("Processing request: {}", request.getRequestURI());
        String accessToken = TokenUtils.getAccessToken(request);
logger.debug("Access token: {}", accessToken);
        // accessToken为空,表示未登录
        if (StringUtils.isBlank(accessToken)) {
            //放行  security的过滤器链配置了拦截的地方权限认证的
            chain.doFilter(request, response);
            return;
        }
        // accessToken不为空,表示已登录 校验是否过期
        int i = AppJwtUtil.verifyToken(AppJwtUtil.getClaimsBody(accessToken));
        if (1==i|| 2== i){
            // 过期
            OnlineUserManager.removeUser(accessToken);
            chain.doFilter(request, response);
            return;
        }
        // 获取登录用户信息
        // 用户不存在
        if (!OnlineUserManager.containsUser(accessToken)){
            chain.doFilter(request, response);
            return;
        }

        SysUser user = OnlineUserManager.getUser(accessToken);
        Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        chain.doFilter(request, response);
        logger.debug("Completed processing request: {}", request.getRequestURI());
    }

}

主要的原因是

  .anyRequest().authenticated()

对于这些资源受限的接口.Security会判断上下文对象中是否包含Authentication 因为上下文对象本质是维护了Threadlocal线程隔离的,所以就可以对每个请求进行判断上下文中是否通过认证,所以对于所有请求大可以放心,后需的security过滤器链会判断资源是由可以访问
(自己可以注释下面代码在进行携带token 看看是不是认证就失效了)

  SysUser user = OnlineUserManager.getUser(accessToken);
    Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authentication);
GPT解释:

Spring Security的核心机制依赖于SecurityContextHolder中的Authentication对象来决定是否允许访问某个受保护的资源。

Spring Security 的默认行为
Spring Security 的默认行为是,如果一个请求需要认证(例如在你的配置中,所有未明确permitAll()的请求),它会检查SecurityContextHolder中是否存在一个有效的Authentication对象。如果没有,Spring Security会拒绝请求,返回一个未授权的响应(通常是HTTP 401错误)。
关键逻辑解释
在你的代码中,以下这段代码是关键部分:

SysUser user = OnlineUserManager.getUser(accessToken);
Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);

这个逻辑做了以下事情:
获取用户信息:OnlineUserManager.getUser(accessToken) 获取与accessToken关联的用户信息。
创建Authentication对象:UsernamePasswordAuthenticationToken 用来封装用户的认证信息(包括用户身份和权限)。
设置SecurityContext:SecurityContextHolder 中保存着当前请求的安全上下文。通过context.setAuthentication(authentication),你将用户的认证信息存入了上下文中。
SecurityContextHolder生效:当SecurityContextHolder持有一个有效的Authentication对象时,Spring Security会认为这个请求是经过认证的,可以访问那些需要认证的接口。
不设置SecurityContextHolder 的效果
如果你不执行上述代码(即不设置SecurityContextHolder中的Authentication),那么即使请求被你的过滤器放行,Spring Security仍然会认为这个请求是未认证的。最终,Spring Security会拒绝这个请求访问那些需要认证的接口。

security的认证流程

虽然上面的文章已经详细说明了,这里做总结

这个流程会帮助你更好地理解Spring Security是如何管理认证和授权的。

  1. 请求到达
    当用户尝试访问受保护的资源(例如,需要登录才能访问的页面或API),请求会首先经过Spring Security的过滤器链。
  2. UsernamePasswordAuthenticationFilter 处理登录请求
    对于表单登录(Form Login),Spring Security默认使用UsernamePasswordAuthenticationFilter来处理登录请求。这个过滤器会拦截登录请求(通常是/login路径),并提取用户名和密码。
  3. 认证管理器 (AuthenticationManager) 处理认证
    UsernamePasswordAuthenticationFilter会将提取到的用户名和密码封装成一个UsernamePasswordAuthenticationToken对象,并将其提交给AuthenticationManager进行认证。
    AuthenticationManager通常会委托给一个或多个AuthenticationProvider来执行具体的认证逻辑。
    一个常见的AuthenticationProvider是DaoAuthenticationProvider,它通过加载用户的详细信息(如用户名、密码和权限)来进行认证。
  4. UserDetailsService 加载用户信息
    DaoAuthenticationProvider会使用UserDetailsService加载用户信息。UserDetailsService是一个接口,它的实现类会从数据库或其他持久化存储中加载用户的信息(如用户名、密码、权限等)。
    如果用户名和密码匹配,AuthenticationProvider会创建一个已认证的Authentication对象,并将其返回给AuthenticationManager。
  5. 认证成功或失败
    认证成功:认证成功后,AuthenticationManager会将认证信息(包括用户的身份和权限)保存到SecurityContextHolder中,供后续的安全检查使用。
    认证失败:如果认证失败,Spring Security会返回一个认证失败的响应(通常是HTTP 401错误)。
    l>6. 设置SecurityContextHolder
    如果认证成功,Spring Security会将用户的认证信息放入SecurityContextHolder中。SecurityContextHolder是一个持有安全上下文的全局对象,它保存了当前请求的安全信息。
    这一步是关键,所有后续的请求处理都会依赖于SecurityContextHolder中的Authentication对象。
  6. 请求放行
    如果认证成功,Spring Security会允许请求继续处理,并最终访问受保护的资源。
  7. 访问受保护的资源
    在访问受保护资源时,Spring Security会检查SecurityContextHolder中的Authentication对象,以确定用户是否经过认证以及是否有足够的权限访问请求的资源。
    如果用户有足够的权限,Spring Security会允许访问。
    如果用户没有足够的权限或未认证,Spring Security会拒绝访问,并返回相应的错误响应。
  8. 后续请求的处理
    对于后续的每个请求,Spring Security会再次检查SecurityContextHolder中的Authentication对象。如果用户已认证且持有足够权限,Spring Security会允许访问受保护的资源。
    额外说明:自定义过滤器的作用
    当你创建一个自定义过滤器(例如你提到的AuthenticationTokenFilter),并将其添加到过滤器链中时,你可以控制是否将认证信息放入SecurityContextHolder中。如果你选择不设置SecurityContextHolder,那么即使请求通过了过滤器,Spring Security仍然会认为请求未认证,从而拒绝访问需要认证的资源。

总结
登录认证过程主要包括:拦截登录请求、验证用户名和密码、将认证信息存入SecurityContextHolder。
自定义过滤器的作用:可以在过滤器中对请求进行检查,并决定是否将认证信息放入SecurityContextHolder,进而影响请求是否能够访问受保护的资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝胖子不是胖子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值