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

目录

前言

今日进度

详细过程

相关知识点

前言

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

今日进度

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

详细过程

一、添加过滤器

自定义过滤器作用:自定义的 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 {@Resourceprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//1.获取请求头中的tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {//2.token为空,直接放行filterChain.doFilter(request, response);return;}//3.token不为空//3.1解析token中的idString 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封装用户信息到AuthenticationUsernamePasswordAuthenticationToken authenticationToken= new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());//5.2将信息存入SecurityContextHolderSecurityContextHolder.getContext().setAuthentication(authenticationToken);//6.放行filterChain.doFilter(request,response);}
}

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

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

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

二、登出功能

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

@Overridepublic ResponseResult logout() {//1.从SecurityContextHolder中查找到AuthenticationAuthentication authentication = SecurityContextHolder.getContext().getAuthentication();//2.从Authentication中获取LoginUserLoginUser 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 {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Overrideprotected 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@Overridepublic 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 {@Autowiredprivate UserMapper userMapper;@Overridepublic 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中的redisAuthoritiesString 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;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if(authorities!=null){return authorities;}else{authorities = redisAuthorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());return authorities;}}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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;@Overridepublic 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中的redisAuthoritiesString 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.xdnf.cn/news/10525.html

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

相关文章

【数据处理】数据预处理·数据变换(熵与决策树)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;软件开发必备知识_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前…

【电子通识】TINA-TI 如何产生动态电流波形?

在仿真过程中我们有些时候需要动态电流负载。比如说从5A跳到1A。那这种情况下我们怎么仿真呢&#xff1f; 首先打开TINA-TI软件&#xff0c;找到Sources源&#xff0c;放置一个可控电流源。 放置好后双击器件打开属性&#xff0c;点到Signal信号框中的三个小点。 找到piecewise…

【Qt聊天室客户端】登录窗口

1. 验证码 具体实现 登录界面中创建验证码图片空间&#xff0c;并添加到布局管理器中 主要功能概述&#xff08;创建一个verifycodewidget类专门实现验证码操作&#xff09; 详细代码 // 头文件#ifndef VERIFYCODEWIDGET_H #define VERIFYCODEWIDGET_H#include <QWidget>…

十七:Spring Boot依赖 (2)-- spring-boot-starter-web 依赖详解

目录 1. spring-boot-starter-web 简介 1.1 作用与功能&#xff1a; 1.2 引入方式&#xff1a; 1.3 包含的核心依赖&#xff1a; 2. 自动配置原理 3. 内嵌 Servlet 容器 3.1 默认 Tomcat 配置&#xff1a; 3.2 替换容器&#xff08;Jetty 或 Undertow&#xff09;&#…

Python注意力机制Attention下CNN-LSTM-ARIMA混合模型预测中国银行股票价格|附数据代码...

全文链接&#xff1a;https://tecdat.cn/?p38195 股票市场在经济发展中占据重要地位。由于股票的高回报特性&#xff0c;股票市场吸引了越来越多机构和投资者的关注。然而&#xff0c;由于股票市场的复杂波动性&#xff0c;有时会给机构或投资者带来巨大损失。考虑到股票市场的…

MySQL中的INT(4)里面的4究竟代表着什么

目录 1. 理解INT类型中的数字2. INT显示宽度与INSERT操作3. SELECT、INSERT、UPDATE、DELETE与显示宽度4. 实际应用中的影响场景5. 创建表时的建议 1. 理解INT类型中的数字 在MySQL中&#xff0c;当你定义一个整数字段为INT(M)&#xff0c;这里的M代表的是显示宽度。 这个数字…

Spring Boot实现的工程认证计算机课程管理解决方案

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理基于工程教育认证的计算机课程管理平台的相…

中药香料价钱快速划价计算器软件 中药文本识别计算费用管理系统操作教程

一、概述 【软件试用版资源文件下载可点文章最后官网卡片了解】 中药香料价钱快速划价计算器软件 中药文本识别计算费用管理系统操作教程 ‌核心功能‌&#xff1a;快速划价与账单管理。 ‌快速划价‌&#xff1a;复制药方文本&#xff0c;点击划价按钮即可计算总金额‌。‌账…

代码随想录刷题记录(二十五)——54. 替换数字

&#xff08;一&#xff09;问题描述 54. 替换数字&#xff08;第八期模拟笔试&#xff09;https://kamacoder.com/problempage.php?pid1064给定一个字符串 s&#xff0c;它包含小写字母和数字字符&#xff0c;请编写一个函数&#xff0c;将字符串中的字母字符保持不变&#…

【温度表达转化】

【温度表达转化】 C语言代码C代码Java代码Python代码 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 利用公式 C5∗(F−32)/9 &#xff08;其中C表示摄氏温度&#xff0c;F表示华氏温度&#xff09; 进行计算转化。 输出 输出一行&#x…

【时间之外】IT人求职和创业应知【32】-RTE二次出现

目录 新闻一&#xff1a;AI-AGENT加速落地&#xff0c;计算机行业利润端好转 新闻二&#xff1a;声网CEO赵斌&#xff1a;RTE将成为生成式AI时代AI Infra的关键部分 新闻三&#xff1a;11月科技盛会&#xff1a;新技术与产品发布一览 认知和思考决定了你的赚钱能力。以下是今…

基于51单片机的温控电风扇proteus仿真

地址&#xff1a;https://pan.baidu.com/s/1vgYgY41tp_axxVFTHAPwFg 提取码&#xff1a;1234 仿真图&#xff1a; 芯片/模块的特点&#xff1a; AT89C52/AT89C51简介&#xff1a; AT89C52/AT89C51是一款经典的8位单片机&#xff0c;是意法半导体&#xff08;STMicroelectron…

Spring 配置绑定原理分析

Spring 配置绑定原理分析 前言 Spring 应用中存在诸多配置&#xff0c;有的是系统配置&#xff0c;有的命令行启动参数配置&#xff0c;有的是yaml配置&#xff0c;有的是分布式配置中心配置&#xff0c;但对使用者而言总是可以通过ConfigurationProperties将它关联到一个Java…

Hadoop生态圈框架部署(五)- Zookeeper完全分布式部署

文章目录 前言一、Zookeeper完全分布式部署&#xff08;手动部署&#xff09;1. 下载Zookeeper2. 上传安装包2. 解压zookeeper安装包3. 配置zookeeper配置文件3.1 创建 zoo.cfg 配置文件3.2 修改 zoo.cfg 配置文件3.3 创建数据持久化目录并创建myid文件 4. 虚拟机hadoop2安装并…

HarmonyOS Next 实战卡片开发 03

HarmonyOS Next 实战卡片开发 03 在前面两张&#xff0c;我们基本掌握了卡片的使用流程&#xff0c;本章节就通过一个实战来加强对卡片使用的理解。 要完成的案例 新建项目和新建服务卡片 设置沉浸式 entry/src/main/ets/entryability/EntryAbility.ets 首页显示轮播图数据 1…

基于 PyTorch 从零手搓一个GPT Transformer 对话大模型

一、从零手实现 GPT Transformer 模型架构 近年来&#xff0c;大模型的发展势头迅猛&#xff0c;成为了人工智能领域的研究热点。大模型以其强大的语言理解和生成能力&#xff0c;在自然语言处理、机器翻译、文本生成等多个领域取得了显著的成果。但这些都离不开其背后的核心架…

三、整数规划

整数规划 建立逻辑变量来整合多个方案。如0-1变量&#xff08;要说明0和1分别表示什么&#xff09;见P79求解纯整数规划的分支定界法&#xff1a; 求解整数规划的松弛问题的最优解若松弛问题的最优解满足整数要求&#xff0c;则得到整数规划的最优解&#xff0c;否则转下一步任…

Docker了解

Docker是一种容器化技术&#xff0c;它可以将应用程序和其依赖项打包到一个独立的、可移植的容器中&#xff0c;以便在不同的环境中运行。Docker基于Linux操作系统的容器化技术&#xff0c;可以提供更轻量、更快速、更灵活、更一致的应用部署和管理方式。 Docker的基本概念包括…

stm32以太网接口:MII和RMII

前言 使用stm32和lwip进行网络通信开发时&#xff0c;实现结构如下&#xff1a; 而MII和RMII就是stm32与PHY芯片之间的通信接口&#xff0c;类似于I2C、UART等。 stm32以太网模块有专用的DMA控制器&#xff0c;通过AHB接口将以太网内核和存储器相连。 数据发送时&#xff0c;…

【GESP】C++一级真题练习(202312)luogu-B3921,小杨的考试

GESP一级真题练习。为2023年12月一级认证真题。逻辑计算问题。 题目题解详见&#xff1a;【GESP】C一级真题练习(202312)luogu-B3921&#xff0c;小杨的考试 | OneCoder 【GESP】C一级真题练习(202312)luogu-B3921&#xff0c;小杨的考试 | OneCoderGESP一级真题练习。为2023…