SpringSecurity工作原理(一)

news2025/2/24 8:26:39

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

大概工作流程 

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来自定义安全配置

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;  // 自动注入JWT认证过滤器

    @Bean  // 标识返回的对象应该被Spring容器管理
    public PasswordEncoder passwordEncoder() {
        // 定义密码编码器,使用BCrypt强哈希算法
        return new BCryptPasswordEncoder();
    }

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

    @Override
    protected 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 {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            filterChain.doFilter(request, response);
            return;
        }

        //解析token
        String 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,用于处理认证请求
    @Autowired
    private AuthenticationManager authenticationManager;

    // 自动注入RedisCache,用于缓存相关操作
    @Autowired
    private RedisCache redisCache;

    // 登录方法
    @Override
    public 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 token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);

        // 将认证信息存储到Redis中
        redisCache.setCacheObject("login:" + userId, loginUser);

        // 构造响应数据,包含JWT token
        HashMap<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 {

    @Autowired
    private UserMapper userMapper;

    @Override
    public 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意味着没有指定权限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    // 获取用户的密码,用于认证过程中的密码校验
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    // 获取用户的用户名,用于认证过程中的用户识别
    @Override
    public String getUsername() {
        return user.getUserName();
    }

    // 账户是否未过期。返回true表示账户未过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 账户是否未被锁定。返回true表示账户未被锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 凭证(密码)是否未过期。返回true表示密码未过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 账户是否可用。返回true表示账户是可用的
    @Override
    public 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 {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;

    @Override
    public 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来自定义安全配置

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;  // 自动注入JWT认证过滤器

    @Bean  // 标识返回的对象应该被Spring容器管理
    public PasswordEncoder passwordEncoder() {
        // 定义密码编码器,使用BCrypt强哈希算法
        return new BCryptPasswordEncoder();
    }

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

    @Override
    protected 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过滤器没有添加认证就直接返回403
           jwt在UsernamePasswordAuthenticationFilter之前执行
         */

    }
}

访问权限规则

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

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

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

权限认证

未完待续...

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

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

相关文章

Linux自启服务提示:systemd[1]: *.service: main process exited, code=exited, status=1问题

这两天一直在沉迷于配脚本&#xff0c;由于服务器很多&#xff0c;所以我都是从一台服务器上配置好的脚本直接copy到另一台服务器&#xff0c;按说完全一样的脚本一样的操作&#xff0c;那么应该是一样的执行结果 but, Gul’dan&#xff0c;代…我重启服务器后服务并没有正常启…

k8s官方镜像代理加速

背景 大家可能在云原生领域需要部署周边的一些生态组件时&#xff0c;在国内遇到无法正常拉取镜像&#xff0c;显得就有点苦恼&#xff0c;不过没关系&#xff0c;常见的${{ registry_name }} 例如 “gcr.io”&#xff0c;“registry.k8s.io” Failed to pull image “registry…

【c】杨辉三角

下面介绍两种方法 1.利用上面性质的第五条&#xff0c;我们可以求各行各列的组合数 2.利用上面性质的第7条&#xff0c;我们可以用数组完成 下面附上代码 1. #include<stdio.h> void fact(int n ,int m )//求组合数 {long long int sum11;long long int sum21;int a…

AI Pika 生成进击的巨人动漫分镜案例

背景介绍 Pika 是一个使用 AI 生成和编辑视频的平台。它致力于通过 AI 技术使视频制作变得简单和无障碍。 Pika 1.0 是 Pika 的一个重大产品升级&#xff0c;包含了一个新的 AI 模型,可以在各种风格下生成和编辑视频,如 3D 动画&#xff0c;动漫&#xff0c;卡通和电影风格。…

跳水比赛(C++)

系列文章目录 进阶的卡莎C++_睡觉觉觉得的博客-CSDN博客数1的个数_睡觉觉觉得的博客-CSDN博客双精度浮点数的输入输出_睡觉觉觉得的博客-CSDN博客足球联赛积分_睡觉觉觉得的博客-CSDN博客大减价(一级)_睡觉觉觉得的博客-CSDN博客小写字母的判断_睡觉觉觉得的博客-CSDN博客纸币(…

从零开发短视频电商 Jmeter压测示例模板详解(无认证场景)

文章目录 添加线程组添加定时器添加HTTP请求默认值添加HTTP头管理添加HTTP请求添加结果断言响应断言 Response AssertionJSON断言 JSON Assertion持续时间断言 Duration Assertion 添加察看结果树添加聚合报告添加表格察看结果参考 以压测百度搜索为例 https://www.baidu.com/s…

面试多线程八股文十问十答第一期

面试多线程八股文十问十答第一期 作者&#xff1a;程序员小白条&#xff0c;个人博客 相信看了本文后&#xff0c;对你的面试是有一定帮助的&#xff01; ⭐点赞⭐收藏⭐不迷路&#xff01;⭐ 1.ThreadLocal如何实现线程安全 Java的ThreadLocal是一个线程本地变量&#xff0…

Python中字符串拼接及其应用场景

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 字符串拼接是Python中常见而重要的操作&#xff0c;它涉及到将多个字符串连接成一个字符串。本文将深入探讨Python中字符串拼接的不同方式、性能比较、以及在实际应用中的场景和最佳实践。 常见的字符串拼接方法…

【JUC】二十一、CAS比较并交换

文章目录 1、初体验2、CAS概述3、Unsafe类4、Unsafe汇编5、原子引用AutomicReference6、手写自旋锁SpinLock7、CAS的两大缺点8、AtomicStampedReference类解决ABA问题 1、初体验 没有CAS时&#xff0c;多线程环境下不使用原子类保证线程安全&#xff0c;比如i&#xff0c;可以…

21章网络通信

21.1——网络程序设计基础 网络程序设计编写得到是与其他计算机进行通信的程序 21.1.1——局域网与互联网 为了实现两台计算机的通信&#xff0c;必须用一个网络线路连接两台计算机 21.1.2——网络协议 网络协议规定了计算机之间连接的物理、机械 (网线与网卡的连接规定)、…

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

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

Linux环境下的MySQL安装

文章目录 前提说明1.卸载内置环境2.检查系统安装包3.卸载这些默认安装包4.获取MySQL官方yum源5.安装MySQLyum源&#xff0c;对比前后yum源6.查看yum源是否生效7.安装MySQL服务8.查看相对应的配置文件9.启动服务10.查看启动服务11.登录方法一12.登录方法二13.登录方法三14.设置开…

护理简历自我评价15篇

自我评价示例1&#xff1a; 我性格开朗&#xff0c;上进心强&#xff0c;做事不马虎&#xff0c;有良好的思想道德&#xff0c;注重集体荣誉感。我具备强大的护理技能和团队协作精神&#xff0c;能够在高压环境下保持冷静&#xff0c;积极应对挑战。我期待着在医疗领域发挥我的…

MySQL数据库与其管理工具Navicat

这里介绍MySQL数据库和Navicat的使用 1.下载MySQL数据库及MySQL客户端管理工具Navicat 登录www.mysql.com下载MySQL 登录www.navicat.com.cn/download下载客户端管理工具 2.启动MySQL数据库服务器 以管理员身份打开命令提示窗口 找到mysql的bin目录 输入初始化命令mysqld…

认识jmeter接口测试工具!

jmeter简介 Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试&#xff0c;它最初被设计用于Web应用测试&#xff0c;但后来扩展到其他测试领域。 下载 下载地址&#xff1a;Apache JMeter - Download Apache JMeter 安装 由于Jmeter是基于Java的…

springboot 整合 Spring Security+JWT 实现token 认证和校验

1.大概是这个样子 JWT 是什么&#xff1f; Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准&#xff08;(RFC 7519).该token被设计为紧凑且安全的&#xff0c;特别适用于分布式站点的单点登录&#xff08;SSO&#xff09;场景。JWT的声明…

设计原则 | 依赖转置原则

一、依赖转置原则&#xff08;DIP&#xff1a;Dependence Inversion Principle&#xff09; 1、原理 高层模块不应该依赖低层模块&#xff0c;二者都应该依赖于抽象抽象不应该依赖于细节&#xff0c;细节应该依赖于抽象 2、层次化 Booch曾经说过&#xff1a;所有结构良好的面…

HTML5+CSS3+JS小实例:数字滑动选择控件

实例:数字滑动选择控件 技术栈:HTML+CSS+JS 效果: 源码: 【HTML】 <!DOCTYPE html> <html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"><meta name="viewport" content=&quo…

【数据结构(八)】哈希表

文章目录 1. 基本概念1.1. 哈希表基本介绍 2. 实例应用2.1. 思路分析2.2. 代码实现2.2.1. 实现添加、显示功能2.2.2. 实现查找功能 1. 基本概念 先看一个实际需求&#xff1a; google 公司的一个上机题&#xff1a;     有一个公司&#xff0c;当有新的员工来报道时&…

Centos7部署NFS服务

搭建NFS存储服务器--基于CentOS7系统 - jianmuzi - 博客园 在CentOS中搭建NFS - 陌上荼靡 - 博客园 NFS简介 NFS 是 Network FileSystem 的缩写&#xff0c;顾名思义就是网络文件存储系统&#xff0c;它最早是由 Sun 公司发展出来的&#xff0c;也是 FreeBSD 支持的文件系统…