当前位置: 首页 > news >正文

整合SpringSecurity+JWT实现登录认证

一、关于 SpringSecurity

在 Spring Boot 出现之前,SpringSecurity 的使用场景是被另外一个安全管理框架 Shiro 牢牢霸占的,因为相对于 SpringSecurity 来说,SSM 中整合 Shiro 更加轻量级。Spring Boot 出现后,使这一情况情况大有改观。

这是因为 Spring Boot 为 SpringSecurity 提供了自动化配置,大大降低了 SpringSecurity 的学习成本。另外,SpringSecurity 的功能也比 Shiro 更加强大。

JWT,目前最流行的一个跨域认证解决方案:客户端发起用户登录请求,服务器端接收并认证成功后,生成一个 JSON 对象(如下所示),然后将其返回给客户端。

从本质上来说,JWT 就像是一种生成加密用户身份信息的 Token,更安全也更灵活。

三、整合步骤

第一步,给需要登录认证的模块添加 codingmore-security 依赖:

<!--        后端管理模块需要登录认证--><dependency><groupId>top.codingmore</groupId><artifactId>codingmore-security</artifactId><version>1.0-SNAPSHOT</version>

比如说 codingmore-admin 后端管理模块需要登录认证,就在 codingmore-admin/pom.xml 文件中添加 codingmore-security 依赖。

第二步,在需要登录认证的模块里添加 CodingmoreSecurityConfig 类,继承自 codingmore-security 模块中的 SecurityConfig 类。

Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CodingmoreSecurityConfig extends CustomSecurityConfig {@Autowiredprivate IUsersService usersService;@Autowiredprivate IResourceService resourceService;@Beanpublic UserDetailsService userDetailsService() {//获取登录用户信息return username -> usersService.loadUserByUsername(username);}

UserDetailsService 这个类主要是用来加载用户信息的,包括用户名、密码、权限、角色集合....其中有一个方法如下:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

认证逻辑中,SpringSecurity 会调用这个方法根据客户端传入的用户名加载该用户的详细信息,包括判断:

  • 密码是否一致
  • 通过后获取权限和角色
    public UserDetails loadUserByUsername(String username) {// 根据用户名查询用户Users admin = getAdminByUsername(username);if (admin != null) {List<Resource> resourceList = getResourceList(admin.getId());return new AdminUserDetails(admin,resourceList);}throw new UsernameNotFoundException("用户名或密码错误");}

getAdminByUsername 负责根据用户名从数据库中查询出密码、角色、权限等。

     @Overridepublic Users getAdminByUsername(String username) {// QueryWrapper<Users> queryWrapper = new QueryWrapper<>();//user_login为数据库字段// queryWrapper.eq("user_login", username);//List<Users> usersList = baseMapper.selectList(queryWrapper);LambdaQueryWrapper<Users> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Users::getUserLogin , username);List<Users> usersList = baseMapper.selectList(queryWrapper);if (usersList != null && usersList.size() > 0) {return usersList.get(0);}// 用户名错误,提前抛出异常throw new UsernameNotFoundException("用户名错误");}

第三步,在 application.yml 中配置下不需要安全保护的资源路径:

secure:ignored:urls: #安全路径白名单- /doc.html- /swagger-ui/**- /swagger/**- /swagger-resources/**- /**/v3/api-docs- /**/*.js- /**/*.css- /**/*.png- /**/*.ico- /webjars/springfox-swagger-ui/**- /actuator/**- /druid/**- /users/login- /users/register- /users/info- /users/logout

第四步,在登录接口中添加登录和刷新 token 的方法:

@Controller
@Api(tags = "用户")
@RequestMapping("/users")
public class UsersController {@Autowiredprivate IUsersService usersService;@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.tokenHead}")private String tokenHead;@ApiOperation(value = "登录以后返回token")@RequestMapping(value = "/login", method = RequestMethod.POST)@ResponseBodypublic ResultObject login(@Validated UsersLoginParam users, BindingResult result) {String token = usersService.login(users.getUserLogin(), users.getUserPass());if (token == null) {return ResultObject.validateFailed("用户名或密码错误");}// 将 JWT 传递回客户端Map<String, String> tokenMap = new HashMap<>();tokenMap.put("token", token);tokenMap.put("tokenHead", tokenHead);return ResultObject.success(tokenMap);}@ApiOperation(value = "刷新token")@RequestMapping(value = "/refreshToken", method = RequestMethod.GET)@ResponseBodypublic ResultObject refreshToken(HttpServletRequest request) {String token = request.getHeader(tokenHeader);String refreshToken = usersService.refreshToken(token);if (refreshToken == null) {return ResultObject.failed("token已经过期!");}Map<String, String> tokenMap = new HashMap<>();tokenMap.put("token", refreshToken);tokenMap.put("tokenHead", tokenHead);return ResultObject.success(tokenMap);}
}

使用 Apipost 来测试一下,首先是文章获取接口,在没有登录的情况下会提示暂未登录或者 token 已过期。

四、实现原理

仅用四步就实现了登录认证,主要是因为将 SpringSecurity+JWT 的代码封装成了通用模块,我们来看看 codingmore-security 的目录结构。

codingmore-security
├── component
|    ├── JwtAuthenticationTokenFilter -- JWT登录授权过滤器
|    ├── RestAuthenticationEntryPoint
|    └── RestfulAccessDeniedHandler
├── config
|    ├── IgnoreUrlsConfig
|    └── SecurityConfig
└── util└── JwtTokenUtil -- JWT的token处理工具类

JwtAuthenticationTokenFilter 和 JwtTokenUtil 补充一下,

客户端的请求头里携带了 token,服务端肯定是需要针对每次请求解析校验 token 的,所以必须得定义一个过滤器,也就是 JwtAuthenticationTokenFilter:

  • 从请求头中获取 token
  • 对 token 进行解析、验签、校验过期时间
  • 校验成功,将验证结果放到 ThreadLocal 中,供下次请求使用

重点来看其他四个类。第一个 RestAuthenticationEntryPoint(自定义返回结果:未登录或登录过期):

public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setHeader("Access-Control-Allow-Origin", "*");response.setHeader("Cache-Control","no-cache");response.setCharacterEncoding("UTF-8");response.setContentType("application/json");response.getWriter().println(JSONUtil.parse(ResultObject.unauthorized(authException.getMessage())));response.getWriter().flush();}
}

可以通过 debug 的方式看一下返回的信息正是之前用户未登录状态下访问文章页的错误信息。

具体的信息是在 ResultCode 类中定义的。

public enum ResultCode implements IErrorCode {SUCCESS(0, "操作成功"),FAILED(500, "操作失败"),VALIDATE_FAILED(506, "参数检验失败"),UNAUTHORIZED(401, "暂未登录或token已经过期"),FORBIDDEN(403, "没有相关权限");private long code;private String message;private ResultCode(long code, String message) {this.code = code;this.message = message;}
}

第二个 RestfulAccessDeniedHandler(自定义返回结果:没有权限访问时):

public class RestfulAccessDeniedHandler implements AccessDeniedHandler{@Overridepublic void handle(HttpServletRequest request,HttpServletResponse response,AccessDeniedException e) throws IOException, ServletException {response.setHeader("Access-Control-Allow-Origin", "*");response.setHeader("Cache-Control","no-cache");response.setCharacterEncoding("UTF-8");response.setContentType("application/json");response.getWriter().println(JSONUtil.parse(ResultObject.forbidden(e.getMessage())));response.getWriter().flush();}
}

第三个IgnoreUrlsConfig(用于配置不需要安全保护的资源路径):

@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {private List<String> urls = new ArrayList<>();
}

通过 lombok 注解的方式直接将配置文件中不需要权限校验的路径放开,比如说 Knife4j 的接口文档页面。如果不放开的话,就被 SpringSecurity 拦截了,没办法访问到了。

第四个SecurityConfig(SpringSecurity通用配置):

public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowired(required = false)private DynamicSecurityService dynamicSecurityService;@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();//不需要保护的资源路径允许访问for (String url : ignoreUrlsConfig().getUrls()) {registry.antMatchers(url).permitAll();}// 任何请求需要身份认证registry.and().authorizeRequests().anyRequest().authenticated()// 关闭跨站请求防护及不使用session.and().csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)// 自定义权限拒绝处理类.and().exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler()).authenticationEntryPoint(restAuthenticationEntryPoint())// 自定义权限拦截器JWT过滤器.and().addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);//有动态权限配置时添加动态权限校验过滤器if(dynamicSecurityService!=null){registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);}}
}

这个类的主要作用就是告诉 SpringSecurity 那些路径不需要拦截,除此之外的,都要进行 RestfulAccessDeniedHandler(登录校验)、RestAuthenticationEntryPoint(权限校验)和 JwtAuthenticationTokenFilter(JWT 过滤)。

并且将 JwtAuthenticationTokenFilter 过滤器添加到 UsernamePasswordAuthenticationFilter 过滤器之前。

五、测试

第一步,测试登录接口,Apipost 直接访问 http://localhost:9002/users/login,可以看到 token 正常返回。

第二步,不带 token 直接访问文章接口,可以看到进入了 RestAuthenticationEntryPoint 这个处理器

第三步,携带 token,这次我们改用 Knife4j 来测试,发现可以正常访问:

相关文章:

整合SpringSecurity+JWT实现登录认证

一、关于 SpringSecurity 在 Spring Boot 出现之前&#xff0c;SpringSecurity 的使用场景是被另外一个安全管理框架 Shiro 牢牢霸占的&#xff0c;因为相对于 SpringSecurity 来说&#xff0c;SSM 中整合 Shiro 更加轻量级。Spring Boot 出现后&#xff0c;使这一情况情况大有…...

C# Onnx Yolov9 Detect 物体检测

目录 介绍 效果 项目 模型信息 代码 下载 C# Onnx Yolov9 Detect 物体检测 介绍 yolov9 github地址&#xff1a;https://github.com/WongKinYiu/yolov9 Implementation of paper - YOLOv9: Learning What You Want to Learn Using Programmable Gradient Information…...

Flink SQL 基于Update流出现空值无法过滤问题

问题背景 问题描述 基于Flink-CDC &#xff0c;Flink SQL的实时计算作业在运行一段时间后&#xff0c;突然发现插入数据库的计算结果发生部分主键属性发生失败&#xff0c;导致后续计算结果无法插入&#xff0c; 超过失败次数失败的情况问题报错 Caused by: java.sql.BatchUp…...

git-怎样把连续的多个commit合并成一个?

Git怎样把连续的多个commit合并成一个&#xff1f; Git怎样把连续的多个commit合并成一个&#xff1f; 参考URL: https://www.jianshu.com/p/5b4054b5b29e 查看git日志 git log --graph比如下图的commit 历史&#xff0c;想要把bai “Second change” 和 “Third change” 这…...

2024年2月游戏手柄线上电商(京东天猫淘宝)综合热销排行榜

鲸参谋监测的线上电商&#xff08;京东天猫淘宝&#xff09;游戏手柄品牌销售数据已出炉&#xff01;2月游戏手柄销售数据呈现出强劲的增长势头。 根据鲸参谋数据显示&#xff0c;今年2月游戏手柄月销售量累计约43万件&#xff0c;同比去年上涨了78%&#xff1b;销售额累计达1…...

Sass5分钟速通基础语法

前言 近来在项目中使用sass,想着学习一下,但官方写的教程太冗杂,所以就有了本文速通Sass的基础语法 Sass 是 CSS 的一种预编译语言。它提供了 变量&#xff08;variables&#xff09;、嵌套规则&#xff08;nested rules&#xff09;、 混合&#xff08;mixins&#xff09; 等…...

百度蜘蛛池平台在线发外链-原理以及搭建教程

蜘蛛池平台是一款非常实用的SEO优化工具&#xff0c;它可以帮助网站管理员提高网站的排名和流量。百度蜘蛛池原理是基于百度搜索引擎的搜索算法&#xff0c;通过对网页的内容、结构、链接等方面进行分析和评估&#xff0c;从而判断网页的质量和重要性&#xff0c;从而对网页进行…...

Android_ android使用原生蓝牙协议_连接设备以后,给设备发送指令触发数据传输---Android原生开发工作笔记167

之前通过蓝牙连接设备的时候,直接就是连接上蓝牙以后,设备会自动发送数据,有数据的时候,会自动发送,但是,有一个设备就不会,奇怪了很久,设备启动了也连接上了,但是就是没有数据过来. 是因为,这个设备有几种模式是握力球,在设备连接到蓝牙以后,需要,给设备通过蓝牙发送一个指令…...

【Java面试题】操作系统

文章目录 1.进程/线程/协程1.1辨别进程和线程的异同1.2优缺点1.2.1进程1.2.2线程 1.3进程/线程之间通信的方法1.3.1进程之间通信的方法1.3.2线程之间通信的方法 1.4什么是线程上下文切换1.5协程1.5.1协程的定义&#xff1f;1.5.2使用协程的原因&#xff1f;1.5.3协程的优缺点&a…...

SQLite数据库成为内存中数据库(三)

返回&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLite使用的临时文件&#xff08;二&#xff09; 下一篇&#xff1a;SQLite中的原子提交&#xff08;四) ​​ SQLite数据库通常存储在单个普通磁盘中文件。但是&#xff0c;在某些情况下&#xff0c;数据库可能…...

多张图片怎么合成一张gif?快来试试这个方法

将多张图片合成一张gif动图是现在常见的图像处理的方式&#xff0c;适合制作一些简单的动态图片。通过使用在线图片合成网站制作的gif动图不仅体积小画面丰富&#xff0c;画质还很清晰。不需要下载任何软件小白也能轻松上手&#xff0c;支持上传jpg、png以及gif格式图片&#x…...

爬取b站音频和视频数据,未合成一个视频

一、首先找到含有音频和视频的url地址 打开一个视频&#xff0c;刷新后&#xff0c;找到这个包&#xff0c;里面有我们所需要的数据 访问这个数据包后&#xff0c;获取字符串数据&#xff0c;用正则提取&#xff0c;再转为json字符串方便提取。 二、获得标题和音频数据后&…...

mysql进阶知识总结

1.存储引擎 1.1MySQL体系结构 1).连接层 最上层是一些客户端和链接服务&#xff0c;包含本地sock通信和大多数基于客户端/服务端工具实现的类似于TCP/IP的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念&#xff0c;为通过认证…...

量化交易入门(二十五)什么是RSI,原理和炒股实操

前面我们了解了KDJ&#xff0c;MACD&#xff0c;MTM三个技术指标&#xff0c;也进行了回测&#xff0c;结果有好有坏&#xff0c;今天我们来学习第四个指标RSI。RSI指标全称是相对强弱指标(Relative Strength Index),是通过比较一段时期内的平均收盘涨数和平均收盘跌数来分析市…...

快速上手Spring Cloud 九:服务间通信与消息队列

快速上手Spring Cloud 一&#xff1a;Spring Cloud 简介 快速上手Spring Cloud 二&#xff1a;核心组件解析 快速上手Spring Cloud 三&#xff1a;API网关深入探索与实战应用 快速上手Spring Cloud 四&#xff1a;微服务治理与安全 快速上手Spring Cloud 五&#xff1a;Spring …...

python——遍历网卡并禁用/启用

一、遍历网卡 注意&#xff1a;只能遍历到启用状态的网卡&#xff0c;如果网卡是禁止状态&#xff0c;则遍历不到&#xff01;&#xff01;&#xff01; import os import time import psutil import loggingdef get_multi_physical_network_card():physical_nic_list []try:…...

初识 51

keil的使用: 具体细节请移步我上一篇博客:创建第一个51文件-CSDN博客 hex -- 汇编语言实现的文件 -- 直接与单片机对接的文件 单片机 -- 一个集成电脑芯片 单片机开发版 -- 基于单片机的集成电路 stc 89 c52RC / RD 系列单片机 命名规则: 89 -- 版本号&#xff1f; C --…...

【回溯与邻里交换】纸牌三角

1.回溯算法 旋转有3种可能&#xff0c;镜像有2种 所以最后次数&#xff1a;counts/3/2 #include<iostream> using namespace std;int num[9]; int counts0; bool bools[9];//默认为false int dfs(int step){if(step9){//索引 if((num[0]num[1]num[2]num[3]num[3]num[4]n…...

微服务(基础篇-004-Feign)

目录 http客户端Feign Feign替代RestTemplate&#xff08;1&#xff09; Feign的介绍&#xff08;1.1&#xff09; 使用Feign的步骤&#xff08;1.2&#xff09; 自定义配置&#xff08;2&#xff09; 配置Feign日志的两种方式&#xff08;2.1&#xff09; Feign使用优化…...

Linux IRC

目录 入侵框架检测 检测流程图 账号安全 查找账号中的危险信息 查看保存的历史命令 检测异常端口 入侵框架检测 1、系统安全检查&#xff08;进程、开放端口、连接、日志&#xff09; 这一块是目前个人该脚本所实现的功能 2、Rootkit 建议使用rootkit专杀工具来检查&#…...

生成xcframework

打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式&#xff0c;可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

ServerTrust 并非唯一

NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...

select、poll、epoll 与 Reactor 模式

在高并发网络编程领域&#xff0c;高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表&#xff0c;以及基于它们实现的 Reactor 模式&#xff0c;为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。​ 一、I…...

智能AI电话机器人系统的识别能力现状与发展水平

一、引言 随着人工智能技术的飞速发展&#xff0c;AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术&#xff0c;在客户服务、营销推广、信息查询等领域发挥着越来越重要…...

Java求职者面试指南:计算机基础与源码原理深度解析

Java求职者面试指南&#xff1a;计算机基础与源码原理深度解析 第一轮提问&#xff1a;基础概念问题 1. 请解释什么是进程和线程的区别&#xff1f; 面试官&#xff1a;进程是程序的一次执行过程&#xff0c;是系统进行资源分配和调度的基本单位&#xff1b;而线程是进程中的…...

2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...

JavaScript 数据类型详解

JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型&#xff08;Primitive&#xff09; 和 对象类型&#xff08;Object&#xff09; 两大类&#xff0c;共 8 种&#xff08;ES11&#xff09;&#xff1a; 一、原始类型&#xff08;7种&#xff09; 1. undefined 定…...

springboot 日志类切面,接口成功记录日志,失败不记录

springboot 日志类切面&#xff0c;接口成功记录日志&#xff0c;失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...

云原生周刊:k0s 成为 CNCF 沙箱项目

开源项目推荐 HAMi HAMi&#xff08;原名 k8s‑vGPU‑scheduler&#xff09;是一款 CNCF Sandbox 级别的开源 K8s 中间件&#xff0c;通过虚拟化 GPU/NPU 等异构设备并支持内存、计算核心时间片隔离及共享调度&#xff0c;为容器提供统一接口&#xff0c;实现细粒度资源配额…...

【iOS】 Block再学习

iOS Block再学习 文章目录 iOS Block再学习前言Block的三种类型__ NSGlobalBlock____ NSMallocBlock____ NSStackBlock__小结 Block底层分析Block的结构捕获自由变量捕获全局(静态)变量捕获静态变量__block修饰符forwarding指针 Block的copy时机block作为函数返回值将block赋给…...