SpringSecurity 一文彻底掌握

news2025/2/26 7:14:13

文章目录

  • 前言
  • 一、SpringSecurity Web方案
    • 🍓Test Controller 测试请求控制器
    • 🤣SpringSecurity 基本原理
    • 🌍代码底层流程:重点看三个过滤器
      • FilterSecurityInterceptor 方法级的权限过滤器
      • ExceptionTranslationFilter 异常过滤器
      • UsernamePasswordAuthenticationFilter 对/login 的 POST 请求做拦截
    • 🍇过滤器是如何进行加载的 DelegatingFilterProxy
      • Q:过滤器是如何进行加载的 ?
    • 🍔两个重要接口 UserDetailsService / PasswordEncoder
      • UserDetailsService 接口
      • PasswordEncoder 接口
    • 🍕配置文件配置默认的用户名和密码
    • 💖配置类配置默认用户名和密码
    • 🚑总结适配器模式:WebSecurityConfigurerAdapter
    • 🥩自定义类实现用户名和密码
    • 🍧自定义其他配置,如有改动请移步官方文档
    • 自定义登录页面
    • 🥑基于角色和权限进行访问控制
      • 1、在配置类设置路径请求需要权限
      • 2、在 UserDetailsService 接口设置用户权限
    • 😢自定义403 页面 以及 完整配置类
    • 😘SpringSecurity 注解开发
      • 启动类添加注解
      • @Secured({"ROLE_sale","ROLE_manager"})
      • @PreAuthorize 适合进入方法前的权限验证
      • @PostAuthorize 方法执行后进行校验
      • @PostFilter 对方法的返回数据进行过滤
      • @PreFilter 进入控制器方法之前对数据进行过滤
    • 🎶实现用户注销
    • 🤦‍♂️基于数据库实现 “记住我”
      • 1、创建数据表
      • 2、设置配置类
      • 3、添加一个复选框
      • 4、测试
    • 🥜CSRF 跨站请求伪造
      • 1、开启 CSRF 防护
      • 2、添加表单隐藏域
      • 3、CsrfFilter 源码分析
  • 二、 SpringSecurity 微服务权限方案 :请移步谷粒学院
    • 🥚认证授权过程分析
    • 🍏需要的数据模型: 最基本的五张表
    • 🥩开发环境
  • 三、SpringSecurity源码分析
    • 🍭 认证流程 UsernamePasswordAuthenticationFilter
    • 🍖权限认证流程
    • 🥙认证信息共享
    • 🍤总结认证流程


前言

在这里插入图片描述
在这里插入图片描述


一、SpringSecurity Web方案

在这里插入图片描述

创建一个springBooot工程,引入SpringSecurity场景依赖,然后编写一个测试controller进行请求测试

🍓Test Controller 测试请求控制器

package com.example.springsecurity.controller;


import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("hello")
    public String hello() {
        return "hello security";
    }

    @GetMapping("index")
    public String index() {
        return "hello index";
    }

}

启动boot 访问controller 的 test / hello 路径

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

🤣SpringSecurity 基本原理

在这里插入图片描述

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
ter
org.springframework.security.web.context.SecurityContextPersistenceFilter 
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter 
org.springframework.security.web.session.SessionManagementFilter 
org.springframework.security.web.access.ExceptionTranslationFilter 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

🌍代码底层流程:重点看三个过滤器

在这里插入图片描述

在这里插入图片描述

FilterSecurityInterceptor 方法级的权限过滤器

是一个方法级的权限过滤器, 基本位于过滤链的最底部, 由于是责任链模式,前面处理不了的请求会寻找下一个责任人去处理任务,直到找到能处理的责任为止。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

ExceptionTranslationFilter 异常过滤器

是个异常过滤器,用来处理在认证授权过程中抛出的异常

在这里插入图片描述
在这里插入图片描述

UsernamePasswordAuthenticationFilter 对/login 的 POST 请求做拦截

对/login 的 POST 请求做拦截,校验表单中用户名,密码。就像我们上面提到的 SpringSecurity 为我们提供的登录表单一样

在这里插入图片描述

🍇过滤器是如何进行加载的 DelegatingFilterProxy

自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方
案,可以使用更少的配置来使用 Spring Security。

springBoot 帮我们自动装配了一个过滤器 DelegatingFilterProxy 授权过滤代理类,如果不使用Springboot整合,我们需要从 DelegatingFilterProxy 开始配置 SpringSecurity

在这里插入图片描述

Q:过滤器是如何进行加载的 ?

问 :过滤器是如何进行加载的 ?

: 如果没有SpringBoot 加持要从 DelegatingFilterProxy 开始进行配置,当然使用SpringBoot 整合 Spring Security 也是从 DelegatingFilterProxy 进行自动装载,由于 SpringSecurity 是一系列的过滤器链,所以 DelegatingFilterProxy 也是间接实现了 Filter 接口,然后会从 doFilter 开始,判断一系列的非空,然后拿到一个 WebApplicationContext 对象 WebApplicationContext wac = this.findWebApplicationContext(); 然后调用
this.initDelegate(wac); 初始化方法,来到初始化方法会使用 getBean方法获取到SpringBoot整合 SPringSecurity 的 SecurityFilterAutoConfiguration 自动装配对象,然后从这个自动装配的对象中获取到 beanName private static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain"; 从getBean 方法出来后会获取到一个 FilterChainProxy 对象 Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class); 这里是使用 父类型 进行接收,FilterChainProxy对象也间接实现了Filter 接口,FilterChainProxy中的 doFilter 方法会将所有的过滤器链封装到一个 List 集合中,至此过滤器链加载结束。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

WebSecurity最终会创建FilterChainProxy对象,并作为springSecurityFilterChain返回,

SecurityFilterAutoCOnfiguration自动配置类引入了SecurityAutoConfiguration配置类,

SecurityAutoConfiguration配置类引入了WebSecurityEableConfiguration配置类(需要满足条件),

WebSecurityEnableConfiguration配置类中构造springSecurityFilterChain时,调用了WebSecurity的build方法。

后面的源码就需要deBug了,直接DeBug 走起

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

🍔两个重要接口 UserDetailsService / PasswordEncoder

在这里插入图片描述

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中
账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。
如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下
这个类是系统默认的用户“主体”

UserDetailsService 接口

在这里插入图片描述

// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();

以后我们只需要使用 User 这个实体类即可!

在这里插入图片描述

PasswordEncoder 接口

// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹
配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个
参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
false。默认返回 falsedefault boolean upgradeEncoding(String encodedPassword) {
return false;
}

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析
器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单
向加密。可以通过 strength 控制加密强度,默认 10.

在这里插入图片描述

🍕配置文件配置默认的用户名和密码

在这里插入图片描述

在这里插入图片描述

重启项目,控制台并没有输出密码

在这里插入图片描述
在这里插入图片描述

💖配置类配置默认用户名和密码

编写一个配置类继承 WebSecurityConfigurerAdapter ,Adapter(适配器)可见 WebSecurityConfigurerAdapter是一个适配器模式的类,一会我们跟进去看一下

在这里插入图片描述

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //对密码进行加密
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
    }

    // 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错
    //因为上面 new 出来的对象不归 spring 管理
    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

回到 WebSecurityConfigurerAdapter 抽象适配器类,会有三个重载的方法对我们传入的不同的对象进行适配

在这里插入图片描述

disableLocalConfigureAuthenticationBldr 是默认为false ,只有我们没有重写这个方法时,会对其赋值为 true,一旦我们自定义用户名和密码,进行方法重写disableLocalConfigureAuthenticationBldr 就一直为 flase

在这里插入图片描述

这里是空方法应该是给子类进行实现的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

🚑总结适配器模式:WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapter 是一个抽象适配器,实现了WebSecurityConfigurer 接口,里面的方法 init(初始化) 和 configure(配置),很显然是接口适配器模式,然后 WebSecurityConfigurerAdapter 又重载了三个方法 configure ,根据不同的参数进行对用户配置的适配,其中一个传入 AuthenticationManagerBuilder 建造者对象进行自定义的用户名和密码进行设置,这样就覆盖掉了抽象适配器的 configure 方法,使用我们自己定义的用户名和密码进行登录。至此完成了用户和密码的自定义配置,使用接口适配器对用户传入的参数进行适配,和建造者模式 让用户自定义用户和密码。

🥩自定义类实现用户名和密码

SpringSeucrity 在认证过程中,首先会去找配置文件和配置类,如果配置文件和配置类中有用户名和密码,就会使用这里面的用密码和密码进行配置,如果都没有找到,就会去找一个接口 UserDetailsService ,通过这个接口的实现类找到用户自定义的用户名和密码,然后进行认证过程

在这里插入图片描述

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义实现用户名和密码
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    // 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }
}

从数据库查用户名和密码,只需要在 MyUserDetailsService 的 loadUserByUsername 添加相应的业务逻辑就行了

先把数据写死测试一下

在这里插入图片描述

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    //@Autowired
    //private UsersMapper usersMapper;




    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<GrantedAuthority> auths =
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
        return new User("jack",
                new BCryptPasswordEncoder().encode("123"),auths);
    }

}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UsersMapper usersMapper;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //调用usersMapper方法,根据用户名查询数据库
        QueryWrapper<Users> wrapper = new QueryWrapper();
        // where username=?
        wrapper.eq("username",username);
        Users users = usersMapper.selectOne(wrapper);
        //判断
        if(users == null) {//数据库没有用户名,认证失败
            throw  new UsernameNotFoundException("用户名不存在!");
        }


        List<GrantedAuthority> auths =
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
        //从查询数据库返回users对象,得到用户名和密码,返回
        return new User(users.getUsername(),
                new BCryptPasswordEncoder().encode(users.getPassword()),auths);
    }
}

🍧自定义其他配置,如有改动请移步官方文档

在这里插入图片描述

自定义登录页面

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

package com.example.springsecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

import javax.sql.DataSource;

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    //注入数据源
    @Autowired
    private DataSource dataSource;
    //配置对象
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义实现用户名和密码
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    // 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()   //自定义自己编写的登录页面
                .loginPage("/login.html")  //登录页面设置
                .loginProcessingUrl("/user/login")   //登录访问路径
                .defaultSuccessUrl("/success.html").permitAll()  //登录成功之后,跳转路径
                .failureUrl("/unauth.html")//登陆失败跳转页面
        .and().authorizeRequests() //对哪些请求进行验证
        .antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
        .anyRequest().authenticated()//除了上面的  "/","/test/hello","/user/login" 其他请求都需要进行验证  /** 才表示所有请求
        .and().csrf().disable();  //关闭csrf防护
    }
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

🥑基于角色和权限进行访问控制

在这里插入图片描述

1、在配置类设置路径请求需要权限

在这里插入图片描述

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()   //自定义自己编写的登录页面
                .loginPage("/login.html")  //登录页面设置
                .loginProcessingUrl("/user/login")   //登录访问路径
                .defaultSuccessUrl("/success.html").permitAll()  //登录成功之后,跳转路径
                .failureUrl("/unauth.html")//登陆失败跳转页面
        .and().authorizeRequests() //对哪些请求进行验证
        .antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
                //当前登录用户,只有具有admins权限才可以访问这个路径
                //1 hasAuthority方法  当前用户要用有 admins 权限才能访问
//                 .antMatchers("/test/index").hasAuthority("admins")
                //2 hasAnyAuthority方法  当前用户要用有 任 一 权限才能访问
                 .antMatchers("/test/index").hasAnyAuthority("admins,manager")
                //3 hasRole方法   ROLE_sale  使用这个方法时,给用户设定角色时需要加一个 ROLE_ 的前缀
                .antMatchers("/test/index").hasRole("sale")
        .anyRequest().authenticated()//除了上面的  "/","/test/hello","/user/login" 其他请求都需要进行验证  /** 才表示所有请求
        .and().csrf().disable();  //关闭csrf防护
    }

2、在 UserDetailsService 接口设置用户权限

在这里插入图片描述

上面两个地方互相打配合就可以达到用户授权的效果

😢自定义403 页面 以及 完整配置类

诸君自行搜索

完整配置类

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    //注入数据源
    @Autowired
    private DataSource dataSource;
    //配置对象
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义实现用户名和密码
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    // 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");

        http.formLogin()   //自定义自己编写的登录页面
                .loginPage("/login.html")  //登录页面设置
                .loginProcessingUrl("/user/login")   //登录访问路径
                .defaultSuccessUrl("/success.html").permitAll()  //登录成功之后,跳转路径
        .and().authorizeRequests() //对哪些请求进行验证
        .antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
                //当前登录用户,只有具有admins权限才可以访问  /test/index  这个路径
                //1 hasAuthority方法  当前用户要用有 admins 权限才能访问
                 .antMatchers("/test/index").hasAuthority("admins")
                //2 hasAnyAuthority方法  当前用户要用有 任 一 权限才能访问
//                 .antMatchers("/test/index").hasAnyAuthority("admins,manager")
                //3 hasRole方法   ROLE_sale  使用这个方法时,给用户设定角色时需要加一个 ROLE_ 的前缀
//                .antMatchers("/test/index").hasRole("sale")
        .anyRequest().authenticated()//除了上面的  "/","/test/hello","/user/login" 其他请求都需要进行验证  /** 才表示所有请求
        .and().csrf().disable();  //关闭csrf防护
    }
}

😘SpringSecurity 注解开发

在这里插入图片描述

启动类添加注解

在这里插入图片描述

@Secured({“ROLE_sale”,“ROLE_manager”})

判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“
当前用户拥有 sale 和 manger 角色才能访问这个方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

@PreAuthorize 适合进入方法前的权限验证

启动类开启注解

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

@PostAuthorize 方法执行后进行校验

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

控制台输出,确定了是方法执行后再进行校验

@PostFilter 对方法的返回数据进行过滤

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

只返回了一个用户,说明确实对数据进行了过滤

@PreFilter 进入控制器方法之前对数据进行过滤

在这里插入图片描述

🎶实现用户注销

在这里插入图片描述

目前为止的 配置文件

package com.example.springsecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

import javax.sql.DataSource;

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    //注入数据源
    @Autowired
    private DataSource dataSource;
    //配置对象
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义实现用户名和密码
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    // 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //退出
        http.logout().logoutUrl("/logout").
                logoutSuccessUrl("/test/hello").permitAll();

        //配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");

        http.formLogin()   //自定义自己编写的登录页面
                .loginPage("/login.html")  //登录页面设置
                .loginProcessingUrl("/user/login")   //登录访问路径
                .defaultSuccessUrl("/success.html").permitAll()  //登录成功之后,跳转路径
        .and().authorizeRequests() //对哪些请求进行验证
        .antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
                //当前登录用户,只有具有admins权限才可以访问  /test/index  这个路径
                //1 hasAuthority方法  当前用户要用有 admins 权限才能访问
                 .antMatchers("/test/index").hasAuthority("admins")
                //2 hasAnyAuthority方法  当前用户要用有 任 一 权限才能访问
//                 .antMatchers("/test/index").hasAnyAuthority("admins,manager")
                //3 hasRole方法   ROLE_sale  使用这个方法时,给用户设定角色时需要加一个 ROLE_ 的前缀
//                .antMatchers("/test/index").hasRole("sale")
        .anyRequest().authenticated()//除了上面的  "/","/test/hello","/user/login" 其他请求都需要进行验证  /** 才表示所有请求
        .and().csrf().disable();  //关闭csrf防护
    }
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

🤦‍♂️基于数据库实现 “记住我”

在这里插入图片描述
在这里插入图片描述

1、创建数据表

CREATE TABLE `persistent_logins` (
 `username` varchar(64) NOT NULL,
 `series` varchar(64) NOT NULL,
 `token` varchar(64) NOT NULL,
 `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP,
 PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2、设置配置类

package com.example.springsecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

import javax.sql.DataSource;

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    //注入数据源
    @Autowired
    private DataSource dataSource;

    //配置对象
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //jdbcTokenRepository.setCreateTableOnStartup(true);  //设置启动时创建表,这里让 SpringSecurity 可能会有些问题,我们自己创建
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义实现用户名和密码
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    // 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //退出
        http.logout().logoutUrl("/logout"). //退出的超链接
                logoutSuccessUrl("/test/hello").permitAll();//退出后跳转的路径

        //配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");

        http.formLogin()   //自定义自己编写的登录页面
                .loginPage("/login.html")  //登录页面设置
                .loginProcessingUrl("/user/login")   //登录访问路径
                .defaultSuccessUrl("/success.html").permitAll()  //登录成功之后,跳转路径
        .and().authorizeRequests() //对哪些请求进行验证
                .antMatchers("/", "/test/hello", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
                //当前登录用户,只有具有admins权限才可以访问  /test/index  这个路径
                //1 hasAuthority方法  当前用户要用有 admins 权限才能访问
                .antMatchers("/test/index").hasAuthority("admins")
                //2 hasAnyAuthority方法  当前用户要用有 任 一 权限才能访问
//                 .antMatchers("/test/index").hasAnyAuthority("admins,manager")
                //3 hasRole方法   ROLE_sale  使用这个方法时,给用户设定角色时需要加一个 ROLE_ 的前缀
//                .antMatchers("/test/index").hasRole("sale")
                .anyRequest().authenticated()//除了上面的  "/","/test/hello","/user/login" 其他请求都需要进行验证  /** 才表示所有请求
        .and().rememberMe().tokenRepository(persistentTokenRepository())  //设置记住我
                .tokenValiditySeconds(180)//设置有效时长,单位秒
                .userDetailsService(userDetailsService); //要使用  userDetailsService 去操作数据库
        // .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        // .and().csrf().disable();  //关闭csrf防护
    }
}

3、添加一个复选框

在这里插入图片描述

4、测试

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

🥜CSRF 跨站请求伪造

在这里插入图片描述

浏览器会获取到我们所有的 cookie 当我们进行认证之后,其他网站可以拿到我们的 cookie 信息,进行跨站请求伪造,进行一系列的恶意操作。

1、开启 CSRF 防护

在这里插入图片描述

2、添加表单隐藏域

添加一个隐藏域,固定格式,Spring Security 会往这个隐藏域的表单属性自动注入token,由于别的网站获取到了 cookie ,但是每次的 token是不同的 _csrf.token,
利用其他网站伪造的攻击是拿不到这个 token 的,然后会拿着 token 和 session 中保存的另一个同样的 token进行比较
如果一样就表示是本站认证过的请求,如果不一样则表示为其他网站伪造的请求,因为 session 是一次浏览器与服务器的会话,别人仿造请求肯定是两次会话了
两个session 就算伪造的请求带有 token 也不会和 SpringSecurity 生成的 token 一样,因此就完成了 CSRF 跨网站伪造请求攻击。

在这里插入图片描述
在这里插入图片描述

3、CsrfFilter 源码分析

在这里插入图片描述

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);

        //从请求中获取 session 对象
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);

        //根据token 是否为 null 赋值 missingToken
        boolean missingToken = csrfToken == null;

        //由于上面一直为 null 这里也一直为 true
        if (missingToken) {

            //获取到一个默认的 token
            csrfToken = this.tokenRepository.generateToken(request);

            // saveToken () 方法会 往session域设置  一个默认的 token,至此session 存入了一个 token,在这里往 session 设置token,每个 session 的token 不一样
            this.tokenRepository.saveToken(csrfToken, request, response);
        }

        //然后将 token 放到 request 域中,将同一个 token 放入不同的地方,后面进行比对

        //setAttribute 是一个 K (String),V (Object) 结构,也就等于把 token 绑定到了 request 域中的不同对象上去
        request.setAttribute(CsrfToken.class.getName(), csrfToken);

        //把 token 绑定到 (请求参数) 表单隐藏域,用户的每次请求都需要获取到这个参数对应的 token,而这个 token只有和服务器自己知道
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
        //判断 request 请求方式是否需要拦截 (需要拦截的请求在 set 集合中吗? 不在返回 flase 然后 非运算一下,就会将在set 集合中的请求类型进行放行)

        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            // "GET", "HEAD", "TRACE", "OPTIONS" 不进行拦截
            filterChain.doFilter(request, response);
        } else {

            //从请求头获取 token
            String actualToken = request.getHeader(csrfToken.getHeaderName());

            //如果没有获取到
            if (actualToken == null) {
                // 从请求参数(隐藏表单域)获取 token,这个 token是我们程序设置的黑客拿不到,只能拿到一个参数名,黑客如果不设置 token 就会抛异常
                actualToken = request.getParameter(csrfToken.getParameterName());
            }

            //判断 session 的token 和 表单或者请求头的 token 是否一致,别的 session 拿到的表单 token 也是和自己的session 一致,而且token 随机,就不会被伪造
            if (!csrfToken.getToken().equals(actualToken)) {

                //不一致抛出异常
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
                }

                if (missingToken) {
                    this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
                } else {
                    this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
                }

            } else {
                //一致进行放行
                filterChain.doFilter(request, response);
            }
        }
    }

总结: CSRF攻击是在用户已经认证我们系统的基础上,访问了其他恶意连接,导致cookie泄露了出去,从而让黑客有可乘之机,我们只需要在每个用户的请求上加一个token,这个token只有服务器自己知道,这样黑客就算拿到了用户的cookie,没有服务器端的 token也是徒劳。

第一次获取 请求获取token,由于这时request还没有和服务器建立连接,里面判断session是否为空,request.getSession(false)表示当前的request如果没有session就为null,如果为true会创建一个新的session对象,这里肯定不能创建新的session对象的,由于第一次请求没有session对象,这时会返回一个null的token对象,后面再判断这个token是否为空,为空就获得一个默认的token,这时有了默认token,再去获取session对象,当程序走到这里会去创建一个session对象,把token存入session对象中,然后将token渲染到认证的表单,只有服务器的表单能够进行token的渲染,黑客的表单是不能通过服务器进行渲染的,然后我们在需要提交表单的地方都进行隐藏域渲染token,在html的Dom树进行加载时进行渲染。

这样认证的表单会有隐藏域(系服务器自己的表单),有这个隐藏域才会将token渲染上去,这一步是为了让用户进行认证,而黑客的csrf攻击是在用户认证过后,拿到cookie进行恶意操作,所以这里不用担心,接着会查看request的提交方式是否需要进行请求放行,如果不需要csrf防护就直接放行,然后就是获取请求头的token,如果获取不到请求头的token,就从参数获取,然后将token和存入session的token进行比对,如果一致进行放行,不一致抛出异常,由于用户的第一次认证会将渲染在表单的token带到服务器和session的token进行比对,这时用户肯定是可以认证的。

用户的第二次请求,先拿到request,这时用户已经和服务器有了session对象,就会获取到这个session对象的token,而不是去获取默认的token。这时还是会去做表单渲染(POST一般都是表单请求),但由于表单渲染都是在服务器端进行渲染,黑客伪造的网站是不能渲染的,然后用户的请求就可以拿着token,然后判断请求方式是否需要放行,需要进行拦截过滤的话,就会获取request请求头的token,如果获取不到,就去找请求参数的token,拿着request获取的token和session的token进行对比,如果不一致抛出异常,一致进行放行。

由于黑客是拿着用户认证后的cookie进行csrf攻击,虽然在用户认证是服务器将token渲染在了客户端,但渲染也是在服务端进行的,只是返回给浏览器一个html页面,这时当黑客伪拿到用户的cookie伪造了一个请求,就需要往request放入token,才能正确的访问服务端,由于这个token的创建和渲染都是在服务端进行的,黑客伪造的网页是不会渲染到token的,没有这个token,黑客伪造的请求自然而然的也就不能通过验证。

也就是在服务器端进行表单渲染,服务器能够被渲染的页面肯定是自己系统的网站,然后用户拿着渲染后的token,再次提交服务器,这时token一致,验证通过,而黑客伪造的网站并不能渲染到这个token,黑客最多拿到一个参数名,但是token值并不能获取,黑客发出的请求自然而然的不能通过验证,就算黑客的表单渲染到了token,但是 session 不同token不同,黑客的表单和服务器建立session,黑客的session中的token和他拿到用户的token还是不一样的。但如果黑客真的不经意间获取到了这个token,又该怎么办?

用户在与服务器建立链接每次都只能发送一个请求,这时就让改请求的token一次性,就算黑客获取到了token,但token是一次性的,黑客的请求拿着token访问服务器,这时获取session,渲染表单,黑客的表单不能渲染到token,这时黑客获取到的token就成了旧的token,还是不能访问服务器。

在这里插入图片描述

二、 SpringSecurity 微服务权限方案 :请移步谷粒学院

🥚认证授权过程分析

(1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找
到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。
(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限
信息中去

在这里插入图片描述

🍏需要的数据模型: 最基本的五张表

在这里插入图片描述

🥩开发环境

在这里插入图片描述
在这里插入图片描述

三、SpringSecurity源码分析

🍭 认证流程 UsernamePasswordAuthenticationFilter

在这里插入图片描述

大致的认证流程是从 UsernamePasswordAuthenticationFilter 开始, UsernamePasswordAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter
当登录请求过来时会调用 父类 AbstractAuthenticationProcessingFilter 的 doFilter 方法,然后判断 if (!this.requiresAuthentication(request, response)) 请求是否需要验证,不需要验证直接放行,然后父类 doFilter 继续往下走 会调用 abstract Authentication attemptAuthentication 抽象方法,这个方法的具体实现在子类进行,也就是会调用到 UsernamePasswordAuthenticationFilter 的attemptAuthentication( ) 方法,返回一个Authentication 对象用来封装认证结果 authResult,如果该对象不为空,会去配置session 会话策略this.sessionStrategy.onAuthentication, 如果这个封装的对象为空,说明认证失败,直接 return 了,如果在子类执行 attemptAuthentication( ) 方法时抛出异常,会catch住异常并且调用 this.unsuccessfulAuthentication 做一系列的断尾操作, 如果上面操作没有任何异常就会判断 if (this.continueChainBeforeSuccessfulAuthentication) 是否为 true,为true就调用 chain.doFilter(request, response); 放行,上面操作没有异常会将 continueChainBeforeSuccessfulAuthentication设置为 true的,所以不用担心,最后会调用 this.successfulAuthentication( ) 方法做一些认证成功的结尾逻辑。

接着继续看子类 UsernamePasswordAuthenticationFilter 的 attemptAuthentication( ) 方法尝试验证身份,判断请求方式是否为 POST 不是的话直接抛出异常,然后获取到 userName和passWord进行判断是否为空串,不是空串对userName变量进行去除前后空格,但这些都不是重点,然后调用 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); 获取到一个 authRequest 未认证的标记,因为这时还没有去查数据库什么的,然后调用 this.setDetails(request, authRequest); 将request 请求的详细参数封装到 authRequest 未认证标记,最后调用return this.getAuthenticationManager().authenticate(authRequest); AuthenticationManager身份验证管理器将这个为验证的 authRequest 放进去进行验证。

最后值得我们注意的是,authRequest 未认证标记UsernamePasswordAuthenticationToken 对象是一个 Authentication 接口的实现类,其中有一个方法 void setAuthenticated(boolean isAuthenticated) , UsernamePasswordAuthenticationToken 在构造函数的参数不同对方法 setAuthenticated( ) 设置了认证标记 ,根据不同的参数进行判断当前的用户是否已经认证,不过根据我们的分析现在还没有进行认证,需要调用 authenticate(authRequest); 进行验证,而 authenticate( ) 方法来自另一个接口AuthenticationManager,该接口只有一个方法 Authentication authenticate(Authentication authentication) 是验证一个 Authentication ,并将这个 Authentication 返回,至此子类attemptAuthentication( ) 方法尝试验证身份结束,最后会将验证过的 Authentication 返回给父类AbstractAuthenticationProcessingFilter

如果大家感兴趣还可以去 AuthenticationManager的实现类ProviderManager 的 authenticate()方法是怎么对 UsernamePasswordAuthenticationToken 认证标记对象进行认证的,里面全是if else 和 try catch。

最后我们来到父类 AbstractAuthenticationProcessingFilter,如果子类获取身份验证结果没有任何异常就会调用 this.successfulAuthentication(request, response, chain, authResult); 处理认证成功的业务,SecurityContextHolder.getContext().setAuthentication(authResult);会将认证结果放到SpringSecurity 的全局上下文对象 SecurityContextHolder供全局使用,然后调用 this.rememberMeServices.loginSuccess(request, response, authResult); 实现下次请求无需认证,然后判断发布事件监听器如果不为空就 调用 this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); 获取一个InteractiveAuthenticationSuccessEvent 交互式身份验证成功事件最后调用 this.successHandler.onAuthenticationSuccess(request, response, authResult);成功处理器,至此SpringSecurity认证流程彻底完毕。

最后 处理认证失败的业务和处理成功的业务大差不差。

在这里插入图片描述

在这里插入图片描述

🍖权限认证流程

在这里插入图片描述

🥙认证信息共享

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

🍤总结认证流程

在这里插入图片描述

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

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

相关文章

智能玩具机器人语音识别方案——NRK3301离线语音IC

机器人玩具已经成为儿童玩具和教育用品的主流&#xff0c;它不仅能充分激发和满足了儿童消费群体的好奇心&#xff0c;同时还能强化了消费群体和玩具的互动体验。 机器人玩具主要是通过语音识别技术&#xff0c;让我们可以与玩具对话&#xff0c;可以用语音对玩具发出命令&…

ENVI实现基于像元方法的栅格图像镶嵌拼接(所有图像无需地理信息)

本文介绍基于ENVI软件&#xff0c;利用“Pixel Based Mosaicking”工具实现栅格遥感影像镶嵌拼接的方法。 首先需要说明的是&#xff0c;本文需要镶嵌的遥感影像并不含地理参考信息&#xff0c;因此仅可以使用ENVI中的“Pixel Based Mosaicking”工具&#xff08;该工具可以对…

SpringMVC简介、请求与响应、REST风格、SSM整合、拦截器

目录 SpringMVC简介 SpringMVC概述 入门案例 入门案例工作流程分析 Controller加载控制 PostMan 请求与响应 设置请求映射路径 五种类型参数传递 JSON数据传输参数 JSON对象数据 JSON对象数组 日期类型参数传递 响应 REST风格 REST风格简介 RESTful入门案例…

前后端分离实现社区销售系统

在当今的互联网时代&#xff0c;社区销售系统越来越普及。这种系统可以方便地管理商品、订单以及会员等信息&#xff0c;使得销售过程更加高效和便利。本文将介绍如何通过前后端分离的方式实现一个社区销售系统。 需求分析 社区销售系统主要包括会员管理、商品管理、订单管理…

C++ ---- 类和对象(中)

目录 类的默认成员函数介绍 构造函数 构造函数概念 构造函数特性 析构函数 析构函数概念 析构函数特性 拷贝构造 拷贝构造概念 拷贝构造特点 赋值重载 赋值重载介绍 赋值重载特性 取地址重载和const取地址重载 const成员 取地址和const取地址重载 类的默认成员函…

【致敬未来的攻城狮计划】— 连续打卡第三十天:总结与回顾

学习目标&#xff1a; 自2023年4月13日开始&#xff0c;我参加了为期一个月的【致敬未来的攻城狮计划】&#xff0c;今天是第三十天&#xff0c;做一个总结和回顾。 我参加的是【致敬未来的攻城狮计划】第二期&#xff08;攻城狮计划&#xff09; 在这里首先还是感谢 李…

【云服务器】关于UDP/TCP跨平台网络通信服务器无响应的情况及解决办法

关于跨平台网络通信服务器无反应的情况 一、问题出现二、云服务器Centos7防火墙开放端口2.1 检查防火墙状态2.2 开启防火墙2.3 在running 状态下&#xff0c;向firewall 添加需要开放的端口2.4 重新加载防火墙配置2.5 查看端口是否放开 三、云服务器防火墙配置开放端口3.1 进入…

决策树与随机森林

决策树解决回归问题时进行平均数计算。 决策树 (1)熵&#xff08;entropy)与特征节点 熵&#xff08;entropy&#xff09;&#xff0c;度量着信息的不确定性&#xff0c;信息的不确定性越大&#xff0c;熵越大。信息熵和事件发生的概率成反比。 ■信息熵代表随机变量的复杂度…

c++《list容器的使用》

本文主要介绍list的一些常见接口的使用 文章目录 一、list的介绍二、list的使用2.1 list的构造函数2.2 list迭代器的使用2.3 list相关的容量大小相关的函数2.4 list数据的访问相关的函数2.5 list的数据调整相关的函数2.6 list中其他函数操作 一、list的介绍 list是可以以O(1)的…

IOC理论推导

1.UserDao接口 package com.kuang.dao;public interface UserDao {void getUser(); }2.UserDaoImpl业务接口 package com.kuang.dao;public class UserDaoImpl implements UserDao{Overridepublic void getUser() {System.out.println("默认获取用户数据");} }3.Us…

【ChatGPT】国内免费使用的ChatGPT镜像

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 什么是ChatGPT镜像&#xff1f; 亲测&#xff1a; 一、二狗问答(AI对话) 二、AiDuTu 三、WOChat 四、ChatGPT(个人感觉最好用) 我们可以利用ChatGPT干什么&#xff1f; 一、三分…

基于Python3的tkinter Text文本框加滚动条显示信息

用tkinter进行界面程序开发中&#xff0c;经常需要将信息展示到界面上&#xff0c;给用户及时的反馈和想要看到的结果。Text控件允许用户以不同的样式、属性来显示和编辑文本&#xff0c;它可以包含纯文本或者格式化文本&#xff0c;同时支持嵌入图片、显示超链接以及带有 CSS …

C++ Primer Plus——第6章 分支语句和逻辑运算符

第6章 分支语句和逻辑运算符 6.1 if语句6.1.1 if else语句6.1.2 格式化if else语句6.1.3 if else if else结构 6.2 逻辑表达式6.2.1 逻辑OR运算符&#xff1a;||6.2.2 逻辑AND运算符&#xff1a;&&6.2.3 用&&来设置取值范围 6.1 if语句 if语句让程序能够决定是…

CSP-202212-2 训练计划

目录 一、题目 二、思路 三、C代码如下 一、题目 问题背景 西西艾弗岛荒野求生大赛还有 n 天开幕&#xff01; 问题描述 为了在大赛中取得好成绩&#xff0c;顿顿准备在 n 天时间内完成“短跑”、“高中物理”以及“核裂变技术”等总共 m 项科目的加强训练。其中第 i 项…

【源码解析】@ControllerAdvice实现异常捕获与响应增强处理的原理解析

全局异常处理 demo展示 Slf4j RestControllerAdvice public class GlobalExceptionAdvice {ExceptionHandler(RuntimeException.class)public R<Void> handleNotPermissionException(RuntimeException e, HttpServletRequest request) {String requestURI request.get…

卷土重来?我羊羊羊羊羊了!

大家注意&#xff1a;因为微信最近又改了推送机制&#xff0c;经常有小伙伴说错过了之前被删的文章&#xff0c;比如前阵子冒着风险写的爬虫&#xff0c;再比如一些限时福利&#xff0c;错过了就是错过了。 所以建议大家加个星标&#xff0c;就能第一时间收到推送。&#x1f44…

顶尖作品集封面封底来了 共28套

各位打算换工作的都找到心仪东家没? 我们都知道作品集作为我们的敲门砖 其重要性不言而喻 如何让我们的作品集脱颖而出 这里作品集封面就很重要一个设计感超强的封面 可以让面试官眼前一亮 今天给大家整理了28款设计师专属作品集封面封底 让HR看后过目不忘大大提高面试…

删除二叉搜索树中的节点

1题目 给定一个二叉搜索树的根节点 root 和一个值 key&#xff0c;删除二叉搜索树中的 key 对应的节点&#xff0c;并保证二叉搜索树的性质不变。返回二叉搜索树&#xff08;有可能被更新&#xff09;的根节点的引用。 一般来说&#xff0c;删除节点可分为两个步骤&#xff1a…

Oracle EBS Interface/API(48)- AP发票取消API

快速参考 参考点内容功能导航N: AP->发票->录入->发票并发请求None基表AP.AP_INVOICES_ALLAPI参考下面介绍错误信息表None接口FormNone接口RequestDebug ProfileNone详细例子参考如下实例官方文档None数据验证包None用户界面 Path:AP->发票>录入>发票->活…

Android Studio下配置NDK和Cmake

文章目录 NDK简介AS上安装NDK和CmakeAS项目中添加支持C和CMake NDK简介 ndk是SDK的扩展部分&#xff0c;ndk是一套在Android Studio 上支持开发及编译链接C/C的工具链。 AS上安装NDK和Cmake 点击完OK后&#xff0c;等待安装就行了。安装完毕后&#xff1a; 在local.propertie…