整合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 来测试,发现可以正常访问:

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

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

相关文章

【编程笔记】学会使用 Git

目录 一、介绍 Git二、安装 Git三、 常用 linux 目录四、Git 的必要配置(1) 查看和删除之前的配置(2) 配置 Git 五、Git 基本理论六、Git 项目搭建七、Git 文件操作八、分支Git 笔记 ❀❀❀(1) 常规使用(2) 分支 一、介绍 Git &#x1f4d6; VCS&#xff1a;Version Control S…

当代深度学习模型介绍--卷积神经网络(CNNs)

AI大模型学习 方向一&#xff1a;AI大模型学习的理论基础 模型和应用的多样化&#xff1a;随着研究的深入&#xff0c;深度学习领域出现了多种创新的模型架构&#xff1a; 卷积神经网络&#xff08;CNNs&#xff09;专门针对图像处理任务进行了优化&#xff0c;通过模拟生物视…

关于WPARAM和LPARAM参数

从前&#xff0c;Windows 是 16 位的。每条message信息都可以携带两段数据&#xff0c;分别称为 WPARAM 和 LPARAM。在消息参数传递中对指针类型使用强制类型转换&#xff0c;这是一种常见用法。第一个参数是一个 16 位值&#xff08;"word"&#xff09;&#xff0c;…

如何过得更幸福?我推荐你读这5本书

快乐不等于幸福。快乐是一种短暂的体验&#xff0c;随着多巴胺的消退而迅速减退。快乐是有捷径的&#xff0c;那就是戏弄相关的神经回路。 幸福是有意义、有目的和积极的生活的持久体验。 今天&#xff0c;为大家推荐一份“幸福书单”。 01 《幸福的勇气》 岸见一郎、古贺史…

【jenkins+cmake+svn管理c++项目】jenkins回传文件到svn(windows)

书接上文&#xff1a;创建一个项目 在经过cmakemsbuild顺利生成动态库之后&#xff0c;考虑到我一个项目可能会生成多个动态库&#xff0c;它们分散在build内的不同文件夹&#xff0c;我希望能将它们收拢到一个文件夹下&#xff0c;并将其回传到svn。 一、动态库移位—cmake实…

45 对接海康视频九宫格的实现

前言 这里主要是 来看一下 海康视频 的一个九宫格播放的需求 然后 在实际使用的过程中产生了一些问题, 比如 增加一个视频, 应该只增量请求这一个视频的服务, 而一些实现下是全量请求了 整个视频列表的服务 另外 就是全屏播放, 如果是 自己写样式来实现 全屏播放, 可能需要 …

使用vagrant快速创建linux虚拟机

1. 技术解释 vagrant&#xff1a;是一款用于管理虚拟机的命令行使用软件&#xff0c;用Ruby语言开发而成&#xff0c;可以通过 Vagrant 封装一个 Linux 的开发环境&#xff0c;是一款用来构建虚拟开发环境的工具。使用vagrant可以省去你使用虚拟机创建操作系统的所有操作&…

场效应管(MOS管)知识点总结

目录 一、场效应管(FET)基础知识 1.名称 2.电路符号 3.分类 4.应用场景 5.厂商介绍 二、MOS管G、S、D以及判定 三、耗尽型场效应管工作原理 (耗尽型&#xff1a;depletion mode) 四、NMOS与PMOS的区别 (区别&#xff1a;difference) (多晶硅&#xff1a;polysilicon) …

zookeeper 监控 与 JVM 设置

一、通过JMX监控 JVM 默认情况下&#xff0c;在 ./bin/zkServer.sh 脚本中是默认开启了JMX监控的&#xff0c;并且是不需要认证的&#xff0c;详情可见ZOOMAIN变量&#xff1b; ZOOMAIN"-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port$JMXPORT -D…

vue内置组件、特殊元素、组件api详解

vue3移动端项目搭建&#xff0c;vue3vantviteaxiospiniasass完整版代码下载&#xff1a; https://download.csdn.net/download/randy521520/88820174 vue3移动端项目构建TS&#xff0c;vue3vantviteaxiospiniasasstypescript完整版代码下载&#xff1a; https://download.csdn.…

java中的多态以及对象的类型转换

多态 多态指的就是同一事物在不同情况下表现出来的不同状态或者行为。在java中&#xff0c;当调用同一个方法时&#xff0c;由于对象存在差异&#xff0c;不同的对象之间可能存在不同的行为&#xff0c;当父类引用指向了子类对象&#xff0c;用这个父类引用调用子类中重写的方法…

深度学习十大算法之Diffusion扩散模型

1. 引言 扩散模型在近年来成为了热门话题&#xff0c;其火速蹿红主要归功于在图像生成领域的突破应用。尤其是一些从文本到图像的生成技术&#xff0c;它们成功地运用了扩散模型来创建令人惊叹的逼真图像。如果你听说过某个应用能够迅速且高质量地生成图像&#xff0c;那么很可…

【效率提升】Xinstall免填邀请码,让App注册更便捷

在移动互联网时代&#xff0c;App已经成为我们生活中不可或缺的一部分。然而&#xff0c;每当我们要尝试一个新的App时&#xff0c;繁琐的注册流程总是让人感到头疼。其中&#xff0c;填写邀请码这一环节更是让许多人望而却步。幸运的是&#xff0c;Xinstall作为国内专业的App全…

每日OJ题_栈⑤_力扣946. 验证栈序列

目录 力扣946. 验证栈序列 解析代码 力扣946. 验证栈序列 946. 验证栈序列 难度 中等 给定 pushed 和 popped 两个序列&#xff0c;每个序列中的 值都不重复&#xff0c;只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时&#xff0c;返回 true&a…

数据链路层之信道:数字通信的桥梁与守护者

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

C#/BS手麻系统源码 手术麻醉管理系统源码 商业项目源码

C#/BS手麻系统源码 手术麻醉管理系统源码 商业项目源码 手麻系统从麻醉医生实际工作环境和流程需求方面设计&#xff0c;与HIS&#xff0c;LIS&#xff0c;PACS&#xff0c;EMR无缝连接&#xff0c;方便查看患者的信息;实现术前、术中、术后手术麻醉信息全记录;减少麻醉医师在…

.NET Core教程:深入实践与实例解析

.NET Core教程&#xff1a;深入实践与实例解析 随着技术的发展&#xff0c;.NET Core作为微软推出的一个开源、跨平台的.NET实现&#xff0c;已经越来越受到开发者的青睐。本文将详细讲解.NET Core的基础知识&#xff0c;并通过实例来解析其关键特性和使用方法。 一、.NET Co…

AI程序员Devin的开源替代Devika

前天介绍了 AI软件工程师 微软AI 程序员AutoDev&#xff0c;自主执行工程任务生成代码-CSDN博客 今天介绍下另一款Devin替代品&#xff0c;开源的Devika (另外还有一个开源的 OpenDevin : https://github.com/OpenDevin/OpenDevin) Devika https://github.com/stitionai/dev…

Redis在多节点上如何实现分布式锁?

在多个 Redis 节点上实现分布式锁可以通过以下步骤来完成&#xff1a; 选择分布式锁的实现方式&#xff1a;有多种实现分布式锁的方式&#xff0c;如使用 Redis 的普通锁、RedLock 等。RedLock 是一种基于多个 Redis 节点的分布式锁实现&#xff0c;它通过在多个节点上获取锁来…

awesome-cheatsheets:超级速查表 - 编程语言、框架和开发工具的速查表

awesome-cheatsheets&#xff1a;超级速查表 - 编程语言、框架和开发工具的速查表&#xff0c;单个文件包含一切你需要知道的东西 官网&#xff1a;GitHub - skywind3000/awesome-cheatsheets: 超级速查表 - 编程语言、框架和开发工具的速查表&#xff0c;单个文件包含一切你需…