SpringBoot整合Spring-Security 认证篇(保姆级教程)

本文项目基于以下教程的代码版本: https://javaxbfs.blog.csdn.net/article/details/135195636

代码仓库: https://gitee.com/skyblue0678/springboot-demo

为了跟shiro区别开,新建了一个分支:

目录

🌹1、友善问候一下 Spring Security

⭐2、 POM依赖

🌹3、登录

⭐4、根据账号从DB中获取用户实体

🌹5、校验密码是否正确

🌹6、全局异常返回

⭐7、测试

🌹8、细说spring security


🌹1、友善问候一下 Spring Security

Spring Security是Spring家族中的安全框架,可以用来做用户验证和权限管理等。Spring Security是一款重型框架,不过功能十分强大。

一般来说,如果项目中需要进行权限管理,具有多个角色和多种权限,我们可以使用Spring Security。如果是较为简单的项目,只需要控制一下某些接口只有登录后才能访问,则可以使用Shiro框架,Shiro也是一款安全框架,它是一款轻量级框架,功能没有Spring Security多,但使用起来要简单不少。

也就是说,Spring Security的颗粒度更细。

SpringSecurity 采用的是责任链的设计模式,是一堆过滤器链的组合,它有一条很长的过滤器链。

不过我们不需要去仔细了解每一个过滤器的含义和用法,只需要搞定以下几个问题即可:怎么登录、怎么校验账户、认证失败处理。

2、 POM依赖

没啥好说的,maven导入即可。

<!--springSecurity-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>

不写版本号,默认就会下载最新的版本。

3、登录

不管你用哪种权限框架,第一个要解决的问题就是登录。就是在我们的登录接口中,将账户密码委托给权限框架接管,让权限框架帮我们做校验和权限认证。

新建一个登录方法

@GetMapping("/security/login")
public String securityLogin(String username,String password){UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username,password);// 使用authenticationManager调用loadUserByUsername获取数据库中的用户信息,Authentication authentication = authenticationManager.authenticate(authToken);if(authentication == null) {throw new RuntimeException("Login false");}//获取符合security规范的UserSecurityUser securityUser = (SecurityUser) authentication.getPrincipal();String token = jwtUtil.createToken(securityUser.getUser());return token;
}

这个方法的目的是验证用户的用户名和密码,并在验证成功后为该用户生成一个JWT。 UsernamePasswordAuthenticationToken就是我们委托框架帮我们托管的登录凭证,shiro框架也有类似的东西。 然后是:

Authentication authentication = authenticationManager.authenticate(authToken);

这就是spring security帮我们执行认证和授权的方法,最终返回一个认证结果。大家思考一下,我们正常登录的逻辑无非是四步走:

  1. 输入账号密码

  2. 根据账号从DB中获取用户实体

  3. 校验密码是否正确

  4. 校验成功,将用户生成token后返回

我们再回过来看这段代码,第2步和第3步没见到,只见到spring security帮我们做了。但是,这并不代表我们可以省略这两步,只是需要我们写在别的地方,仅此而已。

4、根据账号从DB中获取用户实体

这个步骤是不可能不写的,只是写到了别处。 先说个事儿哈,spring security中的用户概念,有自己的一套规则,不能直接用我们系统里面的User类。

如果我们要用spring security,就得实现他的用户接口:UserDetails。 所以,我们新建一个SecurityUser类:

// 使用Lombok库的@Data注解,自动生成getter、setter、equals、hashCode和toString方法
// 同样使用@NoArgsConstructor注解,自动生成无参构造函数
@Data
@NoArgsConstructor
public class SecurityUser implements UserDetails {// 使用聚合模式,将我们自己的User对象聚合到SecurityUser中// user字段存储了用户的一些信息,例如用户名和密码等private User user;// 覆盖UserDetails接口中的getAuthorities方法,返回用户的权限集合// 这里返回null,表示未实现获取权限的逻辑@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}// 覆盖UserDetails接口中的getPassword方法,返回用户的密码// 这里通过调用user对象的getPwd方法获取密码@Overridepublic String getPassword() {return user.getPwd();}// 覆盖UserDetails接口中的getUsername方法,返回用户的用户名// 这里通过调用user对象的getUserName方法获取用户名,并注释说明有的地方可能会用email作为用户名,这里还是使用userName@Overridepublic String getUsername() {// 用户名:有的地方可能会用email作为用户名,我们这还是userNamereturn user.getUserName();}// 覆盖UserDetails接口中的isAccountNonExpired方法,判断账户是否过期// 这里直接返回true,表示账户不过期@Overridepublic boolean isAccountNonExpired() {return true;}// 覆盖UserDetails接口中的isAccountNonLocked方法,判断账户是否被锁定// 这里直接返回true,表示账户未被锁定@Overridepublic boolean isAccountNonLocked() {return true;}// 覆盖UserDetails接口中的isCredentialsNonExpired方法,判断凭证是否过期// 这里直接返回true,表示凭证不过期@Overridepublic boolean isCredentialsNonExpired() {return true;}// 覆盖UserDetails接口中的isEnabled方法,判断用户是否启用// 这里直接返回true,表示用户启用状态为true@Overridepublic boolean isEnabled() {return true;}
}

我们系统里面有自己的user了,但是为了适配,所以就聚合进来。

然后是如何查询DB呢,是不是得有个Service去查询,我们依据有自己的UserService了,但是很可惜,spring security有自己的规范,我们自己写的user Service,他不认,气死偶了。

没办法,重新写个Service,我们自己写都写了,也不能不管对不对?嗯,那还是聚合进来。

@Service
@Slf4j
public class UserDetailService implements UserDetailsService {@ResourceUserService userService;/*** 根据用户名直接从DB中查询用户数据,作为登录校验的依据* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userService.getByUsername(username);if (user == null) {log.info("username not found");throw new UsernameNotFoundException("username not found");}SecurityUser securityUser = new SecurityUser();securityUser.setUser(user);return securityUser;}}

核心逻辑就是,我们还是用之前的方法拿到User,但为了适配,就塞到SecurityUser里面去。

UserDetailsService是spring security认可的接口,我们得实现这个接口,并且实现loadUserByUsername方法,这个方法在spring security的认证逻辑里面会用到。目的就是拿到DB中真实的User,跟我们登录的账号密码进行比对。

5、校验密码是否正确

为什么会有这一步呢,因为很多时候我们的密码是要进行加密的,但是我们登录肯定传的是明文密码,所以会需要转换后再去比对,否则肯定是校验失败了。

这个校验密码的逻辑,需要写在spring security的配置类中。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@ResourceUserDetailService userService;/*** 新增security账户* @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {@Overridepublic String encode(CharSequence rawPassword) {return rawPassword.toString();}@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return rawPassword.equals(encodedPassword);}});}}

因为我们的项目并没有对密码加密,所以就直接比较了。

在LoginController中,我们用到了AuthenticationManager这个对象,需要在配置类中注册。

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {// 身份验证管理器, 直接继承即可.return super.authenticationManagerBean();
}

AuthenticationManager是Spring Security框架中的一个核心接口,它负责处理身份验证请求。在认证过程中,用户提交身份验证信息(如用户名和密码),AuthenticationManager会验证这些信息的有效性。

最后是路由的相关配置

@Override  protected void configure(HttpSecurity http) throws Exception {  http  // 禁用跨站请求伪造保护  .csrf().disable()   // 设置会话管理策略为无会话,因为我们使用token作为信息传递介质  .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)   .and()  // 进行认证请求的配置  .authorizeRequests()   // 将所有登入和注册的接口放开,这些都是无需认证就访问的  .antMatchers("/security/login").anonymous()   // 除了上面的那些,剩下的任何接口请求都需要经过认证  .anyRequest().authenticated()   .and()  // 允许跨域请求  .cors()   ;  // 在UsernamePasswordAuthenticationFilter之前添加JWT认证过滤器  http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);  }

这段代码是Spring Security框架中用于配置HTTP安全的部分。它主要涉及到跨站请求伪造(CSRF)的禁用、会话管理策略的设置、认证请求的配置以及跨域请求的允许。同时,还添加了一个JWT(JSON Web Token)认证过滤器。

因为我们项目用到了jwt,所以在进行账号密码验证之前,要先走jwt的过滤器。

jwt过滤器代码如下:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@ResourceJWTUtil jwtUtil;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {//获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {//token为空的话, 就不管它, 让SpringSecurity中的其他过滤器处理请求//请求放行filterChain.doFilter(request, response);return;}//token不为空时, 解析tokenUser user = null;try {user = jwtUtil.verify(token);} catch (Exception e) {// 过滤器中抛出的异常,无法被统一异常捕获,所以在这里直接返回e.printStackTrace();Result result = new Result();result.setCode(403);result.setMsg("Token无效:" + e.getMessage());WebUtils.response(response,result);return;}SecurityUser securityUser = new SecurityUser();securityUser.setUser(user);//将用户安全信息存入SecurityContextHolder, 在之后SpringSecurity的过滤器就不会拦截UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(securityUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}

流程可以参考这个图:

如果校验成功了,那么在登录方法中就会往下走,生成token返回,结束。

6、全局异常返回

认证过程中,难免出现各种异常,我们一般会做一个通用的返回,直接上代码,没啥好说的,网上一大堆

Result

Data
public class Result implements Serializable {private int code;private String msg;private Object data;public static Result succ(Object data) {return success(200, "操作成功", data);}public static Result error(String msg) {return error(400, msg, null);}public static Result success (int code, String msg, Object data) {Result result = new Result();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}public static Result error (int code, String msg, Object data) {Result result = new Result();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}
}

GlobalExceptionHandler

/*** 全局异常处理*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 400 错误:运行时异常* @param e* @return*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(value = RuntimeException.class)public Result handler(RuntimeException e) {log.error("运行时异常:----------------{}", e.getMessage());return Result.error(e.getMessage());}/*** 403 错误:权限不足* @param e* @return*/@ResponseStatus(HttpStatus.FORBIDDEN)@ExceptionHandler(value = AccessDeniedException.class)public Result handler(AccessDeniedException e) {log.info("security权限不足:----------------{}", e.getMessage());return Result.error("权限不足");}/*** 400 错误:异常请求-方法参数不匹配* @param e* @return*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(value = MethodArgumentNotValidException.class)public Result handler(MethodArgumentNotValidException e) {log.info("实体校验异常:----------------{}", e.getMessage());BindingResult bindingResult = e.getBindingResult();ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();return Result.error(objectError.getDefaultMessage());}/*** 400 错误:异常请求-非法参数* @param e* @return*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(value = IllegalArgumentException.class)public Result handler(IllegalArgumentException e) {log.error("Assert异常:----------------{}", e.getMessage());return Result.error(e.getMessage());}}

7、测试

localhost:8080/security/login?username=jack&password=1

将token放到header,发送 localhost:8080

能正常返回就OK了。

如果token过期,就会报:

{"msg": "Token无效:The Token has expired on Tue Dec 26 15:43:42 CST 2023.","code": 403
}

8、细说spring security

Spring Security 并非一个新生的事物,它最早不叫 Spring Security ,叫 Acegi Security,叫 Acegi Security 并不是说它和 Spring 就没有关系了,它依然是为 Spring 框架提供安全支持的。事实上,Java 领域的框架,很少有框架能够脱离 Spring 框架独立存在。

当 Spring Security 还叫 Acegi Security 的时候,虽然功能也还可以,但是实际上这个东西并没有广泛流行开来。最重要的原因就是它的配置太过于繁琐,当时网上流传一句话:“每当有人要使用 Acegi Security,就会有一个精灵死去。” 足见 Acegi Security 的配置是多么可怕。直到今天,当人们谈起 Spring Security 的时候,依然在吐槽它的配置繁琐。

后来 Acegi Security 投入 Spring 的怀抱,改名叫 Spring Security,事情才慢慢开始发生变化。新的开发团队一直在尽力简化 Spring Security 的配置,Spring Security 的配置相比 Acegi Security 确实简化了很多。但是在最初的几年里,Spring Security 依然无法得到广泛的使用。

直到有一天 Spring Boot 像谜一般出现在江湖边缘,彻底颠覆了 JavaEE 的世界。一人得道鸡犬升天,自从 Spring Boot 火了之后,Spring 家族的产品都被带了一把,Spring Security 就是受益者之一,从此飞上枝头变凤凰。

Spring Boot/Spring Cloud 现在作为 Java 开发领域最最主流的技术栈,这一点大家应该都没有什么异议,而在 Spring Boot/Spring Cloud 中做安全管理,Spring Security 无疑是最方便的。

你想保护 Spring Boot 中的接口,添加一个 Spring Security 的依赖即可,事情就搞定了,所有接口就保护起来了,甚至不需要一行配置。

所以说,因为 Spring Boot/Spring Cloud 火爆,让 Spring Security 跟着沾了一把光。

「有的人觉得 Spring Security 配置臃肿。」

如果是 SSM + Spring Security 的话,我觉得这话有一定道理。

但是如果是 Spring Boot 项目的话,其实并不见得臃肿。Spring Boot 中,通过自动化配置 starter 已经极大的简化了 Spring Security 的配置,我们只需要做少量的定制的就可以实现认证和授权了。

「有人觉得 Spring Security 中概念复杂。」这个是这样的,没错。

Spring Security 由于功能比较多,支持 OAuth2 等原因,就显得比较重量级,不像 Shiro 那样轻便。

但是如果换一个角度,你可能会有不一样的感受。

在 Spring Security 中你会学习到许多安全管理相关的概念,以及常见的安全攻击。这些安全攻击,如果你不是 web 安全方面的专家,很多可能存在的 web 攻击和漏洞你可能很难想到,而 Spring Security 则把这些安全问题都给我们罗列出来并且给出了相应的解决方案。

所以我说,我们学习 Spring Security 的过程,也是在学习 web 安全,各种各样的安全攻击、各种各样的登录方式、各种各样你能想到或者想不到的安全问题,Spring Security 都给我们罗列出来了,并且给出了解决方案,从这个角度来看,你会发现 Spring Security 好像也不是那么让人讨厌。

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

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

相关文章

【高性能篇】QPS概念、RT概念

什么是QPS&#xff0c;什么是RT&#xff1f; ✔️典型解析✔️扩展知识仓✔️RT ✔️QPS✔️ QPS和TPS✔️并发用户数✔️最佳线程数 ✔️典型解析 QPS&#xff0c;指的是系统每秒能处理的请求数(Query Per Second)&#xff0c;在Web应用中我们更关注的是Web应用每秒能处理的re…

Arduino驱动VL6180X光学测距传感器(OLED显示)

Arduino驱动VL6180X光学测距传感器&#xff08;OLED显示&#xff09; 简介原理模块参数接线图代码结果 简介 VL6108X三合一光电模块&#xff0c;芯片内集成了IR VSEL(vertical-cavity surface-emitting laser)红外垂直腔面发射激光器光源、接近传感器、环境光传感器&#xff0…

node fs模块读取文件 readFile、readFileSync、fsPromises.readFile、createReadStream

文章目录 1.读取文件1.1 readFile1.2 readFileSync1.3 fsPromises.readFile&#xff1a;promise的写法1.4 fs.createReadStream 1.读取文件 readFile&#xff1a;异步读取文件readFileSync&#xff1a;同步读取文件fsPromises.readFile&#xff1a;promise的写法 需要注意的是…

C语言rand函数,srand函数,time函数实现随机数,及猜数字小游戏

怀心之所爱&#xff0c;奔赴山河 前言 最近在复习c的知识&#xff0c;想起之前写过一个猜数字小游戏&#xff0c;所以今天就把自己关于随机数的使用经验分享一下&#xff0c;希望对大家有帮助。 一.rand函数 1.函数的声明如下 可以看到&#xff0c;返回值是int类型&#xff…

【黑产攻防道04】利用pow工作量证明降低黑产的破解效率

上一期我们提到&#xff0c;黑产有三种常见的破解方式&#xff1a; 1.通过识别出验证码图片答案实现批量破解验证&#xff0c;即图片答案识别&#xff1b; 2.在了解通讯流程之后直接携带相关参数发请求&#xff0c;即协议破解&#xff1b; 3.使用各种客户端模拟器来模拟真人…

分享72个Python爬虫源码总有一个是你想要的

分享72个Python爬虫源码总有一个是你想要的 学习知识费力气&#xff0c;收集整理更不易。 知识付费甚欢喜&#xff0c;为咱码农谋福利。 链接&#xff1a;https://pan.baidu.com/s/1v2P4l5R6KT2Ul-oe2SF8cw?pwd6666 提取码&#xff1a;6666 项目名称 10 photo websites…

盘点2023 | 校企合作结硕果,产教融合谱新篇

回首2023&#xff0c;电巢科技与众多高校建立了紧密的合作关系&#xff0c;以实习就业为导向&#xff0c;帮助学生打开技术和产业视野&#xff0c;提前做好职业发展规划&#xff0c;按照电子行业的企业用人标准&#xff0c;帮助高校进行“人才前置化”培养&#xff0c;并且持续…

如何使用ArcGIS Pro自动矢量化建筑

相信你在使用ArcGIS Pro的时候已经发现了一个问题&#xff0c;那就是ArcGIS Pro没有ArcScan&#xff0c;在ArcGIS Pro中&#xff0c;Esri确实已经移除了ArcScan&#xff0c;没有了ArcScan我们如何自动矢量化地图&#xff0c;从地图中提取建筑等要素呢&#xff0c;这里为大家介绍…

一文详解Cookie以及Selenium自动获取Cookie

前言 以后数据获取途径以及数据资产绝对会是未来核心要素生产工具和资源之一&#xff0c;每个大模型都离不开更加精细化数据的二次喂养训练。不过现在来看收集大量数据的方法还是有很多途径的&#xff0c;有些垂直领域的专业数据是很难获取得到的&#xff0c;靠人力去搜寻相当…

【滑动窗口】【二分查找】C++算法:和至少为 K 的最短子数组

作者推荐 动态规划 多源路径 字典树 LeetCode2977:转换字符串的最小成本 本题涉及知识点 滑动窗口 有序向量 二分查找 LeetCode862:和至少为 K 的最短子数组 给你一个整数数组 nums 和一个整数 k &#xff0c;找出 nums 中和至少为 k 的 最短非空子数组 &#xff0c;并返回…

安装 PyQt5 保姆级教程

作者&#xff1a;billy 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 前言 博主之前做应用层开发用的一直是 Qt&#xff0c;这次尝试一下在 python 中使用 Pyqt5 模块来开发 UI 界面&#xff0c;这里做一些…

[电磁学]大学物理陈秉乾老师课程笔记

主页有博主其他上万字的精品笔记,都在不断完善ing~ 第一讲 绪论,库仑定律 主要讲解了电磁学中的库伦定律和电场的相关概念&#xff0c;介绍了电荷和电磁相互作用的规律&#xff0c;并讲解了电场强度和电势的概念。 03:14 &#x1f393; 库伦定律&#xff1a;电势能与电荷的关…