整合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 出现之前,SpringSecurity 的使用场景是被另外一个安全管理框架 Shiro 牢牢霸占的,因为相对于 SpringSecurity 来说,SSM 中整合 Shiro 更加轻量级。Spring Boot 出现后,使这一情况情况大有…...
C# Onnx Yolov9 Detect 物体检测
目录 介绍 效果 项目 模型信息 代码 下载 C# Onnx Yolov9 Detect 物体检测 介绍 yolov9 github地址:https://github.com/WongKinYiu/yolov9 Implementation of paper - YOLOv9: Learning What You Want to Learn Using Programmable Gradient Information…...
Flink SQL 基于Update流出现空值无法过滤问题
问题背景 问题描述 基于Flink-CDC ,Flink SQL的实时计算作业在运行一段时间后,突然发现插入数据库的计算结果发生部分主键属性发生失败,导致后续计算结果无法插入, 超过失败次数失败的情况问题报错 Caused by: java.sql.BatchUp…...
git-怎样把连续的多个commit合并成一个?
Git怎样把连续的多个commit合并成一个? Git怎样把连续的多个commit合并成一个? 参考URL: https://www.jianshu.com/p/5b4054b5b29e 查看git日志 git log --graph比如下图的commit 历史,想要把bai “Second change” 和 “Third change” 这…...
2024年2月游戏手柄线上电商(京东天猫淘宝)综合热销排行榜
鲸参谋监测的线上电商(京东天猫淘宝)游戏手柄品牌销售数据已出炉!2月游戏手柄销售数据呈现出强劲的增长势头。 根据鲸参谋数据显示,今年2月游戏手柄月销售量累计约43万件,同比去年上涨了78%;销售额累计达1…...
Sass5分钟速通基础语法
前言 近来在项目中使用sass,想着学习一下,但官方写的教程太冗杂,所以就有了本文速通Sass的基础语法 Sass 是 CSS 的一种预编译语言。它提供了 变量(variables)、嵌套规则(nested rules)、 混合(mixins) 等…...
百度蜘蛛池平台在线发外链-原理以及搭建教程
蜘蛛池平台是一款非常实用的SEO优化工具,它可以帮助网站管理员提高网站的排名和流量。百度蜘蛛池原理是基于百度搜索引擎的搜索算法,通过对网页的内容、结构、链接等方面进行分析和评估,从而判断网页的质量和重要性,从而对网页进行…...
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协程的定义?1.5.2使用协程的原因?1.5.3协程的优缺点&a…...
SQLite数据库成为内存中数据库(三)
返回:SQLite—系列文章目录 上一篇:SQLite使用的临时文件(二) 下一篇:SQLite中的原子提交(四) SQLite数据库通常存储在单个普通磁盘中文件。但是,在某些情况下,数据库可能…...
多张图片怎么合成一张gif?快来试试这个方法
将多张图片合成一张gif动图是现在常见的图像处理的方式,适合制作一些简单的动态图片。通过使用在线图片合成网站制作的gif动图不仅体积小画面丰富,画质还很清晰。不需要下载任何软件小白也能轻松上手,支持上传jpg、png以及gif格式图片&#x…...
爬取b站音频和视频数据,未合成一个视频
一、首先找到含有音频和视频的url地址 打开一个视频,刷新后,找到这个包,里面有我们所需要的数据 访问这个数据包后,获取字符串数据,用正则提取,再转为json字符串方便提取。 二、获得标题和音频数据后&…...
mysql进阶知识总结
1.存储引擎 1.1MySQL体系结构 1).连接层 最上层是一些客户端和链接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于TCP/IP的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证…...
量化交易入门(二十五)什么是RSI,原理和炒股实操
前面我们了解了KDJ,MACD,MTM三个技术指标,也进行了回测,结果有好有坏,今天我们来学习第四个指标RSI。RSI指标全称是相对强弱指标(Relative Strength Index),是通过比较一段时期内的平均收盘涨数和平均收盘跌数来分析市…...
快速上手Spring Cloud 九:服务间通信与消息队列
快速上手Spring Cloud 一:Spring Cloud 简介 快速上手Spring Cloud 二:核心组件解析 快速上手Spring Cloud 三:API网关深入探索与实战应用 快速上手Spring Cloud 四:微服务治理与安全 快速上手Spring Cloud 五:Spring …...
python——遍历网卡并禁用/启用
一、遍历网卡 注意:只能遍历到启用状态的网卡,如果网卡是禁止状态,则遍历不到!!! 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 -- 版本号? C --…...
【回溯与邻里交换】纸牌三角
1.回溯算法 旋转有3种可能,镜像有2种 所以最后次数: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(1) Feign的介绍(1.1) 使用Feign的步骤(1.2) 自定义配置(2) 配置Feign日志的两种方式(2.1) Feign使用优化…...
Linux IRC
目录 入侵框架检测 检测流程图 账号安全 查找账号中的危险信息 查看保存的历史命令 检测异常端口 入侵框架检测 1、系统安全检查(进程、开放端口、连接、日志) 这一块是目前个人该脚本所实现的功能 2、Rootkit 建议使用rootkit专杀工具来检查&#…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...
从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...
visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...
【无标题】路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论
路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论 一、传统路径模型的根本缺陷 在经典正方形路径问题中(图1): mermaid graph LR A((A)) --- B((B)) B --- C((C)) C --- D((D)) D --- A A -.- C[无直接路径] B -…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...
C#学习第29天:表达式树(Expression Trees)
目录 什么是表达式树? 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持: 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...
作为测试我们应该关注redis哪些方面
1、功能测试 数据结构操作:验证字符串、列表、哈希、集合和有序的基本操作是否正确 持久化:测试aof和aof持久化机制,确保数据在开启后正确恢复。 事务:检查事务的原子性和回滚机制。 发布订阅:确保消息正确传递。 2、性…...
libfmt: 现代C++的格式化工具库介绍与酷炫功能
libfmt: 现代C的格式化工具库介绍与酷炫功能 libfmt 是一个开源的C格式化库,提供了高效、安全的文本格式化功能,是C20中引入的std::format的基础实现。它比传统的printf和iostream更安全、更灵活、性能更好。 基本介绍 主要特点 类型安全:…...

