【业务功能篇59】Springboot + Spring Security 权限管理 【下篇】

news/2024/4/26 21:55:23/文章来源:https://blog.csdn.net/studyday1/article/details/131992712

UserDetails接口定义了以下方法:

  1. getAuthorities(): 返回用户被授予的权限集合。这个方法返回的是一个集合类型,其中每个元素都是一个GrantedAuthority对象,表示用户被授予的权限。
  2. getPassword(): 返回用户的密码。这个方法返回的是一个字符串类型,表示用户的密码。
  3. getUsername(): 返回用户的用户名。这个方法返回的是一个字符串类型,表示用户的用户名。
  4. isAccountNonExpired(): 返回一个布尔值,表示用户的账户是否未过期。
  5. isAccountNonLocked(): 返回一个布尔值,表示用户的账户是否未锁定。
  6. isCredentialsNonExpired(): 返回一个布尔值,表示用户的凭证(如密码)是否未过期。
  7. isEnabled(): 返回一个布尔值,表示用户是否已激活。

第三步 测试

访问登录地址 http://localhost:8080/login ,输入用户名密码

image.png

登录失败,后台报错

 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

报错原因

  • Spring Security中密码的存储格式是“{id}…………”.前面的id是加密方式,id可以是bcrypt、sha256等,后面跟着的是加密后的密码.也就是说,程序拿到传过来的密码的时候,会首先查找被“{”和“}”包括起来的id,来确定后面的密码是被怎么样加密的,如果找不到就认为id是null.

如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。就可以正常登录了, 例如

image.png

(3) BCryptPasswordEncoder 密码加密存储

1. BCryptPasswordEncoder 介绍

在实际的项目中,为了保护密码的安全,我们通常不会将密码以明文的形式存储在数据库中。通常,我们使用SpringSecurity提供的BCryptPasswordEncoder来进行加密。

BCryptPasswordEncoder是Spring Security提供的一个PasswordEncoder实现类,它使用了bcrypt算法对密码进行加密和解密。

2. 常用方法测试

BCryptPasswordEncoder主要有以下方法:

  • encode(CharSequence rawPassword):对原始密码进行加密处理,并返回加密后的密码字符串。
  • matches(CharSequence rawPassword, String encodedPassword):对比原始密码和加密后的密码是否匹配。rawPassword为原始密码,encodedPassword为从数据库或其他地方获取的已经加密的密码字符串,如果匹配则返回true,否则返回false。
 @Autowiredprivate PasswordEncoder passwordEncoder;@Testpublic void testBcryp(){String e1 = passwordEncoder.encode("123456");String e2 = passwordEncoder.encode("123456");System.out.println(e1);System.out.println(e2);System.out.println(e1.equals(e2));//$2a$10$0CS95XYw7GyDQNXq6FO7FuWDHR4yLTVyFXgQICjgTddWIG9OJ6isyboolean b = passwordEncoder.matches("123456","$2a$10$0CS95XYw7GyDQNXq6FO7FuWDHR4yLTVyFXgQICjgTddWIG9OJ6isy");System.out.println("=============== " + b);}

BCryptPasswordEncoder使用随机盐值对密码进行加密,每次加密的结果都不同,即使相同的原始密码,加密后得到的字符串也是不同的。这种随机性增加了密码的安全性,防止了攻击者通过破解一个用户密码的方式,来破解其他用户的密码。

3.引入 BCryptPasswordEncoder

我们只需要将BCryptPasswordEncoder对象注入到Spring容器中,SpringSecurity就会使用该PasswordEncoder来验证密码。

为了配置SpringSecurity,我们可以定义一个继承自WebSecurityConfigurerAdapter的配置类。

 @Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}}

修改数据库的明文密码为加密后的密码, 测试一下

image.png

(4) 自定义登录接口

我们需要自定义一个登陆接口,并让SpringSecurity不要对该接口进行登录验证,以允许未登录用户访问。

在该接口中,我们使用AuthenticationManager的authenticate方法进行用户认证,需要在SecurityConfig中配置将AuthenticationManager注入到容器中。

如果认证成功,则需要生成一个jwt并将其放入响应中返回。为了让用户在下次请求时能够通过jwt识别出具体的用户,我们需要将用户信息存储在redis中,可以将用户id作为key。

当需要自定义登录接口时,可以按照以下步骤进行:

  1. 创建一个新的登录接口,例如LoginController , 用于接收用户的登录信息。
 @RestControllerpublic class LoginController {@Autowiredprivate LoginService loginService;@PostMapping("/user/login")public ResponseResult login(@RequestBody SysUser user){//登录return loginService.login(user);}}
  1. 创建LoginService和其实现类 LoginServiceImpl, 登录操作主要的实现逻辑都在实现类中
 public interface LoginService {ResponseResult login(SysUser sysUser);}@Servicepublic class LoginServiceImpl implements LoginService {@Overridepublic ResponseResult login(SysUser sysUser) {//1.调用AuthenticationManager的 authenticate方法,进行用户认证。//2.如果认证没有通过,给出错误提示//3.如果认证通过,使用userId生成一个JWT,并将其保存到 ResponseResult对象中返回//4.将用户信息存储在Redis中,在下一次请求时能够识别出用户,userid作为keyreturn null;}}
  1. 配置SecurityConfig 在SecurityConfig中添加一个配置,将自定义登录接口添加到Spring Security中,并设置为放行。
 @Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}/*** 注入 AuthenticationManager,供外部类使用*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//该方法用于配置 HTTP 请求的安全处理@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不会创建会话,每个请求都将被视为独立的请求。.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()//定义请求授权规则.authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();}}

后面我们再去详细说明一下configure方法中的细节.

  1. 回到loginService的login方法,补全剩余步骤
 @Servicepublic class LoginServiceImpl implements LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(SysUser sysUser) {//1.调用AuthenticationManager的 authenticate方法,进行用户认证。//1.1 需要传入一个Authentication对象的实现,该对象包含用户信息Authentication usernamePasswordAuthenticationToken =new UsernamePasswordAuthenticationToken(sysUser.getUserName(),sysUser.getPassword());Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);//2.如果认证没有通过,给出错误提示if(Objects.isNull(authentication)){throw new RuntimeException("登录失败");}//3.如果认证通过,使用userId生成一个JWT,并将其保存到 ResponseResult对象中返回//3.1 获取经过身份验证的用户的主体信息LoginUser loginUser = (LoginUser) authentication.getPrincipal();//3.2 获取到userID 生成JWTString userId = loginUser.getSysUser().getUserId().toString();String jwt = JwtUtil.createJWT(userId);//4.将用户信息存储在Redis中,在下一次请求时能够识别出用户,userid作为keyredisCache.setCacheObject("login:"+userId,loginUser);//5.封装ResponseResult,并返回Map<String,String> map = new HashMap<>();map.put("token",jwt);return new ResponseResult(200,"登录成功",map);}}
(5) 使用postman测试

image.png

(6) 实现认证过滤器

当用户再次发送请求的时候,要进行校验,用户会携带登录时生成的JWT,所以我们需要自定义一个Jwt认证过滤器

image.png

  • 获取token
  • 解析token获取其中的userid login:+userId
  • 从redis中获取用户信息
  • 存入SecurityContextHolder

    SecurityContextHolder 记录如下信息:当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色或权限等等。

    经过自定义认证过滤器过滤后的用户信息会被保存到SecurityContextHolder中,后面的过滤器会从SecurityContextHolder中获取用户信息.

操作步骤如下

  1. 自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid

    自定义过滤器要去继承OncePerRequestFilter,OncePerRequestFilter 旨在简化过滤器的编写,并确保每个请求只被过滤一次,避免多次过滤的问题。

 /*** 自定义认证过滤器,用来校验用户请求中携带的Token* @date 2023/4/25**/@Componentpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;/*** 封装过滤器的执行逻辑* @param request* @param response* @param filterChain*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//1.从请求头中获取tokenString token = request.getHeader("token");//2.判断token是否为空,为空直接放行if(!StringUtils.hasText(token)){//放行filterChain.doFilter(request,response);//return的作用是返回响应的时候,避免走下面的逻辑return;}//3.解析TokenString userId;try {Claims claims = JwtUtil.parseJWT(token);userId = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("非法token");}//4.从redis中获取用户信息String redisKey = "login:" + userId;LoginUser loginUser = redisCache.getCacheObject(redisKey);if(Objects.isNull(loginUser)){throw new RuntimeException("用户未登录");}//5.将用户新保存到SecurityContextHolder,以便后续的访问控制和授权操作使用。UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//6.放行filterChain.doFilter(request,response);}}

UsernamePasswordAuthenticationToken 三个参数的构造方法:

  • principal:表示认证请求的主体,通常是一个用户名或者其他识别主体的信息。
  • credentials:表示认证请求的凭据,通常是密码或者其他证明主体身份的信息。
  • authorities: 权限信息

将Token检验过滤器 添加到过滤器链中

 @Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;/*** 注入 AuthenticationManager,供外部类使用*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//该方法用于配置 HTTP 请求的安全处理@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不会创建会话,每个请求都将被视为独立的请求。.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()//定义请求授权规则.authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//将自定义认证过滤器,添加到过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}}

使用postman进行测试

image.png

(7) 实现退出功能

定义一个登出接口,删除redis中对应的用户数据即可。

为什么不需要清除SecurityContextHolder中的数据

在退出登录时,如果使用 JWT 进行认证,并将 JWT 保存在 Redis 中,需要清除 Redis 中的 JWT 数据。由于 JWT 是无状态的,它本身不会与 Spring Security 的认证信息产生关联,因此在退出登录时,不需要清除 SecurityContextHolder 中的认证信息。

 @RestControllerpublic class LoginController {@GetMapping("/user/logout")public ResponseResult logout(){//登录return loginService.logout();}}public interface LoginService {ResponseResult login(SysUser sysUser);ResponseResult logout();}@Servicepublic class LoginServiceImpl implements LoginService {@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult logout() {//获取当前用户的认证信息UsernamePasswordAuthenticationToken authenticationToken =(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();if(Objects.isNull(authenticationToken)){throw new RuntimeException("获取用户认证信息失败,请重新登录!");}LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal();Long userId = loginUser.getSysUser().getUserId();//删除redis中的用户信息redisCache.deleteObject("login:" + userId);return new ResponseResult(200,"注销成功");}}

测试

image.png

4.2.4 授权

4.2.4.1 什么是授权

授权是指在认证通过之后,根据用户的身份和角色,确定用户是否有权执行某项操作或访问某个资源的过程。

在应用程序中,授权通常是通过访问控制机制来实现的,例如基于角色的访问控制(Role-Based Access Control,RBAC)

4.2.4.2 Spring Security 授权基本流程

Spring Security 的授权基本流程如下:

  1. 进行认证操作,会生成一个 Authentication 对象
  2. 确定了用户的身份和角色之后,可以通过 Spring Security 提供的注解进行授权操作。
  3. 如果授权通过,则可以执行相关操作。

其中第一步操作 将权限信息保存到Authentication,有两个地方与保存权限有关

 @Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//TODO 查询用户权限信息//方法的返回值是UserDetails类型,需要返回自定义的实现类,并且将user信息通过构造方法传入return new LoginUser(sysUser);}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//TODO 获取权限信息封装到 AuthenticationUsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//6.放行filterChain.doFilter(request,response);}

4.2.4.2 SpringSecurity授权实现

(1) 设置资源访问所需要的权限

在security中添加注解 @EnableGlobalMethodSecurity

 @EnableGlobalMethodSecurity(prePostEnabled = true)@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {

@EnableGlobalMethodSecurity(prePostEnabled = true) 是 Spring Security 提供的一个注解,用于启用全局方法级别的安全控制,在使用 Spring Security 进行方法级别的授权控制时,需要使用该注解来启用相关功能。

其中,prePostEnabled = true 表示开启 Spring Security 的方法级别安全控制。pre 表示在方法执行前进行授权校验,post 表示在方法执行后进行授权校验。

在HelloController中添加 @PreAuthorize(“hasAuthority(‘test’)”) 注解

 @RestControllerpublic class HelloController {@RequestMapping("/hello")@PreAuthorize("hasAuthority('test')")public String hello(){return "hello";}}

@PreAuthorize("hasAuthority('test')") 是 Spring Security 提供的一个注解,用于在方法执行前进行权限校验。它的作用是检查当前登录用户是否具有指定的权限,如果有,则允许执行该方法,否则抛出 AccessDeniedException 异常,阻止方法执行。

hasAuthority() 方法用于检查用户是否具有指定的权限

hasAuthority('test') 表示检查当前用户是否具有名为 test 的权限

@PreAuthorize 注解是在方法执行前进行权限校验的,因此如果当前用户不具有指定的权限,该方法将不会被执行。如果需要在方法执行后进行权限校验,可以使用 @PostAuthorize 注解。

(2) 封装权限信息

第一步 在UserDetailsServiceImpl中 ,根据用户查询权限信息,添加到LoginUser中

 @Servicepublic class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询用户信息LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();wrapper.eq(SysUser::getUserName,username);SysUser user = userMapper.selectOne(wrapper);//如果查询不到数据,抛出异常 给出提示if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误");}//TODO 根据用户查询权限信息,添加到LoginUser中,这里的权限信息我们写死,封装到list集合ArrayList<String> list = new ArrayList<>(Arrays.asList("test"));//方法的返回值是 UserDetails接口类型,需要返回自定义的实现类return new LoginUser(user,list);}}

第二步 由于LoginUser中还有这个构造函数,所以我们要修改一下LoginUser

 /* LoginUser *///存储权限信息集合private List<String> permissions;public LoginUser(SysUser user, ArrayList<String> permissions) {this.sysUser = user;this.permissions = permissions;}

第三步 如果SpringSecurity想要获取用户权限信息,其实最终要调用 getAuthorities()方法,所以要在这个方法中将查询到的权限信息进行转换,转换另一个List集合,其中保存的数据类型是 GrantedAuthority 类型.这是一个接口,我们用它下面的这个实现

image.png

 package com.mashibing.springsecurity_example.entity;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.ArrayList;import java.util.Collection;import java.util.List;import java.util.stream.Collectors;/*** @date 2023/4/24**/@Datapublic class LoginUser implements UserDetails {private SysUser sysUser;//存储权限信息集合private List<String> permissions;public LoginUser(SysUser user, ArrayList<String> permissions) {this.sysUser = user;this.permissions = permissions;}/***  用于获取用户被授予的权限,可以用于实现访问控制。*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {//将permissions集合中的String类型权限信息,转换为SimpleGrantedAuthority类型//        List<SimpleGrantedAuthority> authorities = new ArrayList<>();//        for (String permission : permissions) {//            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);//            authorities.add(simpleGrantedAuthority);//        }//1.8 语法List<SimpleGrantedAuthority> authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());return authorities;}}

第四步 对上面的代码进行优化, 将权限的集合提取到方法外,除第一次调用需要正在查询以外,后面判断只要authorities集合不为空,就直接返回

 @Datapublic class LoginUser implements UserDetails {private SysUser sysUser;public LoginUser() {}public LoginUser(SysUser sysUser) {this.sysUser = sysUser;}//存储权限信息集合private List<String> permissions;public LoginUser(SysUser user, ArrayList<String> permissions) {this.sysUser = user;this.permissions = permissions;}//authorities集合不需要序列化,只需要序列化permissions集合即可@JSONField(serialize = false)private List<SimpleGrantedAuthority> authorities;/***  用于获取用户被授予的权限,可以用于实现访问控制。*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {//将permissions集合中的String类型权限信息,转换为SimpleGrantedAuthority类型//        List<SimpleGrantedAuthority> authorities = new ArrayList<>();//        for (String permission : permissions) {//            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);//            authorities.add(simpleGrantedAuthority);//        }if(authorities != null){return authorities;}//1.8 语法authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());return authorities;}}

第五部分 在 JwtAuthenticationTokenFilter认证过滤器中, 将权限信息保存到 SecurityContextHolder

 //TODO 5.将用户保存到SecurityContextHolder,以便后续的访问控制和授权操作使用。UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);

第六步 debug 测试一下

(3) 根据RBAC权限模型创建表

1. RBAC权限模型

  • RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

image.png

2. 创建RBAC模型所需的表

 CREATE TABLE `sys_menu` (`menu_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',`menu_name` VARCHAR(50) NOT NULL COMMENT '菜单名称',`path` VARCHAR(200) DEFAULT '' COMMENT '路由地址',`component` VARCHAR(255) DEFAULT NULL COMMENT '组件路径',`visible` CHAR(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',`status` CHAR(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',`perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识',`icon` VARCHAR(100) DEFAULT '#' COMMENT '菜单图标',`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',`remark` VARCHAR(500) DEFAULT '' COMMENT '备注',PRIMARY KEY (`menu_id`) USING BTREE) ENGINE=INNODB AUTO_INCREMENT=2068 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='菜单权限表'CREATE TABLE `sys_role` (`role_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',`role_name` VARCHAR(30) NOT NULL COMMENT '角色名称',`role_key` VARCHAR(100) NOT NULL COMMENT '角色权限字符串',`status` CHAR(1) NOT NULL COMMENT '角色状态(0正常 1停用)',`del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`role_id`) USING BTREE) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='角色信息表'CREATE TABLE `sys_role_menu` (`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',PRIMARY KEY (`role_id`,`menu_id`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;CREATE TABLE `sys_user` (`user_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` VARCHAR(30) NOT NULL COMMENT '用户昵称',`password` VARCHAR(100) DEFAULT '' COMMENT '密码',`phonenumber` VARCHAR(11) DEFAULT '' COMMENT '手机号码',`sex` CHAR(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',`status` CHAR(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',PRIMARY KEY (`user_id`) USING BTREE) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户信息表'CREATE TABLE `sys_user_role` (`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',PRIMARY KEY (`user_id`,`role_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3. 查询当前有用户所拥有的菜单权限

 SELECT sm.permsFROM sys_user su LEFT JOIN sys_user_role sur ON su.user_id = sur.user_idLEFT JOIN sys_role sr ON sur.role_id = sr.role_idLEFT JOIN sys_role_menu srm ON sr.role_id = srm.role_idLEFT JOIN sys_menu sm ON srm.menu_id = sm.menu_idWHERE su.user_id = 2

4. 创建菜单实体

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@TableName(value = "sys_menu")
public class Menu implements Serializable {@TableIdprivate Long id;//菜单名private String menuName;//路由地址private String path;//组件路径private String component;//菜单状态 (0 显示, 1隐藏)private String visible;//菜单状态 (0 正常, 1 停用)private String status;//权限标识private String perms;//菜单图标private String icon;private String createBy;private String updateBy;private Date updateTime;private Date createTime;private String remark;
}
(4) 从数据库获取权限信息

我们要做的就是根据用户id去查询到其所对应的菜单权限信息即可

1.mapper编写

 /*** @date 2023/4/26**/public interface MenuMapper extends BaseMapper<Menu> {List<String> selectPermsByUserId(Long id);}
 SELECT DISTINCT sm.permsFROM sys_user_role sur LEFT JOIN sys_role sr ON sur.role_id = sr.role_idLEFT JOIN sys_role_menu srm ON sr.role_id = srm.role_idLEFT JOIN sys_menu sm ON srm.menu_id = sm.menu_idWHERE user_id = #{userid}AND sr.status = 0AND sm.status = 0
 <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.mashibing.springsecurity_example.mapper.MenuMapper"><select id="selectPermsByUserId" resultType="java.lang.String">SELECT DISTINCT sm.permsFROM sys_user_role sur LEFT JOIN sys_role sr ON sur.role_id = sr.role_idLEFT JOIN sys_role_menu srm ON sr.role_id = srm.role_idLEFT JOIN sys_menu sm ON srm.menu_id = sm.menu_idWHERE user_id = #{userid}AND sr.status = 0AND sm.status = 0</select></mapper>

在application.yml中配置mapperXML文件的位置

 spring:datasource:url: jdbc:mysql://localhost:3306/test_security?characterEncoding=utf-8&serverTimezone=UTCusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverredis:host: localhostport: 6379mybatis-plus:mapper-locations: classpath*:/mapper/**/*.xml 

2.service编写

UserDetailsServiceImpl中去调用mapper的方法查询权限信息, 然后封装到LoginUser对象中.

 @Servicepublic class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate MenuMapper menuMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询用户信息LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();wrapper.eq(SysUser::getUserName,username);SysUser user = userMapper.selectOne(wrapper);//如果查询不到数据,抛出异常 给出提示if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误");}//TODO 根据用户查询权限信息,添加到LoginUser中,这里的权限信息我们写死,封装到list集合//        ArrayList<String> list = new ArrayList<>(Arrays.asList("test"));List<String> list = menuMapper.selectPermsByUserId(user.getUserId());//方法的返回值是 UserDetails接口类型,需要返回自定义的实现类return new LoginUser(user,list);}}

测试,用普通用户去测试一下

 @RestControllerpublic class HelloController {//拥有system:user:list权限才能访问@RequestMapping("/hello")@PreAuthorize("hasAuthority('system:user:list')")public String hello(){return "hello";}//拥有system:role:list 才能访问@RequestMapping("/ok")@PreAuthorize("hasAuthority('system:role:list')")public String ok(){return "ok";}}

4.4.5 SpringSecurity异常处理

除了保护应用程序中受保护资源的访问,我们还希望在认证失败或授权失败时,能够返回与应用程序其他接口相同的 JSON 格式响应,以便前端能够统一处理。

4.4.5.1 ExceptionTranslationFilter介绍

image.png

ExceptionTranslationFilter 是 Spring Security 框架中的一个关键过滤器,用于处理请求过程中抛出的异常,并将其转化为合适的响应。它的主要作用是保护应用程序中受保护资源的访问,并根据用户的身份进行适当的响应。

当 Spring Security 抛出异常时,ExceptionTranslationFilter 将会捕获该异常并根据异常类型去判断是认证失败还是授权失败出现的异常。然后根据 Spring Security 的配置进行处理。

  • 如果是认证过程中出现的异常会被封装成 AuthenticationException , 然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
  • 如果是授权过程中出现的异常会被封装成 AccessDeniedException , 然后调用AccessDeniedHandler对象的方法去进行异常处理。

4.4.5.2 认证过程中的异常处理

AuthenticationEntryPoint 是 Spring Security 中用于处理未经身份验证的用户访问受保护资源时的异常的接口。

**通过实现 **AuthenticationEntryPoint 接口,我们可以自定义未经身份验证的用户访问需要认证的资源时应该返回的响应。

 /*** 自定义认证过程异常处理* @date 2023/4/26**/@Componentpublic class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");String json = JSON.toJSONString(result);WebUtils.renderString(response,json);}}

4.4.5.3 授权过程中的异常处理

**在 Spring Security 中,当用户请求某个受保护的资源,但是由于权限不足或其他原因被拒绝访问时,Spring Security 会调用 **AccessDeniedHandler 来处理这种情况。

**通过自定义实现 **AccessDeniedHandler 接口,并覆盖 handle 方法,我们可以自定义处理用户被拒绝访问时应该返回的响应。

 /*** 自定义处理授权过程中的异常* @date 2023/4/26**/@Componentpublic class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response,AccessDeniedException accessDeniedException) throws IOException, ServletException {ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),"权限不足,禁止访问");String json = JSON.toJSONString(result);WebUtils.renderString(response,json);}}

4.4.5.4 配置SpringSecurity

  1. 先注入对应的处理器
 @Autowiredprivate AuthenticationEntryPoint authenticationEntryPoint;@Autowiredprivate AccessDeniedHandler accessDeniedHandler;
  1. 然后使用HttpSecurity对象的方法去进行配置
 //配置异常处理器http.exceptionHandling()//配置认证失败处理器.authenticationEntryPoint(authenticationEntryPoint)//配置授权失败处理器.accessDeniedHandler(accessDeniedHandler);

测试一下

4.4.6 跨域解决方案CORS

4.4.6.1 什么是跨域 ?

首先一个url是由:协议、域名、端口 三部分组成。(一般端口默认80)
如:https://mashibing.com:80

跨域是指通过JS在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,只要****协议、域名、端口有任何一个不同,都被当作是不同的域,浏览器就不允许跨域请求。

  • 跨域的几种常见情

image.png

如果跨域调用,会出现如下错误:

image.png

 has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

翻译过来就是:已被CORS策略阻止:对请求的响应未通过访问控制检查 , 这就是没有配置相关的跨域参数,是不能访问这个接口的

由于我们采用的是前后端分离的编程方式,前端和后端必定存在跨域问题。解决跨域问 题可以采用CORS

4.4.6.2 跨域产生原因?

(1) 出于浏览器的同源策略限制

所谓同源(即在同一个域)就是两个页面具有相同的协议(protocol)、主机(host)和端口号(port)。才可以互相访问

否则只要有一个不同,是不能访问的。

image.png

同源策略(Same Orgin Policy)是一种约定,它是浏览器核心也最基本的安全功能,它会阻止一个域的js脚本和另外一个域的内容进行交互,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。

(2) 跨站脚本攻击(XSS)

image.png

(3) 跨站请求伪造 (CSRF)

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

image.png

总结: XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

非同源会出现的限制

  • 无法读取非同源网页的cookie、localstorage等
  • 无法接触非同源网页的DOM和js对象
  • 无法向非同源地址发送Ajax请求

4.4.6.3 如何解决跨域问题

为了安全起见,浏览器在使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略。否则,这将被视为跨域请求,并且默认情况下将被禁止。同源策略要求协议、域名和端口号必须完全相同,以便进行正常通信。

在前后端分离的项目中,前端项目和后端项目通常不属于同一源,因此必然存在跨域请求的问题。因此,我们需要对其进行处理,以便前端能够进行跨域请求。

(1) CORS介绍

CORS(Cross-Origin Resource Sharing)即跨域资源共享,是一种用于处理跨域请求的机制。它允许浏览器向跨域服务器发送XMLHttpRequest请求,以便在不违反同源策略的情况下获取服务器上的资源。

image.png

CORS的实现方式主要是通过HTTP头部来实现的,浏览器会在请求中添加一些自定义的HTTP头部,告诉服务器请求的来源、目标地址等信息。服务器在接收到请求后,会根据请求头中的信息来判断是否允许跨域请求,并在响应头中添加一些自定义的HTTP头部,告诉浏览器是否允许请求、允许哪些HTTP方法、允许哪些HTTP头部等信息。

在响应头中添加以下字段,可以解决跨域问题:

  • access-control-allow-origin : 该字段是必须的。它的值要么是请求时 Origin字段的值,要么是一个 *,表示接受任意域名的请求。
  • access-control-allow-credentials : 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为 true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送Cookie,删除该字段即可
  • Access-Control-Allow-Methods : 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

其实最重要的就是 access-control-allow-origin 字段,添加一个 * ,允许所有的域都能访问

(2) 配置SpringBoot的允许跨域

在SpringBoot项目中只需要编写一个配置类使其实现WebMvcConfigurer接口并重写其addCorsMappings方法即可。

 @Configurationpublic class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}}

**你也可以通过使用 **@CrossOrigin 注解来解决跨域问题。例如:

 @RestControllerpublic class MyController {@CrossOrigin(origins = "http://localhost:8080")@GetMapping("/my-endpoint")public String myEndpoint() {// ...}}

**这里 **@CrossOrigin 注解的 origins 参数指定了允许访问该接口的域名。在上面的例子中,只有来自 http://localhost:8080 域名的请求才能访问 myEndpoint 接口。

(3) 配置SpringSecurity允许跨域

由于我们的资源都会受到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问。

 //该方法用于配置 HTTP 请求的安全处理@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不会创建会话,每个请求都将被视为独立的请求。.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()//定义请求授权规则.authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//将自定义认证过滤器,添加到过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//配置异常处理器http.exceptionHandling()//配置认证失败处理器.authenticationEntryPoint(authenticationEntryPoint)//配置授权失败处理器.accessDeniedHandler(accessDeniedHandler);//允许跨域http.cors();}
(4) 前后端联调测试

**首先运行我在资料中给大家提供的前端项目, **注意前端环境要提前配置完成

然后运行后端的项目,进行访问测试即可. 在SpringSecurity中这两行代码注释掉,才能复现跨域请求问题

  http.csrf().disable();http.cors();

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

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

相关文章

打开域名跳转其他网站,官网被黑解决方案(Linux)

某天打开网站&#xff0c;发现进入首页&#xff0c;马上挑战到其他赌博网站。 事不宜迟&#xff0c;不能让客户发现&#xff0c;得马上解决 我的网站跳转到这个域名了 例如网站跳转到 k77.cc 就在你们部署的代码的当前文件夹下面&#xff0c;执行下如下命令 find -type …

【C++】反向迭代器的模拟实现通用(可运用于vector,string,list等模拟容器)

文章目录 前言一、反向迭代器封装&#xff08;reverseiterator&#xff09;1.构造函数1解引用操作.3.->运算符重载4.前置&#xff0c;后置5.前置--&#xff0c;后置--6.不等号运算符重载7.完整代码 二、rbegin&#xff08;&#xff09;以及rend&#xff08;&#xff09;1.rb…

CRM如何进行数据分析?有什么用?

什么是CRM数据分析软件&#xff1f;CRM数据分析软件可以对数据进行挖掘、统计和分析&#xff0c;帮助企业从大量的客户数据中提取有价值的信息&#xff0c;分析数据背后的含义&#xff0c;从而帮助企业更好地运营的一种工具。 1、提高客户满意度 CRM数据分析软件可以通过对客户…

Java的第十五篇文章——网络编程(后期再学一遍)

目录 学习目的 1. 对象的序列化 1.1 ObjectOutputStream 对象的序列化 1.2 ObjectInputStream 对象的反序列化 2. 软件结构 2.1 网络通信协议 2.1.1 TCP/IP协议参考模型 2.1.2 TCP与UDP协议 2.2 网络编程三要素 2.3 端口号 3. InetAddress类 4. Socket 5. TCP网络…

前端调用合约如何避免出现transaction fail

前言&#xff1a; 作为开发&#xff0c;你一定经历过调用合约的时候发现 gas fee 超出限制&#xff0c;但是不知道报了什么错。这个时候一般都是触发了require错误合约校验。对于用户来说他不理解为什么一笔交易会花费如此大的gas&#xff0c;那我们作为开发如何尽量避免这种情…

基于注解手写Spring的IOC(上)

一、思路 先要从当前类出发找到对应包下的所有类文件&#xff0c;再从这些类中筛选出类上有MyComponent注解的类&#xff1b;把它们都装入Map中&#xff0c;同时类属性完成MyValue的赋值操作。 二、具体实现 测试类结构&#xff1a; 测试类&#xff1a;myse、mycontor、BigSt…

【Linux】线程互斥 -- 互斥锁 | 死锁 | 线程安全

引入互斥初识锁互斥量mutex锁原理解析 可重入VS线程安全STL中的容器是否是线程安全的? 死锁 引入 我们写一个多线程同时访问一个全局变量的情况(抢票系统)&#xff0c;看看会出什么bug&#xff1a; // 共享资源&#xff0c; 火车票 int tickets 10000; //新线程执行方法 vo…

用友畅捷通T+服务器数据库中了locked勒索病毒怎么办,如何处理解决

计算机技术的发展&#xff0c;也为网络安全埋下隐患&#xff0c;其中勒索病毒攻击已经成为企业和组织面临的严重威胁之一。作为一款被广泛使用的企业资源管理软件&#xff0c;用友畅捷通T系统也成为黑客攻击的目标之一。近期&#xff0c;我们收到很多企业的求助&#xff0c;公司…

Android Studio 的版本控制Git

Android Studio 的版本控制Git。 Git 是最流行的版本控制工具&#xff0c;本文介绍其在安卓开发环境Android Studio下的使用。 本文参考链接是&#xff1a;https://learntodroid.com/how-to-use-git-and-github-in-android-studio/ 一&#xff1a;Android Studio 中设置Git …

Intel RealSense D455(D400系列) Linux-ROS 安装配置(亲测可用)

硬件&#xff1a;Intel RealSense D455 系统&#xff1a;Ubuntu 18.04 Part_1: 安装librealsense SDK2.0 1.1 注册密钥 sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key F6E65AC044F831AC80A06380C8B3A55A6F3EFCDE或者 sudo apt-key adv --keyserver hkp:/…

【RabbitMQ】golang客户端教程1——HelloWorld

一、介绍 本教程假设RabbitMQ已安装并运行在本机上的标准端口&#xff08;5672&#xff09;。如果你使用不同的主机、端口或凭据&#xff0c;则需要调整连接设置。如果你未安装RabbitMQ&#xff0c;可以浏览我上一篇文章Linux系统服务器安装RabbitMQ RabbitMQ是一个消息代理&…

25.10 matlab里面的10中优化方法介绍—— 函数fmincon(matlab程序)

1.简述 关于非线性规划 非线性规划问题是指目标函数或者约束条件中包含非线性函数的规划问题。 前面我们学到的线性规划更多的是理想状况或者说只有在习题中&#xff0c;为了便于我们理解&#xff0c;引导我们进入规划模型的一种情况。相比之下&#xff0c;非线性规划会更加贴近…

联想北京公司研发管理部高级经理周燕龙受邀为第十二届中国PMO大会演讲嘉宾

联想&#xff08;北京&#xff09;有限公司研发管理部高级经理周燕龙先生受邀为由PMO评论主办的2023第十二届中国PMO大会演讲嘉宾&#xff0c;演讲议题&#xff1a;PMO如何助力研发。大会将于8月12-13日在北京举办&#xff0c;敬请关注&#xff01; 议题简要&#xff1a; PMO在…

gitee使用参考

Git代码托管服务 2.1 常用的Git代码托管服务 gitHub&#xff08; 地址&#xff1a;https://github.com/ &#xff09;是一个面向开源及私有软件项目的托管平台&#xff0c;因为只支持Git 作为唯一的版本库格式进行托管&#xff0c;故名gitHub码云&#xff08;地址&#xff1a;…

Docker安装部署ShardingProxy详细教程

&#x1f680; ShardingSphere &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&…

Go 下载安装教程

1. 下载地址&#xff1a;The Go Programming Language (google.cn) 2. 下载安装包 3. 安装 &#xff08;1&#xff09;下一步 &#xff08;2&#xff09;同意 &#xff08;3&#xff09;修改安装路径&#xff0c;如果不修改&#xff0c;直接下一步 更改后&#xff0c;点击下一…

13个ChatGPT类实用AI工具汇总

在ChatGPT爆火后&#xff0c;各种工具如同雨后春笋一般层出不穷。以下汇总了13种ChatGPT类实用工具&#xff0c;可以帮助学习、教学和科研。 01 / ChatGPT for google/ 一个浏览器插件&#xff0c;可搭配现有的搜索引擎来使用 最大化搜索效率&#xff0c;对搜索体验的提升相…

DataStructure--Basic

程序设计数据结构算法 只谈数据结构不谈算法就跟去话剧院看梁山伯与祝英台结果只有梁山伯在演&#xff0c;祝英台生病了没来一样。 本文的所有内容都出自《大话数据结构》这本书中的代码实现部分&#xff0c;建议看书&#xff0c;书中比我本文写的全。 数据结构&#xff0c;直…

2023.07.13力扣6题

931. 下降路径最小和 给你一个 n x n 的 方形 整数数组 matrix &#xff0c;请你找出并返回通过 matrix 的下降路径 的 最小和 。 下降路径可以从第一行中的任何元素开始&#xff0c;并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列&#xff08;即位…

【数据结构】无头+单向+非循环链表(SList)(增、删、查、改)详解

一、链表的概念及结构 1、链表的概念 之前学习的顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xff0c;而链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的&#xff0c;可以实现更加…