SpringSecurity登录逻辑快速集成及原理探查

框架简介

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。 一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权 (Authorization)两个部分,这两点也是 Spring Security 重要核心功能。
(1)用户认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
(2)用户授权:经过认证后判断当前用户是否有权限进行某个操作

注:本文基于B站up主“三更草堂”讲解视频进行简化和说明。

  • 使用hutool的jwt工具
  • 使用Redis自带的redisTemplate
  • 去掉过时的接口WebsecurityConfigurerAdapter
  • 去掉无关紧要的代码

前置:前后端分离,默认您已建立好最原始的SpringBoot工程,并连通Mysql,Redis。

总体逻辑

在这里插入图片描述

准备工作

  1. 在pom.xml文件中添加如下四个依赖,除第一个核心依赖外其他的都是为了简化开发。
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
		<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.2</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.22</version>
        </dependency>
  1. 数据库存储密文密码加密方式。
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    public void TestBCryptPasswordEncoder(){
		String encode = passwordEncoder.encode("1234");
		System.out.println(encode);
		// 验证,如果匹配则返回True
        System.out.println(passwordEncoder.matches("1234", 
        			"$2a$10$npv5JSeFR6/wLz8BBMmSBOMb8byg2eyfK4/vvoBk3RKtTLBhIhcpy"));
    }
  1. 实体类User,只包含了必须字段,注意用户名和密码必须叫username和password
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    //用户id
    private int id;
    //用户名
    private String username;
    //用户密码
    private String password;
}
  1. 其他层(Conttroller、Service、ServiceImpl、Mapper)文件请自行建好,并写一个测试接口。
  2. 公共返回类(非必须)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseResult<T> {
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 提示信息,如果有错误时,前端可以获取该字段进行提示
     */
    private String msg;
    /**
     * 查询到的结果数据,
     */
    private T data;

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

核心逻辑

1、当引入Spring Security依赖后,尝试去访问接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。

  • SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。
    在这里插入图片描述
  • 图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
    UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要由它负责。
    ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
    FilterSecurityInterceptor:负责权限校验的过滤器。

2、认证流程在这里插入图片描述
概念速查:
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

3、实现思路

登录
①自定义登录接口,调用ProviderManager的方法进行认证,如果认证通过生成jwt,把用户信息存入redis中
②自定义UserDetailsService,在这个实现类中去查询数据库
校验:
①定义JWT认证过滤器,获取token,解析token获取其中的userId,从redis中获取用户信息存入SecurityContextHolder

实现步骤

1、前面的用户名密码认证是走的 UserDetailsService 中默认的方法,也就是上图的第五步,因此创建一个类实现UserDetailsService 接口,重写 loadUserByUsername 方法,使其从数据库中查询用户信息。

@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername, username);
        User user = userMapper.selectOne(wrapper);
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名不存在");
        }
        return new LoginUser(user);
    }
}

2、因为 UserDetailsService 方法的返回值是 UserDetails 类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

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

3、编写登录接口(只放实现代码,其他层自己写好)

	// 登录,user 中包含前端传过来的 username 和 password
	@Override
    public ResponseResult<Map<String, String>> login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // 认证成功之后,获取用户id
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = String.valueOf(loginUser.getUser().getId());
        // 将用户id存入token的Payload中
        Map<String, Object> map = new HashMap<String, Object>() {
            private static final long serialVersionUID = 1L;
            {
                put("userId", userId);
            }
        };
        String token = JWTUtil.createToken(map, "jwt-secret".getBytes());
        Map<String, String> resultMap = new HashMap<>();
        resultMap.put("token", token);
        // 把完整的用户信息存入redis,userId作为key,过期时间为60分钟
        redisTemplate.opsForValue().set(userId, loginUser, 60 * 60, TimeUnit.SECONDS);
        return new ResponseResult<>(200, "登录成功", resultMap);
    }
    // 注销登录,小坑:注意注销登录的接口不能直接是 /logout
    @Override
    public ResponseResult<String> logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser principal = (LoginUser) authentication.getPrincipal();
        Integer userId = principal.getUser().getId();
        redisTemplate.delete(String.valueOf(userId));
        return new ResponseResult<>(200, "退出成功");
    }

4、配置放行登录接口(三更中使用的 WebsecurityConfigurerAdapter 在 Spring Security 5.3 版本中已被弃用)

@Configuration
@AllArgsConstructor
public class SecurityConfig {

    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
        // 下面配置为验证数据库密码时可以存明文,方便测试。
        // return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 关闭csrf
                .csrf().disable()
                // 前后端分离,不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 添加自定义jwt过滤器,配置在UsernamePasswordAuthenticationFilter过滤器前面
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        return http.build();
    }
}

5、自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的 userId。使用 userId 去 redis 中获取对应的 LoginUser 对象。然后封装 Authentication 对象存入 SecurityContextHolder,因为后面的过滤器需要使用该对象。

@Component
@AllArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private RedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("Authorization");
        if (!StringUtils.hasText(token)) {
            // 放行
            filterChain.doFilter(request, response);
            // 因为后面还有过滤器,防止响应回来代码继续往下执行
            return;
        }
        // 验证token
        if (!JWTUtil.verify(token, "jwt-secret".getBytes())) {
            throw new RuntimeException("token无效");
        }
        //解析token
        JWT jwt = JWTUtil.parseToken(token);
        Object userId = jwt.getPayload("userId");
        //从redis中获取用户信息
        LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get(userId);
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("用户未登录");
        }
        // 存入SecurityContextHolder,因为后面的过滤器需要对请求进行认证,可以判断 SecurityContextHolder 里的用户信息
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

思考:

看完整个流程,是不是发现没有校验数据库密码的代码

认证流程源码探查

下面是没有用dubug模式,建议使用dubug模式,可以参考下面的流程跟踪流程。

1、在登录方法里点击校验方法 authenticat()。
在这里插入图片描述

2、跳转到 AuthenticationManager 接口,接下来进入实现该方法的类 ProviderManager。
在这里插入图片描述
在这里插入图片描述

3、在 ProviderManager 类的 authenticate() 方法中进入调用的方法 provider.authenticate() 中。
在这里插入图片描述

4、在 AuthenticationProvider 接口里找到实现 authenticate() 方法的类。
在这里插入图片描述
在这里插入图片描述

5、在 AbstractUserDetailsAuthenticationProvider 中进入方法 retrieveUser() 中。
在这里插入图片描述

6、进入继承抽象类 AbstractUserDetailsAuthenticationProvider 的 DaoAuthenticationProvider 类,在 retrieveUser() 方法中进入方法 loadUserByUsername() 中。
在这里插入图片描述
在这里插入图片描述

7、进入到了熟悉的接口 UserDetailsService 中,然后找实现该接口的方法。
在这里插入图片描述
在这里插入图片描述

8、最终形成一个闭环。
在这里插入图片描述

注意:那么,检验密码的地方在哪里

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java学习——设计模式——介绍

文章目录 设计模式介绍UML的类图表示类与类之间关系的表示关联关系聚合关系组合关系依赖关系继承关系实现关系 设计模式介绍 设计模式design patterns&#xff0c;指在软件设计中&#xff0c;被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码&#xff0c;提…

Python序列之集合

系列文章目录 Python序列之列表Python序列之元组Python序列之字典Python序列之集合&#xff08;本篇文章&#xff09; Python序列之集合 系列文章目录前言一、集合是什么&#xff1f;二、集合的操作1.集合的创建&#xff08;1&#xff09;使用{}创建&#xff08;2&#xff09;…

电商大数据商品采集:阿里巴巴1688电商网站货源产品信息采集

商品详情大数据采集:阿里巴巴1688电商网站货源产品信息采集 ------------- 数据采集满足多种业务场景&#xff1a;适合产品、运营、销售、数据分析、政府机关、电商从业者、学术研究等多种身份 职业。 舆情监控&#xff1a;全方位监测公开信息&#xff0c;抢先获取舆论趋势。 市…

计算机组成原理复习4

习题 练习题 下列不属于系统总线的为&#xff08;&#xff09; a.数据总线 b.地址总线 c.控制总线 d.片内总线 D 系统总线中地址总线的功能是&#xff08;&#xff09; a.选择主存单元地址 b.选择进行信息传输的设备 c.选择外存地址 d.指定主存和I/O设备接口电路的地址 D 解…

【操作系统xv6】学习记录1

前置说明&#xff1a; git-v9版本&#xff1a;git clone https://github.com/mit-pdos/xv6-public/tree/xv6-rev9 bili:https://www.bilibili.com/video/BV15r4y1z75F 深圳大学罗秋明老师的课程 我自己用的wsl2的ubuntu18 无桌面版本 make qemu-nox bug 起初在双系统的ubuntu…

数据模型设计

数据模型设计&#xff0c;可以理解为数据库中的表结构设计。 我们在设计器中创建的数据模型&#xff0c;也称为实体。我们将前端页面中传过来的数据保存到对应的实体中&#xff0c;即为将前端数据保存到了数据库中。 1 、实体与枚举的创建 1 .1 创建供应商 supplier实体 在左…

旁挂二层隧道转发小实验

WLAN配置 旁挂二层隧道转发 1.基础配置&#xff1a; SW1: system-view vlan batch 100 to 101interface GigabitEthernet 0/0/1 port link-type trunk port trunk pvid vlan 100 //打上管理VLAN的100标签 port trunk allow-pass vlan 100 101interface GigabitEthernet 0/…

蓝桥杯C/C++程序设计——单词分析

题目描述 小蓝正在学习一门神奇的语言&#xff0c;这门语言中的单词都是由小写英文字母组 成&#xff0c;有些单词很长&#xff0c;远远超过正常英文单词的长度。小蓝学了很长时间也记不住一些单词&#xff0c;他准备不再完全记忆这些单词&#xff0c;而是根据单词中哪个字母出…

mount -a 出错任然重启问题

问题来源 在磁盘分区挂载过后&#xff0c;为了创建的新分区的能够永久挂载&#xff0c;我们常常会在/etc/fstab下写下配置文件&#xff0c;使其永久挂载。但是该配置一旦写错&#xff0c;就面临这死机问题&#xff0c;为此&#xff0c;以下操作针对该问题进行 解决方案&#x…

WSL使用VsCode运行cpp文件

文章目录 缘起主要步骤参考 缘起 今天在阅读《C20设计模式-可复用的面向对象设计方法&#xff08;原书第2版&#xff09;》的时候&#xff0c;遇到代码想要运行一下&#xff0c;于是决定使用wsl下的vscode配置cpp的环境。 主要步骤 1.安装gcc和g编译器 打开命令行输入wsl&am…

linux驱动(一):led

本文主要探讨210的led驱动相关知识。 驱动 操作系统驱动硬件的代码,驱动上层是系统调用API,下层是硬件 宏内核&#xff1a;内核整体上为一个过程实现,运行在同一地址空间,相互调用简单高效 微内核&#xff1a;功能为独立过程,过程间通过IPC通信 …

从实际工作情况,介绍嵌入式(MCU)软件开发常用(通用)工具

目录 前言 1、代码阅读及编辑工具&#xff08;VSCode、Understand&#xff09; 2、代码对比工具&#xff08;Beyond Compare&#xff09; 3、代码仓库相关工具&#xff08;Git、SVN、Tortoise&#xff09; 4、文本编辑器&#xff08;Notepad&#xff09; 5、电脑文件搜索工…

使用递归实现深拷贝

文章目录 为什么要使用递归什么深拷贝具体实现基础实现处理 函数处理 Symbol处理 Set处理 Map处理 循环引用 结语-源码 为什么要使用递归什么深拷贝 我们知道在 JavaScript 中可以通过使用JSON序列化来完成深拷贝&#xff0c;但是这种方法存在一些缺陷&#xff0c;比如对于函数…

「Kafka」生产者篇

「Kafka」生产者篇 生产者发送消息流程 在消息发送的过程中&#xff0c;涉及到了 两个线程 ——main 线程和Sender 线程。 在 main 线程中创建了 一个 双端队列 RecordAccumulator。 main线程将消息发送给RecordAccumulator&#xff0c;Sender线程不断从 RecordAccumulator…

<软考高项备考>《论文专题 - 37 采购管理(2) 》

2 过程1-规划采购管理 2.1 问题 4W1H过程做什么记录项目采购决策、明确采购方法&#xff0c;及识别潜在卖方的过程作用&#xff1a;确定是否从项目外部获取货物和服务&#xff0c;如果是&#xff0c;则还要确定将在什么时间、以什么方式获取什么货物和服务为什么做为如何采购…

移动端开发框架mui代码在安卓模拟器上运行(HbuilderX连接到模拟器)

开发工具 HBuilder X 3.8.12.20230817 注意&#xff1a;开发工具尽量用最新的或较新的。太旧的版本在开发调试过程中可能会出现莫名其妙的问题。 1、电脑下载安装安卓模拟器 我这里使用的是 夜神模拟器 &#xff0c;也可以选择其他安卓模拟器 夜神模拟器官网&#xff1a;夜神安…

idea实现Java连接MySQL数据库

1.下载MySQL并安装 首先如果没有mysql的需要先下载MySQL&#xff0c;可以看这个教程&#xff1a; Mysql超详细安装配置教程(保姆级)_mysql安装及配置超详细教程-CSDN博客 2.下载mysql 的jdbc驱动 官网&#xff1a;MySQL :: Download Connector/J 解压并将驱动jar包导入id…

ThinkPad T14s Gen3,ThinkPad X13 Gen3(21BS,21BQ,21BR,21BN)原装出厂Win11系统

lenovo联想ThinkPad系列T14s/X13 Gen3笔记本电脑原装Windows11预装OEM系统镜像 链接&#xff1a;https://pan.baidu.com/s/1yhRMIjlkFvt86aLioOoNOA?pwdfrsp 提取码&#xff1a;frsp 原厂系统自带所有驱动、出厂主题壁纸、系统属性专属联机支持标志、Office办公软件、联想…

内网常规攻击路径

点击星标&#xff0c;即时接收最新推文 随着网络技术的发展&#xff0c;企业内部网络架构的变化&#xff0c;网络设备多样性的增加&#xff0c;面对内网攻击&#xff0c;防御体系逐渐阶梯化&#xff0c;通过不同维度的防御联动&#xff0c;将攻击拒之门外。对于突破网络边界后进…

Rust学习笔记000 安装

安装命令 curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh $ curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh info: downloading installerWelcome to Rust!This will download and install the official compiler for the Rust programming la…