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

简介

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

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

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

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

​ 而认证和授权也是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}的作用

通过使用@{/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.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;@Configuration
@EnableWebSecurity//SPRINGBOOT项目忽略
public class WebSecurityConfig {@Beanpublic 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自定义配置

基于内存配置

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

官方文档:Java自定义配置

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

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

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

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

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

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

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

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

基于数据库的数据源

所以接下来通过改造重写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 {@Resourcepublic 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的规范来使用用户的管理@Overridepublic UserDetails updatePassword(UserDetails user, String newPassword) {return null;}
//原来的内存管理器是在用户在添加到内存的map,实现这个方法这里插入数据库@Overridepublic void createUser(UserDetails userDetails) {
//        在sql中插入信息User user = new User();user.setUsername(userDetails.getUsername());user.setPassword(userDetails.getPassword());user.setEnabled(1);userMapper.insert(user);}@Overridepublic void updateUser(UserDetails user) {}@Overridepublic void deleteUser(String username) {}@Overridepublic void changePassword(String oldPassword, String newPassword) {}@Overridepublic boolean userExists(String username) {return false;}@Resourceprivate UserMapper userMapper;//security底层会根据这个方法来对比用户@Overridepublic 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";}
}

修改配置文件

    @Beanpublic 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;@Overridepublic void adduser(User user) {log.info("最开始接收到的密码"+user.getPassword());
//            security的userUserDetails 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

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

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

登录认证返回结果为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
{@ResourceUserMapper userMapper;@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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()); //注销成功时的处理
});

在这里插入图片描述

请求未认证的接口
实现AuthenticationEntryPoint接口

Servlet Authentication Architecture :: Spring Security

当访问一个需要认证之后才能访问的接口的时候,Spring Security会使用AuthenticationEntryPoint将用户请求跳转到登录页面,要求用户提供登录凭证。

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

package com.atguigu.securitydemo.config;public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic 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失效@Overridepublic 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); // 设置权限列表// 创建tokenString token = createToken(userDetails);log.info("生成Token: {}", token);// 从token中获取用户名String usernameFromToken = getUsernameFromToken(token);log.info("解析出来的用户名: {}", usernameFromToken);// 验证tokenboolean isValid = validateToken(token, userDetails);log.info("是否有效? {}", isValid);// 刷新tokenString refreshedToken = refreshToken(token);log.info("刷新 Token: {}", refreshedToken);// 验证刷新后的tokenboolean isValidRefreshedToken = validateToken(refreshedToken, userDetails);log.info("验证刷新后的token? {}", isValidRefreshedToken);}
}

测试是否可以正常生成token

	@AutowiredJwtUtil jwtUtil;@Test@DisplayName("测试jwtUtil")public void testJwtUtil() {jwtUtil.testJwtUtil();}

redis 工具类 这里用来做过滤器的时候进行请求头比对,并且用于手动让用户下线

@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{@Autowiredpublic 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: 1host: 192.168.249.133 port: 6379password: redis#timeout: 6000ms  # 连接超时时长(毫秒)datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/security-demousername: rootpassword: 111111mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpljwt: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;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
登录接口 login

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

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

/*** 第一步是创建我们的Spring Security Java配置。该配置创建了一个被称为 springSecurityFilterChain 的 Servlet 过滤器,* 它负责应用程序中的所有安全问题(保护应用程序的URL,* 验证提交的用户名和密码,重定向到登录表单,等等)。下面的例子显示了Spring Security Java配置的最基本例子。*/@EnableMethodSecurity
@Configuration
@AllArgsConstructor
public class WebSecurityConfig {private final ApplicationEventPublisher applicationEventPublisher;@Autowiredprivate RedisCache redisCache;@Autowiredprivate JwtUtil jwtUtil;
//    从配置文件注入  ioc先扫描配置文件
@ResourceUserMapper userMapper;/*** 是Spring Security用于处理基于数据库的用户认证的提供者。* DaoAuthenticationProvider需要一个UserDetailsService对象来获取用户的详细信息进行认证,* 所以通过setUserDetailsService()方法设置了我们之前设置的manager。* @return*/@BeanDaoAuthenticationProvider daoAuthenticationProvider() {DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setPasswordEncoder( passwordEncoder());daoAuthenticationProvider.setUserDetailsService(new DBUserDetailsManager(userMapper));return daoAuthenticationProvider;}/*** 把默认的密码加密器换成我们自定义的加密器* @return*/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}/*** 这个Bean创建了一个认证管理器对象,它是Spring Security认证的核心组件之一。* 认证管理器负责协调和管理认证流程,并委托给一个或多个认证提供者(在这里,使用了daoAuthenticationProvider)来进行具体的认证操作。* 这里通过创建一个ProviderManager对象,将之前配置的daoAuthenticationProvider添加到认证管理器中。* 还通过setAuthenticationEventPublisher()方法设置了一个事件发布器,用于在认证事件发生时发布相关的事件,* 这里使用了DefaultAuthenticationEventPublisher,并传入了一个applicationEventPublisher对象,可能用于发布认证事件到Spring的事件机制中。* @return*/@Beanpublic AuthenticationManager authenticationManager() {List<AuthenticationProvider> providerList = new ArrayList<>();providerList.add(daoAuthenticationProvider());ProviderManager providerManager = new ProviderManager(providerList);//在成功或失败的认证事件上发布相应的事件。所以,你可能并不需要显式地创建AuthenticationManager Bean providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher));return providerManager;}@Beanpublic 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();}}

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

@Component
@Slf4j
@AllArgsConstructor
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {private UserMapper userMapper;
//    这样就可以按照security的规范来使用用户的管理@Overridepublic UserDetails updatePassword(UserDetails user, String newPassword) {return null;}@Overridepublic void createUser(UserDetails userDetails) {
//        在sql中插入信息User user = new User();user.setUsername(userDetails.getUsername());user.setPassword(userDetails.getPassword());user.setEnabled(1);userMapper.insert(user);}@Overridepublic void updateUser(UserDetails user) {}@Overridepublic void deleteUser(String username) {}@Overridepublic void changePassword(String oldPassword, String newPassword) {}@Overridepublic boolean userExists(String username) {return false;}//security底层会根据这个方法来对比用户@Overridepublic 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;}}
}

ProviderManager:并配置了authenticationEventPublisher,这实际上是一个正确的步骤,因为这样可以自定义事件发布器。然而,在这个情况下,可能是由于Spring Security的默认配置,它仍然会发布认证成功和失败的事件,即使你并没有显式地配置。
所以这里添加一个认证成功和失败的处理事件

@Component
@AllArgsConstructor
@Slf4j
public class AuthenticationEvents {@EventListenerpublic void onSuccess(AuthenticationSuccessEvent event) {// 用户信息UserDetail user = (UserDetail) event.getAuthentication().getPrincipal();log.info("用户 {} 登录成功", user.getUsername());}@EventListenerpublic void onFailure(AbstractAuthenticationFailureEvent event) {// 用户名String username = (String) event.getAuthentication().getPrincipal();log.info("用户 {} 登录失败", username);}}

过滤器:用于过滤token,然后在上下文对象中存放用户信息,底层也是localthread 和文章中我们自己实现的也是一样的效果

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

/*** OncePerRequestFilter是Spring Security框架提供的一个过滤器基类,* 它确保在一次请求中只被调用一次。这个过滤器可以用来执行一些针对每个请求的操作,例如身份验证、授权、日志记录等。** 类标记为@AllArgsConstructor,这意味着在创建该类的实例时,Spring 将通过构造函数注入所有已声明的依赖项(RedisCache和JwtUtil)。在使用构造函数注入时,Spring 使用类型匹配来确定哪些 bean 应该注入到构造函数中。*/
@Slf4j
@AllArgsConstructorpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter {private final RedisCache redisCache;private final   JwtUtil jwtUtil;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 获取tokenString token = request.getHeader("token");String requestURI = request.getRequestURI();// 假设"/auth/login"是登录路径,您可以根据需要添加更多路径if (requestURI.equals("/auth/login")) {filterChain.doFilter(request, response);return;}response.setContentType("application/json; charset=utf-8");response.setHeader("Access-Control-Allow-Credentials", "true");if (!StringUtils.hasText(token)) {//创建结果对象//转换成json字符串String json = JSON.toJSONString(Result.nologin("需要登录"));//根据实际情况自定义错误信息
//            filterChain.doFilter(request, response);response.getWriter().println(json);return;}// 解析tokenString username;try {username = jwtUtil.getUsernameFromToken(token);log.info("解析到的用户名为:{}", username);} catch (Exception e) {e.printStackTrace();String json = JSON.toJSONString(Result.nologin("Token非法"));response.getWriter().println(json);return;}// 从Redis中获取用户信息String redisKey = "logintoken:" + username;String jsonString = redisCache.getCacheObject(redisKey);if (StringUtils.isEmpty(jsonString)) {log.info(jsonString);String json = JSON.toJSONString(Result.nologin("用户登录已过期"));response.getWriter().println(json);return;}// 转换为对象UserDetail userInfo = JSON.parseObject(jsonString, UserDetail.class);// TODO: 进一步检查用户权限并封装到Authentication中
//        放入上下文对象,方便随取随用// 存入SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(userInfo, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 放行filterChain.doFilter(request, response);}
}

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

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

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

@AutowiredJwtUtil jwtUtil;
@AutowiredRedisCache 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();// 生成 accessTokenString token= jwtUtil.createToken(user);//保存到redis  前缀加用户名redisCache.setCacheObject("logintoken:"+user.getUser().getUsername(), JSON.toJSONString(user),8, TimeUnit.HOURS);return Result.success(token);}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/261166.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在Ubuntu部署Emlog,并将本地博客发布至公网可远程访问

文章目录 前言1. 网站搭建1.1 Emolog网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2.Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 3. 公网访问测试总结 前言 博客作为使…

【JAVA高级面试题】运用锁机制实现一个自定义的阻塞队列

文章目录 前言实战演示写在最后 前言 前几天看见一个高级Java面试题&#xff0c;我觉得很有代表意义。既考察了面试者的基本锁机制运用&#xff0c;也了解了阻塞队列的产生实现原理。先分享出来&#xff0c;以供鉴赏。 面试题&#xff1a;实现一个自定义的阻塞队列&#xff0c…

大数据云计算 - 弹性计算技术全解与实践

文章目录 大数据云计算 - 弹性计算技术全解与实践一、引言弹性&#xff1a;不仅仅是扩展性技术与商业价值 二、基础概念什么是弹性计算&#xff1f;CPU与内存的动态分配与虚拟化的关系 类型公有云与私有云虚拟机、容器与无服务器 优势与挑战优势挑战 实例&#xff1a;Netflix的…

代码随想录算法训练营第二十四天 | 回溯算法理论基础,77. 组合 [回溯篇]

代码随想录算法训练营第二十四天 回溯算法理论基础什么是回溯法回溯法的理解回溯法模板 LeetCode 77.组合题目描述思路参考代码总结优化版本 回溯算法理论基础 文章讲解&#xff1a;代码随想录#回溯算法理论基础 视频讲解&#xff1a;带你学透回溯算法&#xff08;理论篇&#…

体验一下UE5.3的Skeletal Editor

UE5.3中增加了蒙皮网格骨架编辑工具&#xff0c;用户无需导出Fbx就可以直接编辑蒙皮网格&#xff0c;支持修改绑定姿势的骨骼位置、修改蒙皮权重、对已蒙皮多边形进行编辑以及对蒙皮网格减免等操作&#xff0c;就来体验一下。 1.加载插件 要使用Skeletal Editor功能&#xff…

Linux第58步_备份busybox生成rootfs根文件系统

备份busybox生成rootfs根文件系统 打开终端 输入“ls回车” 输入“cd linux/回车” 输入“ls回车”&#xff0c;产看“linux”目录下的文件和文件夹 输入“cd nfs/回车”&#xff0c;切换到“nfs”目录 输入“ls回车”&#xff0c;产看“nfs”目录下的文件和文件夹 输入…

Conda管理Python不同版本教程

Conda管理Python不同版本教程 目录 0.前提 1.conda常用命令 2.conda设置国内源&#xff08;以添加清华源为例&#xff0c;阿里云源同样&#xff09; 3.conda管理python库 4.其它 不太推荐 pyenv管理Python不同版本教程&#xff08;本人另一篇博客&#xff0c;姊妹篇&…

力扣 309. 买卖股票的最佳时机含冷冻期

题目来源&#xff1a;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/ C题解&#xff1a;动态规划 状态1&#xff1a;表示持有股票。更新为之前持有股票&#xff08;dp[i-1][0]&#xff09;或者不持有股票且不处于冷冻期后买入&…

【Go语言】Go语言的数据类型

GO 语言的数据类型 Go 语言内置对以下这些基本数据类型的支持&#xff1a; 布尔类型&#xff1a;bool 整型&#xff1a;int8、byte、int16、int、uint、uintptr 等 浮点类型&#xff1a;float32、float64 复数类型&#xff1a;complex64、complex128 字符串&#xff1a;st…

创意办公:专注 ONLYOFFICE,探索办公新境界

一.ONLYOFFICE 介绍 ONLYOFFICE 是一个基于 Web 的办公套件&#xff0c;提供了文档处理、电子表格和演示文稿编辑等功能。它被设计为一个协作工具&#xff0c;支持多人实时协作编辑文档&#xff0c;并且可以在本地部署或者作为云服务使用。 二.ONLYOFFICE 特点和功能 以下是 …

Bert基础(三)--位置编码

背景 还是以I am good&#xff08;我很好&#xff09;为例。 在RNN模型中&#xff0c;句子是逐字送入学习网络的。换言之&#xff0c;首先把I作为输入&#xff0c;接下来是am&#xff0c;以此类推。通过逐字地接受输入&#xff0c;学习网络就能完全理解整个句子。然而&#x…

Eclipse - Text Editors (文本编辑器)

Eclipse - Text Editors [文本编辑器] References Window -> Preferences -> General -> Editors -> Text Editors Displayed tab witdth: 4 勾选 Insert spaces for tabs 勾选 Show line number References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.n…

《Solidity 简易速速上手小册》第8章:高级 Solidity 概念(2024 最新版)

文章目录 8.1 高级数据类型和结构8.1.1 基础知识解析更深入的理解实际操作技巧 8.1.2 重点案例&#xff1a;构建一个去中心化身份系统案例 Demo&#xff1a;创建去中心化身份系统案例代码DecentralizedIdentityContract.sol 测试和验证拓展案例 8.1.3 拓展案例 1&#xff1a;管…

ARM 之十六 详解 CMSIS 版本变迁、各组件使用示例

目前,CMSIS 已经发展到了第六版,其目录结构也发生了重大的变化。在不断发展中,很多原来 CMSIS 的组件被不断独立出去,并因此成立了很多开源社区,今天就来学习一下! 由于 CMSIS 已经包含了相当丰富的文档,因此,本文重点学习版本之间的变化以及一些实际使用示例。 什么是…

Git笔记——1

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 Git安装_centos 创建本地仓库 配置本地仓库 添加文件——场景一 查看.git文件 添加文件——场景二 修改文件 版本回退 总结 前言 世上有两种耀眼的光芒&#…

GaiaDB-X 获选北京国家金融科技认证中心「数据领域首批专项示范与先行单位」

2023 年 12 月 21 日至 22 日&#xff0c;「2023北京国际金融安全论坛暨金融科技标准认证生态大会」在北京金融安全产业园举办。百度智能云分布式数据库 GaiaDB-X 产品荣登「数据领域首批专项示范与先行单位」名单&#xff0c;并获得了由北京国家金融科技认证中心颁发的「数据产…

开源软件的利弊

目录 开源软件 优势 免费 透明 可更改 可协作 影响力 坏处 安全隐患 良莠不齐 学习成本 持续性问题 未知风险 开源软件 开源软件是一种基于开放协作和共享的软件开发模式&#xff0c;其利弊对于软件产业和社会发展具有重要意义 优势 免费 谁能拒绝不要钱的东西…

[ai笔记11] 论ai韭菜的自我修养

欢迎来到文思源想的ai空间&#xff0c;这是技术老兵学习ai以及观点分享的第11篇内容&#xff01; 上班之后时间确实少了许多&#xff0c;但是最近也没闲着&#xff0c;关于ai的学习一直在探索两个部分&#xff0c;一个是看那本有名的书《这就是ChatGPT》&#xff0c;另外一个则…

新 Mac 使用指南

文章目录 1、安装软件2、修改启动台3、修改 iCloud 设置、同步数据4、管理文件夹5、管理侧边栏6、设置快捷键 更新版本出现问题&#xff08;有机会更新下问题和解决方式&#xff09;&#xff0c;重装 Sonoma&#xff0c;获得了一个新的 macOS。以新用户的视角来看&#xff0c;有…

PHP实践:Laravel中事件使用讲解

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…