小菜家教平台(三):基于SpringBoot+Vue打造一站式学习管理系统

news2024/11/26 18:14:53

目录

前言

今日进度

详细过程

相关知识点

前言

昨天重构了数据库并实现了登录功能,今天继续进行开发,创作不易,请多多支持~

今日进度

添加过滤器、实现登出功能、实现用户授权功能校验

详细过程

一、添加过滤器

自定义过滤器作用:自定义的 JWT 认证过滤器,用于解析请求头中的 token,验证用户身份,并将用户信息存入 SecurityContextHolder,从而支持 Spring Security 的认证和授权功能。

package com.example.familyeducation.utils.filter;

import com.example.familyeducation.entity.LoginUser;
import com.example.familyeducation.utils.JwtUtil;
import com.example.familyeducation.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

import static com.example.familyeducation.utils.constants.RedisConstants.LOGIN_USER_KEY;

/**
 * @ClassDescription:自定义过滤器,获取请求头中的token并解析
 * @Author:小菜
 * @Create:2024/11/6 10:34
 **/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //1.获取请求头中的token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //2.token为空,直接放行
            filterChain.doFilter(request, response);
            return;
        }
        //3.token不为空
        //3.1解析token中的id
        String id;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            id = claims.getSubject();
        } catch (Exception e) {
            //TODO 使用统一异常类封装
            throw new RuntimeException("token非法");
        }
        //4.根据id从redis中获取用户信息
        String key = LOGIN_USER_KEY + id;
        LoginUser loginUser = redisCache.getCacheObject(key);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("redis中数据为空,用户未登录");
        }
        //5.将用户信息存入SecurityContextHolder
        //TODO获取当前用户权限信息封装到Authentication 直接从LoginUser中获取即可
        //5.1封装用户信息到Authentication

        UsernamePasswordAuthenticationToken authenticationToken
                = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        //5.2将信息存入SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //6.放行
        filterChain.doFilter(request,response);
    }
}

添加完过滤器后记得去将过滤器添加上

//添加自定义过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

这样访问其他接口时,就会自动解析token并进行对应操作

二、登出功能

在登出功能中我们将数据从Redis中进行删除,那样其他接口就无法访问到这些数据,在过滤器中就会显示用户为登录,实现登出功能

@Override
    public ResponseResult logout() {
        //1.从SecurityContextHolder中查找到Authentication
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //2.从Authentication中获取LoginUser
        LoginUser loginUser  = (LoginUser) authentication.getPrincipal();
        //3.获取id并从Redis中删除,那样下次再进行过滤时就查询不到Redis中的数据,显示用户未登录
        Integer userId = loginUser.getUser().getId();
        redisCache.deleteObject(LOGIN_USER_KEY+userId);
        return new ResponseResult(200,"成功退出登录");
    }

三、权限校验

权限校验今天踩了很多坑,因为之前没有接触过这个

 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

首先,权限校验的思路如下:

  1. 请求先到过滤器,此时的请求中携带token
  2. 在过滤器中解析token得到id并从Redis中取得数据(用户数据和权限信息)
  3. 过滤器将数据存到SecurityContextHolder,这样请求进行过程中就能得到数据,可以来判断是否有权限,若无权限则返回403

首先我们先去配置中配置一下

我们要开启一下配置并指定路径与权限的关系,指定哪些路径需要哪些权限才能访问

package com.example.familyeducation.config;

import com.example.familyeducation.utils.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
//@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                .antMatchers("/hello/**").hasRole("ADMIN")//对于/hello的路径,只有ADMIN权限的用户才能访问
                .antMatchers("/ok/**").hasAnyRole("ADMIN","USER")//对于/ok的路径,ADMIN和USER权限的用户都可以访问
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //添加自定义过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

配置完后,因为登录成功后才能得到用户数据和权限,所以我们要去UserDetailsServiceImpl中完成之前的TODO将用户权限信息添加到Login中

package com.example.familyeducation.service.impl;

import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.familyeducation.entity.LoginUser;
import com.example.familyeducation.entity.User;
import com.example.familyeducation.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * @ClassDescription:
 * @Author:小菜
 * @Create:2024/11/4 19:06
 **/

//这里继承的是security中的一个默认接口,重写其中的查询用户方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername,username);
        User user = userMapper.selectOne(wrapper);
        //如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        //TODO根据用户查询权限信息 添加到LoginUser中
        //上面的user信息中已经包含了权限信息,但是我们还是要单独把权限提出来

        List<String> list = new ArrayList<>();//这里list就是LoginUser中的redisAuthorities
        String role = user.getRole();
        if(role.equals("admin")){
            list.add("ROLE_ADMIN");//注意这里添加自定义权限时要加前缀ROLE_,SpringSecurity会默认根据ROLE_去查找权限
        } else if (role.equals("teacher")) {
            list.add("ROLE_TEACHER");
        }
        //封装成UserDetails对象返回
        return new LoginUser(user,list);
    }
}

同时在LoginUser中我们要进行authorities的管理并编写getAuthorities()方法保证过滤器能获取到用户权限

package com.example.familyeducation.entity;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;
    private List<String> redisAuthorities;
    public LoginUser(User user, List<String> redisAuthorities) {
        this.user=user;
        this.redisAuthorities=redisAuthorities;
    }

    @JSONField(serialize = false)//保证该集合不被序列化
    private List<GrantedAuthority> authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }else{
            authorities = redisAuthorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
            return authorities;
        }
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

最后我们使用Apifox进行测试

先登录一下,成功返回了token,同时Redis中保存了用户信息和权限

好,接下来对权限进行测试,hello路径是只有ADMIN能进行访问,而我们当前用户刚好是管理员,所以得到了返回信息,测试成功!

相关知识点

登录功能思路:

  1. 请求先到过滤器,过滤器检查到token为空,直接放行
  2. 放行后请求到controller层,并调用service层的实现类LoginServiceImpl
  3. Service实现类LoginServiceImpl中会对认证信息进行验证,于是调用SpringSecurity中的authenticationManager.authenticate()方法进行检验
  4. 这个方法会自动调用UserDetailsService中的loadUserByUsername进行用户信息验证
  5. 在UserDetailsService中进行用户数据查询,同时将用户信息中的权限进行封装
  6. 最后将用户信息和权限信息封装成LoginUser进行返回到登录实现类LoginServiceImpl中
  7. 实现类LoginServiceImpl根据返回的信息生成token返回前端,并将loginUser存到Redis

实现授权功能思路:

  1. 请求先到过滤器,此时的请求中携带token
  2. 在过滤器中解析token得到id并从Redis中取得数据(用户数据和权限信息)
  3. 过滤器将数据存到SecurityContextHolder,这样请求进行过程中就能得到数据,可以来判断是否有权限,若无权限则返回403

这里有两个坑,一个是权限能获取到,但是保存到Redis中一直是null,另一个是成功保存了权限,但是测试时一直显示403。

我们先解决第一个问题,在封装信息时我们会调用LoginUser中的一个获取权限的方法,这里一开始是定义了一个authorities并直接返回,但是就会有一个问题,当一个新方法调用时,authorities会被重新刷新为null,就导致权限信息一直是null,解决方法是定义一个新的List,并将其保存到Redis中,那样调用新方法,我们在过滤器中获取Redis中的权限,就不会是null了。

private User user;
    private List<String> redisAuthorities;
    public LoginUser(User user, List<String> redisAuthorities) {
        this.user=user;
        this.redisAuthorities=redisAuthorities;
    }

    @JSONField(serialize = false)//保证该集合不被序列化
    private List<GrantedAuthority> authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }else{
            authorities = redisAuthorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
            return authorities;
        }
    }

第二个问题,这个真的给我整无语了,Spring Security 会通过 GrantedAuthority 来验证用户是否拥有特定的权限。例子中,用户访问 /ok/** 时,Spring Security 会检查 SecurityContextHolder.getContext().getAuthentication().getAuthorities() 中是否包含 ROLE_ADMINROLE_USER

  • Spring Security 的 hasRole() 方法默认会自动在角色前加上 ROLE_ 前缀,所以当你配置 .hasRole("ADMIN") 时,实际上是在检查用户是否有 ROLE_ADMIN 权限。
  • 如果用户的权限中有 ROLE_ADMINROLE_USER,则通过访问检查,允许访问这个路径,否则拒绝访问。

所以就是在手动封装权限的时候没有加上前缀导致权限信息一直对不上,所以就显示403了。。。

 List<String> list = new ArrayList<>();//这里list就是LoginUser中的redisAuthorities
        String role = user.getRole();
        if(role.equals("admin")){
            list.add("ROLE_ADMIN");//注意这里添加自定义权限时要加前缀ROLE_,SpringSecurity会默认根据ROLE_去查找权限
        } else if (role.equals("teacher")) {
            list.add("ROLE_TEACHER");
        }

ok,大概就是这样,如果有帮到你的话,请多多支持哦!你的鼓励就是我最大的动力,我们下篇再见~

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

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

相关文章

小新学习k8s第六天之pod详解

一、资源限制 Pod是k8s中的最小的资源管理组件&#xff0c;pod也是最小化运行容器化应用的资源对象。一个Pod代表着集群中运行的一个进程。k8s中其他大多数组件都是围绕着Pod来进行支撑和扩展Pod功能的&#xff0c;例如&#xff0c;用于管理Pod运行的StatefulSet和Deployment等…

戴尔电脑 Bios 如何进入?Dell Bios 进入 Bios 快捷键是什么?

BIOS&#xff08;基本输入输出系统&#xff09;是计算机启动时运行的第一个程序&#xff0c;它负责初始化硬件并加载操作系统。对于戴尔电脑用户来说&#xff0c;有时可能需要进入 BIOS 进行一些特定的设置调整&#xff0c;比如更改启动顺序、调整性能选项或解决硬件兼容性问题…

【UE5】一种老派的假反射做法,可以用于移动端,或对反射的速度、清晰度有需求的地方

没想到大家这篇文章呼声还挺高 这篇文章是对它的详细实现&#xff0c;建议在阅读本篇之前&#xff0c;先浏览一下前面的文章&#xff0c;以便更好地理解和掌握内容。 这种老派的假反射技术&#xff0c;适合用于移动端或对反射效果的速度和清晰度有较高要求的场合。该技术通过一…

MongoDB笔记02-MongoDB基本常用命令

文章目录 一、前言二、数据库操作2.1 选择和创建数据库2.2 数据库的删除 3 集合操作3.1 集合的显式创建3.2 集合的隐式创建3.3 集合的删除 四、文档基本CRUD4.1 文档的插入4.1.1 单个文档插入4.1.2 批量插入 4.2 文档的基本查询4.2.1 查询所有4.2.2 投影查询&#xff08;Projec…

【启程Golang之旅】深入理解 Protocol Buffers 及其应用

如果你是 Go 语言的开发者&#xff0c;理解如何在 Go 中使用 Protobuf&#xff0c;将帮助你大幅提升数据传输的效率&#xff0c;并实现更高性能的系统设计。 本篇文章将深入探讨 Go 语言中使用 Protobuf 的基础知识、常见应用以及最佳实践&#xff0c;带你一步步了解如何在项目…

最新发布:数智人才成长引擎

数字经济时代 数智人才缺口不断增大 根据公开发布的信息显示&#xff0c;中国&#xff0c;作为全球最大的数字经济体之一&#xff0c;当前数智人才&#xff08;涵盖了数智人才、数据分析师、AI专家、云计算工程师等关键岗位&#xff09;的总体缺口已攀升至惊人的2500万至3000…

ServletContext介绍

文章目录 1、ServletContext对象介绍1_方法介绍2_用例分析 2、ServletContainerInitializer1_整体结构2_工作原理3_使用案例 3、Spring案例源码分析1_注册DispatcherServlet2_注册配置类3_SpringServletContainerInitializer 4_总结 ServletContext 表示上下文对象&#xff0c;…

Linux的硬盘管理

硬盘有价&#xff0c;数据无价 1. 硬盘的概念 硬盘是一种计算机的存储设备&#xff0c;通常是由一个或者多个磁性盘片组成。硬盘即可以安装在计算机的内部&#xff0c;也可以外接计算机。 保存数据 数据&#xff1a;操作系统&#xff0c;应用程序&#xff0c;文档多媒体文件…

云渲染与汽车CGI图像技术优势和劣势

在数字时代&#xff0c;云渲染技术以其独特的优势在汽车CGI图像制作中占据了重要地位。云渲染通过利用云计算的分布式处理能力&#xff0c;将渲染任务分配给云端的服务器集群进行计算&#xff0c;从而实现高效、高质量的渲染效果。 这种技术的优势主要体现在以下几个方面&#…

鸿蒙NEXT开发-学生管理系统小案例(基于最新api12稳定版)

注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注&#xff0c;博主会一直更新鸿蒙next相关知识 专栏地址: https://blog.csdn.net/qq_56760790/…

《潜行者2切尔诺贝利之心》游戏引擎介绍

潜行者2切尔诺贝利之心是基于虚幻5引擎&#xff0c;所以画面效果大家不必担心。游戏目前已经跳票了很久&#xff0c;预计发售时间是2024 年 11 月 21 日&#xff0c;这次应该不会再跳票。 潜行者2切尔诺贝利之心是虚幻5吗 答&#xff1a;是虚幻5。 潜行者官方推特之前回复了…

WPF+MVVM案例实战(十八)- 自定义字体图标按钮的封装与实现(ABD类)

文章目录 1、案例效果1、按钮分类2、ABD类按钮实现描述1.文件创建与代码实现2、样式引用与控件封装3、按钮案例演示1、页面实现与文件创建2、运行效果如下3、总结4、源代码获取1、案例效果 1、按钮分类 在WPF开发中,最常见的就是按钮的使用,这里我们总结以下大概的按钮种类,…

在Vue和OpenLayers中使用移动传感器实现飞机航线飞行模拟

项目实现的核心代码 项目概述 该项目的目标是使用Vue.js作为前端框架&#xff0c;结合OpenLayers用于地图显示&#xff0c;实时获取来自手机传感器的数据&#xff08;如经纬度、高度、速度&#xff09;来模拟飞机在地图上的飞行轨迹。整体架构如下&#xff1a; Vue.js 用于构建…

C语言-详细讲解-洛谷P1075 [NOIP2012 普及组] 质因数分解

1.题目要求 2.题目解析 解题点在于如何分解质因数&#xff0c;这里介绍一下短除法。&#xff08;虽然解决这个问题可以不用短除法&#xff09; 3.代码实现 贴一下自己的代码 #include <stdio.h> #include <math.h>int main() {int n, i;scanf("%d",…

基于springboot的音乐网站的设计与实现(源码+lw+调试)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

RabbitMQ 管理平台(控制中心)的介绍

文章目录 一、RabbitMQ 管理平台整体介绍二、Overview 总览三、Connections 连接四、Channels 通道五、Exchanges 交换机六、Queues 队列查看队列详细信息查看队列的消息内容 七、Admin 用户给用户分配虚拟主机 一、RabbitMQ 管理平台整体介绍 RabbitMQ 管理平台内有六个模块&…

Golang | Leetcode Golang题解之第542题01矩阵

题目&#xff1a; 题解&#xff1a; type point struct{x, y int }var dirs []point{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}func updateMatrix(mat [][]int) [][]int {var m, n len(mat), len(mat[0])var res make([][]int, m)var visited make([][]bool, m)var queue []poin…

前端介绍|基础入门-html+css+js

文章目录 本课程有什么&#xff1f;前端是什么&#xff1f;1. **前端概述**2. **前端的工作职责**3. **前端技术栈**6. **前端开发工具**7. **HTML、CSS、JS的关系** 本课程有什么&#xff1f; 本套课程是零基础入门保姆级课程&#xff0c;课程主要内容包含&#xff1a; HTML…

自动驾驶---“火热的”时空联合规划

1 背景 早期的不少规划算法都是横纵分离的&#xff08;比如Apollo&#xff09;&#xff0c;先求解path之后&#xff0c;依赖path的结果再进行speed的求解。这种横纵解耦的规划方式具有以下特点&#xff1a; 相对较为简单&#xff0c;计算量通常较小&#xff0c;容易实现实时性…

龙蜥副理事长张东:加速推进 AI+OS 深度融合,打造最 AI 的服务器操作系统

AI 原生时代&#xff0c;操作系统厂商要全面优先拥抱 AI&#xff0c;深度融合 AI 能力&#xff0c;发挥关键生态位作用&#xff0c;做好上游芯片与下游 AI 应用开发商之间的纽带&#xff0c;打造最 AI 的服务器操作系统&#xff0c;实现 AI 能力的快速价值转化。 AI 原生趋势下…