SpringSecurity工作原理(一)

 实现功能就是继承这几个对应功能的类。

大概工作流程 

Spring Security 的过滤器(Filters)和拦截器(Interceptors)是 Spring Security 框架中用于保护 web 应用安全的重要组件。它们在处理 HTTP 请求时扮演不同的角色,并且相互配合以确保安全性。让我用一个更通俗易懂的方式来解释它们的作用和协作方式。

  1. 过滤器(Filters):

    • 角色类似于门卫:想象一下,你的应用是一栋大楼,而过滤器就像是大楼的门卫。每个进入大楼的人(HTTP 请求)都需要通过门卫的检查。
    • 工作在请求级别:过滤器操作在 Servlet 层,这意味着它们在请求到达你的 Spring Controllers 之前就开始工作了。
    • 责任:过滤器负责一些基本的任务,比如身份验证(检查你是谁),授权(检查你能做什么),以及其他安全检查(比如检查是否有恶意软件)。
  2. 拦截器(Interceptors):

    • 角色类似于办公室的前台:如果过滤器让请求进入了这栋大楼,那么拦截器就像是某个办公室的前台。它们在请求处理的更深层次上起作用。
    • 工作在 Controller 处理请求之前和之后:这意味着拦截器可以在你的 Controller 开始处理请求之前以及处理完毕后进行额外的操作。
    • 责任:拦截器通常用于更细粒度的控制,如处理日志记录、事务管理、处理请求数据等。
  3. 他们如何协作:

    • 当一个 HTTP 请求到达你的应用时,它首先会被过滤器处理。过滤器检查请求的安全性,比如用户是否已经登录,他们的角色是什么等。
    • 如果请求通过了过滤器的安全检查,它接下来会到达拦截器。拦截器可以进一步处理请求,比如添加一些特定的头信息,或者检查请求是否符合某些特定的业务规则。
    • 过滤器和拦截器共同确保了请求在达到实际的业务逻辑(比如你的 Controller)之前,已经是安全和符合要求的。

Spring Security 的过滤器和拦截器就像是一系列安全检查点,确保只有合法和安全的请求可以通过并被处理。过滤器更注重于安全性,而拦截器则提供了更多的灵活性和精细的控制。

基础配置

SpringSecurity的基础配置,配置“/user/login”登录不登录都可以访问,其它接口必须要经过过滤器,这里配置了自定义的JWT过滤器,每个请求过来的时候都会由JWT过滤器进行判断与放行:

如果登录了,就把token对应的用户信息放到SpringSecurityCentext中并放行,没登录的直接放行,因为没有放入用户信息,所以SpringSecurity就会直接返回403。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {// 继承WebSecurityConfigurerAdapter来自定义安全配置@AutowiredJwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;  // 自动注入JWT认证过滤器@Bean  // 标识返回的对象应该被Spring容器管理public PasswordEncoder passwordEncoder() {// 定义密码编码器,使用BCrypt强哈希算法return new BCryptPasswordEncoder();}@Bean  // 定义一个Spring管理的Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {// 重写方法以返回AuthenticationManager,用于处理认证请求return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 定义如何通过拦截器保护请求http.csrf().disable()  // 禁用CSRF保护.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // 设置为无状态,不依赖Session.and().authorizeRequests()  // 开始定义URL保护规则.antMatchers("/user/login").anonymous()  // “/user/login”无需认证即可访问.anyRequest().authenticated()  // 其他所有请求都需要认证.and().addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 在UsernamePasswordAuthenticationFilter之前添加自定义JWT过滤器}
}
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {filterChain.doFilter(request, response);return;}//解析tokenString userId = "";try {Claims claims = JwtUtil.parseJWT(token);userId = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}//从redis中获取用户信息String redisKey = "login:" + userId;// 从Redis中获取JSON字符串JSONObject jsonObject = redisCache.getCacheObject(redisKey);LoginUser loginUser = jsonObject.toJavaObject(LoginUser.class);if (Objects.isNull(loginUser)) {throw new RuntimeException("token非法,或者用户登录超时");}//存入SecurityContextHolder//TODO 获取权限信息封装到Authentication中//必须要使用三个构造函数的,这个构造函数中有确定已经认证的代码//UsernamePasswordAuthenticationToken的构造函数,两个参数适用于登录的,三个参数适用于认证的//两个参数 → 用户提交的未经验证的身份信息。三个参数 → 已经经过验证的身份信息,带有权限集合。UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//请求放行filterChain.doFilter(request, response);}
}

 登录流程

这里省略control,直接从service开始,一行一行的讲:

@Service
public class LoginServiceImpl implements LoginService {// 自动注入AuthenticationManager,用于处理认证请求@Autowiredprivate AuthenticationManager authenticationManager;// 自动注入RedisCache,用于缓存相关操作@Autowiredprivate RedisCache redisCache;// 登录方法@Overridepublic ResponseResult login(User user) {// 创建UsernamePasswordAuthenticationToken用于身份验证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());// 调用AuthenticationManager进行身份验证Authentication authenticate = authenticationManager.authenticate(authenticationToken);// 如果认证失败(返回null),抛出异常if (Objects.isNull(authenticate)) {throw new RuntimeException("用户名或密码错误");}// 认证成功后,使用用户ID生成JWT tokenLoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userId = loginUser.getUser().getId().toString();String jwt = JwtUtil.createJWT(userId);// 将认证信息存储到Redis中redisCache.setCacheObject("login:" + userId, loginUser);// 构造响应数据,包含JWT tokenHashMap<String, String> map = new HashMap<>();map.put("token", jwt);return new ResponseResult(200, "登陆成功", map);}
}

让AuthenticationManager加入到Spring管理:

@Bean  // 定义一个Spring管理的Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {// 重写方法以返回AuthenticationManager,用于处理认证请求return super.authenticationManagerBean();
}

1、这行是用于认证,参数为账号与密码, 创建了一个包含用户名和密码的认证令牌。此时,该令牌的Authenticated属性为false,因为它仅表示一个待验证的认证请求。

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());

2、将这个令牌传递给AuthenticationManager的authenticate方法后,开始了实际的认证过程。

Authentication authenticate = authenticationManager.authenticate(authenticationToken);

这行代码内部会调用UserDetailsService实现类中的loadUserByUsername方法,这个方法会返回对应账号的所有信息。

authenticationManager.authenticate会根据返回的用户信息与前端传入的做比较:

  1. authenticate方法会比较UsernamePasswordAuthenticationToken中的凭据和UserDetailsService加载的用户信息。如果认证成功,它会返回一个更新后的UsernamePasswordAuthenticationToken,此时,其Authenticated属性被设置为true,并且该令牌会包含用户的详细信息(通常封装在一个实现了UserDetails的自定义类中,例如LoginUser)。
  2. 如果认证失败(例如,因为密码不匹配或用户不存在),则通常会抛出一个异常(如UsernameNotFoundException或BadCredentialsException)。

下面是UserDetailsService的实现与LoginUser的实现:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//查询用户信息LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getUserName, username);User user = userMapper.selectOne(queryWrapper);if (Objects.isNull(user)){throw new RuntimeException("用户名或密码错误,用户名不存在");}//TODO 查询对应权限信息return new LoginUser(user);}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {// 用户实体类,用于存储用户信息private User user;// 获取用户的权限集合。在这个例子中,返回null意味着没有指定权限@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}// 获取用户的密码,用于认证过程中的密码校验@Overridepublic String getPassword() {return user.getPassword();}// 获取用户的用户名,用于认证过程中的用户识别@Overridepublic String getUsername() {return user.getUserName();}// 账户是否未过期。返回true表示账户未过期@Overridepublic boolean isAccountNonExpired() {return true;}// 账户是否未被锁定。返回true表示账户未被锁定@Overridepublic boolean isAccountNonLocked() {return true;}// 凭证(密码)是否未过期。返回true表示密码未过期@Overridepublic boolean isCredentialsNonExpired() {return true;}// 账户是否可用。返回true表示账户是可用的@Overridepublic boolean isEnabled() {return true;}
}

3、剩下的其它的代码就很常规了。 

为什么不直接使用:Authentication authenticate = authenticationManager.authenticate(用户名,密码的形式)

Spring Security的AuthenticationManager.authenticate接口设计成可以接受不同类型的Authentication实现,而UsernamePasswordAuthenticationToken只是这些实现之一。这种设计提供了极大的灵活性和扩展性,允许Spring Security支持各种不同的认证机制。下面是一些详细解释:

UsernamePasswordAuthenticationToken和其他Authentication实现的区别

UsernamePasswordAuthenticationToken:这是最常见的Authentication实现之一,通常用于基本的用户名和密码认证。
它主要用于表单登录或任何需要用户名和密码的场景。

其他Authentication实现

Spring Security还提供了其他Authentication实现,例如RememberMeAuthenticationToken、JwtAuthenticationToken等。
每种实现都有其特定用途。例如,RememberMeAuthenticationToken用于记住我功能,而JwtAuthenticationToken可能用于基于JWT的认证。
开发者还可以根据需要创建自定义的Authentication实现,以支持特定的认证机制。

特定的需求

每种认证类型可能需要携带不同的信息。例如,JWT认证可能需要携带解析后的令牌信息,而传统的表单登录只需要用户名和密码。

处理逻辑的差异

不同类型的Authentication对象可能需要通过不同的AuthenticationProvider进行处理。例如,UsernamePasswordAuthenticationToken可能由DaoAuthenticationProvider处理,而JWT令牌可能由专门处理JWT的提供者处理。

总结

通过支持不同类型的Authentication实现,Spring Security能够提供一个统一的框架来处理多种认证机制,从而增加了框架的通用性和灵活性。这样,开发者可以根据自己的安全需求和业务逻辑选择或扩展适当的认证类型。

退出登录流程

这里省略control,直接从service开始,一行一行的讲:

@Service
public class LoginServiceImpl implements LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult logout() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();Long userid = loginUser.getUser().getId();redisCache.deleteObject("login:"+userid);return new ResponseResult(200,"退出成功");}
}

1、从SecurityContextHolder获取当前的安全上下文(SecurityContext),并从中检索当前认证的用户信息(Authentication对象)。这是获取当前登录用户详细信息的标准方法。

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

2、从Authentication对象中提取Principal,它代表了当前已认证的用户。在这个场景中,Principal被转换(或“强制转换”)为LoginUser类型,这是一个自定义的用户类。

LoginUser loginUser = (LoginUser) authentication.getPrincipal();

3、剩下的代码就很常规了,因为登录的时候通过token中的userId作为key,User的JSON作为value存储到redis中,接下来的代码把这个key从redis中删除就可以了,就退出登录了。

SpringSecurity配置类

所有认证相关的配置都在继承WebSecurityConfigurerAdapter 类中重写的configure方法里。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {// 继承WebSecurityConfigurerAdapter来自定义安全配置@AutowiredJwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;  // 自动注入JWT认证过滤器@Bean  // 标识返回的对象应该被Spring容器管理public PasswordEncoder passwordEncoder() {// 定义密码编码器,使用BCrypt强哈希算法return new BCryptPasswordEncoder();}@Bean  // 定义一个Spring管理的Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {// 重写方法以返回AuthenticationManager,用于处理认证请求return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 定义如何通过拦截器保护请求http.csrf().disable()  // 禁用CSRF保护.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // 设置为无状态,不依赖Session.and().authorizeRequests()  // 开始定义URL保护规则.antMatchers("/user/login").anonymous()// 只有不认证的才可以访问.antMatchers("/xxx").permitAll()//有没有认证都可以访问.anyRequest().authenticated()  // 其他所有请求都需要认证.and().addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);/*addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)所有的请求到达之前都要先经过jwt过滤器,如果jwt过滤器没有添加认证就直接返回403jwt在UsernamePasswordAuthenticationFilter之前执行*/}
}

访问权限规则

permitAll():有没有认证的才能访问

anonymous():没有认证的才能访问

authenticated():必须要认证才能访问

权限认证

未完待续...

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

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

相关文章

EDW国际数据管理最新趋势(二)|信息供应链与数据

最近Data Fabric、Data Mesh、DataOps等话题非常火。其实&#xff0c;信息供应链谈的也是同样的东西&#xff0c;那就是如何将数据治理与数据集成整合在一起的解决方案。 下图虽然简单但涵盖了非常大的信息量。将4A架构进行了拆解&#xff0c;应用架构与技术架构主要是支撑业务…

【Qt】QLineEdit显示输入十六进制,位数不足时按照规则填充显示及每两个字符以空格填充

问题 在实际开发中&#xff0c;有时候需要对输入进行限制&#xff0c;一是更加合理&#xff0c;二是防止出现误操作。 比如&#xff1a; 使用Qt进行应用程序开发时&#xff0c;对单行编辑框QLineEdit控件&#xff0c;设置只可输入十六进制。 限制输入的方式常用且经典的是使用…

SQL SERVER 设置权限和隐藏其他数据库

一、创建用户名&#xff0c;选择默认数据库 二、分配权限 --对用户EAM分配 View_1视图 只有 只读select权限 GRANT select on View_1 to EAM --对用户分配指定表权限&#xff08;读写删&#xff09; GRANT SELECT , INSERT , UPDATE , DELETE ON table1 TO [用户名] --对用户分…

SpringBoot项目打jar包

本文章使用idea进行打包 1.打开项目&#xff0c;右击项目选中Open Module Settings进入project Structure&#xff0c;如下图所示&#xff1a; 2.选中Artifacts&#xff0c;点击中间的加号&#xff08;Project Settings->Artifacts->JAR->From modules with dependen…

鸿蒙4.0开发笔记之ArkTS语法基础之应用生命周期与页面中组件的生命周期(十六)

文章目录 一、应用生命周期二、生命周期函数定义三、生命周期五函数练习 一、应用生命周期 1、定义 应用生命周期就是代表了一个HarmonyOS应用中所有页面从创建、开启到销毁等过程的全生命周期。查看路径如下&#xff1a; Project/entry/src/main/ets/entryability/EntryAbili…

云上守沪 | 云轴科技ZStack成功实践精选(上海)

为打造国际数字之都&#xff0c;上海发布数字经济发展“十四五”规划&#xff0c;围绕数字新产业、数据新要素、数字新基建、智能新终端等重点领域&#xff0c;加强数据、技术、企业、空间载体等关键要素协同联动&#xff0c;加快进行数字经济发展布局&#xff1b;加快基础软件…

Gee教程7.错误恢复(Panic Recover)

这一章节是实现错误处理机制&#xff0c;是通过中间件形式来实现的。 panic Go 语言中&#xff0c;比较常见的错误处理方法是返回 error&#xff0c;由调用者决定后续如何处理。但是如果是无法恢复的错误&#xff0c;就会触发panic。用户也可以手动触发 panic&#xff0c;当然…

【PTA题目】7-31 前世档案 分数 20

7-31 前世档案 分数 20 全屏浏览题目 切换布局 作者 陈越 单位 浙江大学 网络世界中时常会遇到这类滑稽的算命小程序&#xff0c;实现原理很简单&#xff0c;随便设计几个问题&#xff0c;根据玩家对每个问题的回答选择一条判断树中的路径&#xff08;如下图所示&#xff09…

【算法】约瑟夫环

约瑟夫问题是个有名的问题&#xff1a;N个人围成一圈&#xff0c;从第一个开始报数&#xff0c;第M个将被杀掉&#xff0c;最后剩下一个&#xff0c;其余人都将被杀掉。例如N6&#xff0c;M5&#xff0c;被杀掉的顺序是&#xff1a;5&#xff0c;4&#xff0c;6&#xff0c;2&a…

搭梯子之后电脑连接WIFI打不开浏览器网页:远程计算机或者设备不接受连接

问题描述&#xff1a; 打不开网页&#xff0c;但是能正常使用微信等app windows网络诊断&#xff1a; 远程计算机或者设备不接受连接 解决办法&#xff1a; 电脑搜索【internet选项】 进入连接&#xff0c;点击局域网设置&#xff0c;将里面的代理服务器选项关掉就可以正常打开…

【Angular开发】Angular中的高级组件

在这个博客中&#xff0c;我将解释Angular中的几个高级组件和机制&#xff0c;它们增强了灵活性、可重用性和性能。 通过熟悉这些高级组件和机制&#xff0c;您可以提高您的Angular开发技能&#xff0c;并在应用程序中利用灵活性、可重用性和性能优化的能力。让我们开始吧&…

flink中处理kafka分区的消息顺序

背景 kafka分区的消息是有序的&#xff0c;那么flink在消费kafka分区的时候消息的顺序是怎么样的呢&#xff1f;还能保持这个有序性吗&#xff0c;本文就来记录下 flink消费kafka分区的顺序性 从上图可知&#xff0c;flink的转换算子比如map&#xff0c;flatMap&#xff0c;f…

一次重新加载所有 maven 项目产生的 OOM

1、解决什么问题&#xff1f; 忘了截图了&#xff0c;用文字描述就是由于Reload All Maven Projects导致的 OOM 异常。 2、尝试与解决 2.1、尝试 2.1.1、尝试清理idea缓存&#xff08;无效&#xff09; 2.1.2、重启idea&#xff08;无效&#xff09; 2.1.3、重启电脑&am…

Python替代Adobe从PDF提取数据

大家好&#xff0c;PDF文件是官方报告、发票和数据表的通用格式&#xff0c;然而从PDF文件中提取表格数据是一项挑战。尽管Adobe Acrobat等工具提供了解决方案&#xff0c;但它们并不总是易于获取或可自动化运行&#xff0c;而Python则是编程语言中的瑞士军刀。本文将探讨如何利…

SpringMVC修炼之旅(2)基础入门

一、第一个程序 1.1环境配置 略 1.2代码实现 package com.itheima.controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody;//定义…

Oozie创建定时执行脚本

1&#xff0c;前期创建的有test.sql表数据脚本&#xff0c;一个脚本my_workflow_1,可以看我的上一篇博客Oozie调度工具–一次性脚本文件的创建和执行 2,点击这个查询下面的计划程序中的计划 3&#xff0c;先修改这个项目的名字&#xff0c;然后选择Workflow里面之前自己测试执…

class051 二分答案法与相关题目【算法】

class051 二分答案法与相关题目【算法】 算法讲解051【必备】二分答案法与相关题目 code1 875. 爱吃香蕉的珂珂 // 爱吃香蕉的珂珂 // 珂珂喜欢吃香蕉。这里有 n 堆香蕉&#xff0c;第 i 堆中有 piles[i] 根香蕉 // 警卫已经离开了&#xff0c;将在 h 小时后回来。 // 珂珂…

Windows利用MMDeploy部署OpenMMLab 模型并使用Python进行部署

目录 前言 一、准备工作 二、安装 MMDeploy 总结 前言 近期在用OpenMMLab构建模型&#xff0c;然后需要使用MMDeploy对模型进行部署。虽然官方文档提供了详细的说明&#xff0c;但是写的太繁琐了&#xff0c;而且在实际部署过程中&#xff0c;发现并不是所有步骤和内容都需要&…

MySQL数据库基础篇

文章目录 前言1.MySQL数据库概述2.SQL2.1 SQL通用语法2.2 SQL分类2.3 DDL2.3.1 数据库操作2.3.2 表操作 2.4 图形化界面工具2.5 DML2.5.1 添加数据2.5.2 修改数据2.5.3 删除数据 2.6 DQL2.6.1 基本语法2.6.2 基础查询2.6.3 条件查询2.6.4 聚合函数2.6.5 分组查询2.6.6 排序查询…

deepflow本地部署过程

本地服务器配置&#xff0c;32C&#xff0c;48G内存 整个过程需要配置k8s&#xff0c;安装helm, 安装grafana, 安装deepflow以及deepflow-ctl&#xff0c;以及部署demo 在采用sealos进行ALL-IN-ONE部署之前&#xff0c; grafana 先安装它 wget -q -O /usr/share/keyrings/gr…