当前位置: 首页 > news >正文

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

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();

相关文章:

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

UserDetails接口定义了以下方法&#xff1a; getAuthorities(): 返回用户被授予的权限集合。这个方法返回的是一个集合类型&#xff0c;其中每个元素都是一个GrantedAuthority对象&#xff0c;表示用户被授予的权限。getPassword(): 返回用户的密码。这个方法返回的是一个字符…...

性能优化 - 前端性能监控和性能指标计算方式

性能优化 - 前端性能监控和性能指标计算方式 前言一. 性能指标介绍1.1 单一指标介绍1.2 指标计算① Redirect(重定向耗时)② AppCache(应用程序缓存的DNS解析)③ DNS(DNS解析耗时)④ TCP(TCP连接耗时)⑤ TTFB(请求响应耗时)⑥ Trans(内容传输耗时)⑦ DOM(DOM解析耗时) 1.3 FP(f…...

git stash clear清空本地暂存代码

git stash clear清空本地暂存代码 git stash 或者 git stash list 查看本地暂存的代码。 清除本地暂存的代码修改&#xff1a; git stash clear git回退代码仓库版本_git回退到之前的版本会影响本地代码嘛_zhangphil的博客-CSDN博客git回退代码版本_git回退到之前的版本会影…...

消防应急照明设置要求在炼钢车间电气室的应用

摘 要:文章以GB51309—2018《消防应急照明和疏散指示系统技术标准》为设计依据&#xff0c;结合某炼钢车间转炉项目的设计过程&#xff0c;在炼钢车间电气室的疏散照明和备用照明的设计思路、原则和方法等方面进行阐述。通过选择合理的消防应急疏散照明控制系统及灯具供配电方案…...

element 表单验证 深层验证绑定

直接上代码 :prop 和prop 都可以&#xff0c;vue2和vue3或者是element、elementplus都可以用 <template><div class"page page-table"><section class"page-query-form"><breadcrumb :hasLine"false" /></section&g…...

brew 换镜像网站

在国内,使用brew极慢. 因为它需要访问国外的一些服务器. 解决方法是使用国内的镜像站. 如果是首次安装: curl https://raw.githubusercontent.com/Homebrew/install/master/install.sh > install-brew.sh 然后,在下载的文件中, 修改BREW_REPO为: BREW_REPO"https…...

WIZnet W5500-EVB-Pico 静态IP配置教程(二)

W5500是一款高性价比的 以太网芯片&#xff0c;其全球独一无二的全硬件TCP、IP协议栈专利技术&#xff0c;解决了嵌入式以太网的接入问题&#xff0c;简单易用&#xff0c;安全稳定&#xff0c;是物联网设备的首选解决方案。WIZnet提供完善的配套资料以及实时周到的技术支持服务…...

R语言无法调用stats.dll的问题解决方案[补充]

写在前面 在去年10月份&#xff0c;出过一起关于R语言无法调用stats.dll的问题解决方案,今天&#xff08;你看到后是昨天&#xff09;不知道为什么&#xff0c;安装包&#xff0c;一直安装不了&#xff0c;真的是炸裂了。后面再次把R与Rstuido升级。说实话&#xff0c;我是真不…...

无线蓝牙耳机有什么推荐?怎么选择适合自己的耳机?七款蓝牙耳机分享

随着信息技术的不断发展&#xff0c;蓝牙耳机的不断发展也是必然的&#xff0c;可以说蓝牙耳机在大部分人们的生活中是不可缺少的一部分。那么我们该怎么去挑选出适合我们自己的需求的“蓝”朋友呢&#xff1f; 第一款&#xff1a;南卡小音舱lite2蓝牙耳机 推荐指数&#xff…...

【数据分析专栏之Python篇】四、pandas介绍

前言 在上一篇中我们安装和使用了Numpy。本期我们来学习使用 核心数据分析支持库 Pandas。 一、pandas概述 1.1 pandas 简介 Pandas 是 Python 的 核心数据分析支持库&#xff0c;提供了快速、灵活、明确的数据结构&#xff0c;旨在简单、直观地处理关系型、标记型数据。 …...

《算法竞赛·快冲300题》每日一题:“最小生成树”

《算法竞赛快冲300题》将于2024年出版&#xff0c;是《算法竞赛》的辅助练习册。 所有题目放在自建的OJ New Online Judge。 用C/C、Java、Python三种语言给出代码&#xff0c;以中低档题为主&#xff0c;适合入门、进阶。 文章目录 题目描述题解C代码Java代码Python代码 “ 最…...

mysql的主键选择

一.没有定义主键有什么问题 如果定义了主键&#xff0c;那么InnoDB会使用主键作为聚簇索引如果没有定义主键&#xff0c;那么会使用第一非空的唯一索引&#xff08;NOT NULL and UNIQUE INDEX&#xff09;作为聚簇索引如果既没有主键也找不到合适的非空索引&#xff0c;那么In…...

Eureka 学习笔记1:服务端实例缓存

版本 awsVersion ‘1.11.277’ 缓存类型registryConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>AbstractInstanceRegistry成员变量readWriteCacheMapLoadingCacheResponseCacheImpl成员变量readOnlyCacheMapConcurrentMap<Key, Value>…...

vue : 无法加载文件 C:\Users\86182\AppData\Roaming\npm\vue.ps1,因为在此系统上禁止运行脚本。

windows11&#xff1a; PS E:\VueProjects> vue vue : 无法加载文件 C:\Users\86182\AppData\Roaming\npm\vue.ps1&#xff0c;因为在此系统上禁止运行脚本。有关详细信息&#xff0c;请参阅 https:/ go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policie…...

FLinkCDC读取MySQl时间戳时区相关问题解决汇总

FlinkCDC时间问题timestamp等https://blog.csdn.net/qq_30529079/article/details/127809317 FLinkCDC读取MySQl中的日期问题https://blog.csdn.net/YPeiQi/article/details/130265653 关于flink1.11 flink sql使用cdc时区差8小时问题https://blog.csdn.net/weixin_44762298/…...

第三篇-Tesla P40+CentOS7+CUDA 11.7 部署实践

硬件环境 系统&#xff1a;CentOS-7 CPU: 14C28T 显卡&#xff1a;Tesla P40 24G 准备安装 驱动: 515 CUDA: 11.7 cuDNN: 8.9.2.26 安装依赖 yum clean all yum update yum install -y gcc gcc-c pciutils kernel-devel-$(uname -r) kernel-headers-$(uname -r)查看GPU信息…...

AC+FIT(瘦AP)配置浅谈

FIT ensp实验材料 &#xff1a;pc、路由器、三层交换机、二层交换机、ac、ap 保证连通性&#xff1a; 根据ac与ap设计好的ip配置&#xff0c;使之可以通讯 ac与ap可以实现跨网段管理 1、设置三层交换机的vlan 与vlanif信息 dhcp enable //开启dhcp ip pool forap //…...

【Python】PySpark 数据计算 ② ( RDD#flatMap 方法 | RDD#flatMap 语法 | 代码示例 )

文章目录 一、RDD#flatMap 方法1、RDD#flatMap 方法引入2、解除嵌套3、RDD#flatMap 语法说明 二、代码示例 - RDD#flatMap 方法 一、RDD#flatMap 方法 1、RDD#flatMap 方法引入 RDD#map 方法 可以 将 RDD 中的数据元素 逐个进行处理 , 处理的逻辑 需要用外部 通过 参数传入 map…...

二叉树题目:左叶子之和

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;左叶子之和 出处&#xff1a;404. 左叶子之和 难度 3 级 题目描述 要求 给你二叉树的根结点 root \texttt{ro…...

Spark SQL报错: Task failed while writing rows.

错误 今天运行 Spark 任务时报了一个错误&#xff0c;如下所示&#xff1a; WARN scheduler.TaskSetManager: Lost task 9.0 in stage 3.0 (TID 69, xxx.xxx.xxx.com, executor 3): org.apache.spark.SparkException: Task failed while writing rows.at org.apache.spark.sq…...

Linux系统下U盘打不开: No application is registered as handling this file

简述 系统是之前就安装好使用的Ubuntu14.04&#xff0c;不过由于某些原因只安装到了机械硬盘中&#xff1b;最近新买了一块固态硬盘&#xff0c;所以打算把Ubuntu系统迁移到新的固态硬盘上&#xff1b; 当成功的迁移了系统之后发现其引导有点问题&#xff0c;导致多个系统启动不…...

07 定时器处理非活动连接(上)

07 定时器处理非活动连接&#xff08;上&#xff09; 基础知识 非活跃&#xff0c;是指客户端&#xff08;这里是浏览器&#xff09;与服务器端建立连接后&#xff0c;长时间不交换数据&#xff0c;一直占用服务器端的文件描述符&#xff0c;导致连接资源的浪费。 定时事件&a…...

python——案例四:判断字符串中的元素组成

案例四&#xff1a;判断字符串中的元素组成str"Hello World! 666" print(str.isalnum()) #判读所有的字符都是数字或者是字母 print(str.isalpha()) #判读所有的字符都是字母 print(str.isdigit()) #判读所有的字符都是数字 print(str.islower()) #判读所有的字符都是…...

一起学算法(插入排序篇)

概念&#xff1a; 插入排序&#xff08;inertion Sort&#xff09;一般也被称为直接插入排序&#xff0c;是一种简单的直观的排序算法 工作原理&#xff1a;将待排列元素划分为&#xff08;已排序&#xff09;和&#xff08;未排序&#xff09;两部分&#xff0c;每次从&…...

JVM基础篇-本地方法栈与堆

JVM基础篇-本地方法栈与堆 本地方法栈 什么是本地方法? 本地方法即那些不是由java层面实现的方法&#xff0c;而是由c/c实现交给java层面进行调用&#xff0c;这些方法在java中使用native关键字标识 public native int hashCode()本地方法栈的作用? 为本地方法提供内存空…...

防雷保护区如何划分,防雷分区概念LPZ介绍

在防雷设计中&#xff0c;很重要的一点就是防雷分区的划分&#xff0c;只有先划分好防雷区域等级&#xff0c;才好做出比较好的防雷器设计方案。 因为标准对不同区安装的防雷浪涌保护器要求是不一样的。 那么&#xff0c;防雷保护区是如何划分的呢&#xff1f; 如上图所示&…...

随手笔记——3D−3D:ICP求解

随手笔记——3D−3D&#xff1a;ICP求解 使用 SVD 求解 ICP使用非线性优化来求解 ICP 原理参见 https://blog.csdn.net/jppdss/article/details/131919483 使用 SVD 求解 ICP 使用两幅 RGB-D 图像&#xff0c;通过特征匹配获取两组 3D 点&#xff0c;最后用 ICP 计算它们的位…...

Python调用各大机器翻译API大全

过去的二三年中&#xff0c;我一直关注的是机器翻译API在自动化翻译过程中的应用&#xff0c;包括采用CAT工具和Python编程语言来调用机器翻译API&#xff0c;然后再进行译后编辑&#xff0c;从而达到快速翻译的目的。 然而&#xff0c;我发现随着人工智能的发展&#xff0c;很…...

重生之我要学C++第六天

这篇文章的主要内容是const以及权限问题、static关键字、友元函数和友元类&#xff0c;希望对大家有所帮助&#xff0c;点赞收藏评论支持一下吧&#xff01; 更多优质内容跳转&#xff1a; 专栏&#xff1a;重生之C启程(文章平均质量分93) 目录 const以及权限问题 1.const修饰…...

SpringBoot中ErrorPage(错误页面)的使用--【ErrorPage组件】

SpringBoot系列文章目录 SpringBoot知识范围-学习步骤–【思维导图知识范围】 文章目录 SpringBoot系列文章目录本系列校训 SpringBoot技术很多很多环境及工具&#xff1a;必要的知识深层一些的知识 上效果图在Spring Boot里使用ErrorPage还要注意的是 配套资源作业&#xff…...