Bootstrap

Spring Security框架 前后端分离版

目录

一、SpringSecurity 框架简介

1、 概要

2、 历史

3、 同款产品对比

4 、模块划分

 5、权限管理中的相关概念

二、 SpringSecurity 认证入门体验

1、创建sprongboot

2、数据库创建,以后会用

3、编写测试接口

4、运行这个项目,访问测试接口

5、其他认证方式

方式一:yml配置文件

方式二:配置类

​​​​​​方式三:自定义登录账号和密码校验方式

三、SpringSecurity完整流程

1、SpringSecurity过滤器链

1.1UsernamePasswordAuthenticationFilter 

1.2ExceptionTranslationFilter

1.3FilterSecurityInterceptor

2、启动项目控制台打印过滤器链

四、 SpringSecurity登录认证

1、前后端分离项目登陆校验流程

​编辑

2、认证流程详解

UserDetailsService 接口

PasswordEncoder 接口

3、认证代码思路分析

3.1定义用户实体类

3.2实现UserDetailsService

3.3自定义加密方法

3.4自定义登录接口

以上部分实现了登录下面自定义认证过滤器

3.5JWT认证过滤器代码实现

4、 SpringSecurity退出登录

五、 SpringSecurity权限管理

1、基于注解的权限校验

1.1配置类添加注解,开启权限校验

1.2封装权限信息

1.3从数据库查询权限信息,并储存权限信息

1.4权限信息存入UsernamePasswordAuthenticationToken

1.5接口中添加注解,开启权限校验

1.6测试

2、其他权限校验方法

3、自定义权限校验方法

3.1自定义权限校验方法

3.2接口测试

4、基于配置的权限校验方法

六、 自定义失败处理

1、自定义异常处理流程

1.1创建这两个接口的实现类

1.2将自定义的异常处理类交给Security

1.3 测试

七、 配置跨域

​编辑

八、CSRF


一、SpringSecurity 框架简介

官网: Spring Security Reference

jjwt工具类:(1条消息) jjwt工具类_S Y H的博客-CSDN博客_jjwt工具类

redis配置和工具类 :(1条消息) redis操作模板_S Y H的博客-CSDN博客_redis模板

1、 概要

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的 成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方 案。 正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控 制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权 (Authorization)两个部分,这两点也是 Spring Security 重要核心功能。 (1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问 该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认 证过程。通俗点说就是系统认为用户是否能登录 (2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户 所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以 进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的 权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

2、 历史

“Spring Security 开始于 2003 年年底,““spring 的 acegi 安全系统”。 起因是 Spring 开发者邮件列表中的一个问题,有人提问是否考虑提供一个基于 spring 的安全实现。 Spring Security 以“The Acegi Secutity System for Spring” 的名字始于 2013 年晚些 时候。一个问题提交到 Spring 开发者的邮件列表,询问是否已经有考虑一个机遇 Spring 的安全性社区实现。那时候 Spring 的社区相对较小(相对现在)。实际上 Spring 自己在 2013 年只是一个存在于 ScourseForge 的项目,这个问题的回答是一个值得研究的领 域,虽然目前时间的缺乏组织了我们对它的探索。 考虑到这一点,一个简单的安全实现建成但是并没有发布。几周后,Spring 社区的其他成 员询问了安全性,这次这个代码被发送给他们。其他几个请求也跟随而来。到 2014 年一 月大约有 20 万人使用了这个代码。这些创业者的人提出一个 SourceForge 项目加入是为 了,这是在 2004 三月正式成立。 在早些时候,这个项目没有任何自己的验证模块,身份验证过程依赖于容器管理的安全性 和 Acegi 安全性。而不是专注于授权。开始的时候这很适合,但是越来越多的用户请求额 外的容器支持。容器特定的认证领域接口的基本限制变得清晰。还有一个相关的问题增加 新的容器的路径,这是最终用户的困惑和错误配置的常见问题。 Acegi 安全特定的认证服务介绍。大约一年后,Acegi 安全正式成为了 Spring 框架的子项 目。1.0.0 最终版本是出版于 2006 -在超过两年半的大量生产的软件项目和数以百计的改 进和积极利用社区的贡献。 Acegi 安全 2007 年底正式成为了 Spring 组合项目,更名为"Spring Security"。 1.3 同款产品对比

3、 同款产品对比

SpringSecurity 特点:

  • 和 Spring 无缝整合。
  • 全面的权限控制。
  • 专门为 Web 开发而设计。 ◼旧版本不能脱离 Web 环境使用。 ◼新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独 引入核心模块就可以脱离 Web 环境。
  • 重量级。

ApacheShiro

  • 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求 的互联网应用有更好表现。
  • 通用性。 ◼好处:不局限于 Web 环境,可以脱离 Web 环境使用。 ◼缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。
  •  shiro框架学习请移步:Apache shiro框架_SUN Y H的博客-CSDN博客

Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之 前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直 是 Shiro 的天下。 相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。 自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方 案,可以使用更少的配置来使用 Spring Security。 因此,一般来说,常见的安全管理技术栈的组合是这样的: • SSM + Shiro • Spring Boot/Spring Cloud + Spring Security 以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。

4 、模块划分

 5、权限管理中的相关概念

  • 主体:英文单词:principal。使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统谁就是主体。
  • 认证:英文单词:authentication。权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁。笼统的认为就是以前所做的登录操作。
  • 授权:英文单词:authorization。将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。 所以简单来说,授权就是给用户分配权限。

二、 SpringSecurity 认证入门体验

1、创建sprongboot

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.5.14</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.19</version>
        </dependency>
    </dependencies>

2、数据库创建,以后会用

CREATE TABLE users ( id BIGINT PRIMARY KEY auto_increment, username VARCHAR ( 20 ) UNIQUE NOT NULL, PASSWORD VARCHAR ( 100 ) );-- 密码 atguigu
INSERT INTO users
VALUES
	( 1, '张
	san', '$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na' );-- 密码 atguigu
INSERT INTO users
VALUES
	( 2, '李
	si', '$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na' );
CREATE TABLE role ( id BIGINT PRIMARY KEY auto_increment, NAME VARCHAR ( 20 ) );
INSERT INTO role
VALUES
	( 1, '管理员' );
INSERT INTO role
VALUES
	( 2, '普通用户' );
CREATE TABLE role_user ( uid BIGINT, rid BIGINT );
INSERT INTO role_user
VALUES
	( 1, 1 );
INSERT INTO role_user
VALUES
	( 2, 2 );
CREATE TABLE menu ( id BIGINT PRIMARY KEY auto_increment, NAME VARCHAR ( 20 ), url VARCHAR ( 100 ), parentid BIGINT, permission VARCHAR ( 20 ) );
INSERT INTO menu
VALUES
	( 1, '系统管理', '', 0, 'menu:system' );
INSERT INTO menu
VALUES
	( 2, '用户管理', '', 0, 'menu:user' );
CREATE TABLE role_menu ( mid BIGINT, rid BIGINT );
INSERT INTO role_menu
VALUES
	( 1, 1 );
INSERT INTO role_menu
VALUES
	( 2, 1 );
INSERT INTO role_menu
VALUES
	( 2, 2 );

3、编写测试接口

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

    @GetMapping("/test1")
    public ResultUtils test1() {
        return ResultUtils.success(RespStaticEnum.SUCCESS);
    }
}

4、运行这个项目,访问测试接口

默认用户名:user

密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都回发生变化!

 

5、其他认证方式

Security密码校验方式优先级  配置文件 -->  配置类 -->  UserDetailsService实现类

通过访问项目测试发现,前两种方式输入正确的账号和密码都能正常访问。但是实际工作中肯定不能这样使用

方式一:yml配置文件

spring:
  security:
    user:
      name: atguigu
      password: atguigu

方式二:配置类

Spring Boot 2.7.0 之前的版本中,我们需要写个配置类继承WebSecurityConfigurerAdapter,然后重写Adapter中的三个方法进行配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    // 注入 PasswordEncoder 类到 spring 容器中
    // 上面方法需要该bean
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 前不久Spring Boot 2.7.0 刚刚发布,Spring Security 也升级到了5.7.1 。升级后发现,之前的配置方法居然已经被弃用了,以下是Spring Security的最新用法。

我们虽然使用的高版本,但是依然使用老方法进行讲解

@EnableWebSecurity
@Configuration
public class SecurityConfig{

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.
                formLogin() // 表单登录
                .and()
                .authorizeRequests() // 认证配置
                .anyRequest() // 任何请求
                .authenticated(); // 都需要身份验证

        return http.build();
    }
}

​​​​​​方式三:自定义登录账号和密码校验方式

第一步:数据库表和实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("users")
public class Users {

    @TableId("id")
    private String id;

    @TableField("username")
    private String username;

    @TableField("PASSWORD")
    private String password;

}

 第二步:实现UserDetailsService接口

业务内容仅仅判断该登录用户是否存在于数据库中

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

    @Autowired
    private LoginMapper loginMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<Users> usersLambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 根据用户名称查询用户信息
        usersLambdaQueryWrapper.eq(Users::getUsername, username);
        Users users = loginMapper.selectOne(usersLambdaQueryWrapper);
        // users==null登录失败,users!=null登陆成功
        if(users == null) {
            throw new UsernameNotFoundException("登录失败!");
        }
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        /*
        * Security自带UserDetails实现类User对象,也可以自定义User对象实现UserDetails接口
        * 参数一为账号:张san
        * 参数二为密码:atguigu 加密方式: new BCryptPasswordEncoder().encode("123") == $2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na
        * 第三个值为权限列表
        * */
        return new User(users.getUsername(),  users.getPassword(), auths);
    }
}

第三步:依赖注入

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 自定义加密方式
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

第四步:访问测试

  户名:张san    密码:atguigu     即可访问成功

三、SpringSecurity完整流程

1、SpringSecurity过滤器链

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。

 图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

1.1UsernamePasswordAuthenticationFilter 

负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

1.2ExceptionTranslationFilter

是个异常过滤器,处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 

1.3FilterSecurityInterceptor

是一个方法级的权限过滤器, 基本位于过滤链的最底部

2、启动项目控制台打印过滤器链

 

四、 SpringSecurity登录认证

1、前后端分离项目登陆校验流程

2、认证流程详解

  •  Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
  • AuthenticationManager接口:定义了认证Authentication的方法
  • UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
  • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

UserDetailsService 接口

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑 

如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

(1)返回值 UserDetails

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

// 表示获取登录用户所有权限
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    // 表示获取密码
    String getPassword();

    // 表示获取用户名
    String getUsername();

    // 表示判断账户是否过期
    boolean isAccountNonExpired();

    // 表示判断账户是否被锁定
    boolean isAccountNonLocked();

    // 表示凭证{密码}是否过期
    boolean isCredentialsNonExpired();

    // 表示当前用户是否可用
    boolean isEnabled();
}

(2)以下是 UserDetails 实现类

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

方法参数 username,表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无法接收。

PasswordEncoder 接口

(1)PasswordEncoder 接口源码


public interface PasswordEncoder {
    
    // 表示把参数按照特定的解析规则进行解析
    String encode(CharSequence rawPassword);

    /** 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹
        配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个
        参数表示存储的密码。**/
    boolean matches(CharSequence rawPassword, String encodedPassword);

    /** 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
        false。默认返回 false。 **/
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

(2)PasswordEncoder 接口实现类 BCryptPasswordEncoder

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析
器。

BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单
向加密。可以通过 strength 控制加密强度,默认 10.

密码解析器代码演示

public static void main(String[] args) {
        // 创建密码解析器
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        // 对密码进行加密
        String atguigu = bCryptPasswordEncoder.encode("atguigu");
        // 打印加密之后的数据
        System.out.println("加密之后数据:\t"+atguigu);
        //判断原字符加密后和加密之前是否匹配
        boolean result = bCryptPasswordEncoder.matches("abcdefghij", atguigu);
        // 打印比较结果
        System.out.println("比较结果:\t"+result);
    }

3、认证代码思路分析

3.1定义用户实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("users")
public class Users implements Serializable{

    @TableId("id")
    private String id;

    @TableField("username")
    private String username;

    @TableField("PASSWORD")
    private String password;
}

3.2实现UserDetailsService

@Service
public class SecurityLoginService  implements UserDetailsService{

    @Autowired
    private LoginMapper loginMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<Users> usersLambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 根据用户名称查询用户信息
        usersLambdaQueryWrapper.eq(Users::getUsername, username);
        Users users = loginMapper.selectOne(usersLambdaQueryWrapper);
        // users==null登录失败,users!=null登陆成功
        if(Objects.isNull(users)) {
            throw new UsernameNotFoundException("用户名或密码错误!");
        }
        return new LoginUser(users);
    }
}

实现UserDetails作为loadUserByUsername方法的返回值。也可以使用框架自带的实现类,推荐自定义。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    /*
    * 自定义用户对象
    * */
    private Users users;

    /*
    * 权限信息
    * */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    /*
    * 密码
    * */
    @Override
    public String getPassword() {
        return users.getPassword();
    }

    /*
    * 用户名
    * */
    @Override
    public String getUsername() {
        return users.getUsername();
    }

    /*
    * 表示判断账户是否过期
    * */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /*
    * 表示判断账户是否被锁定
    * */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /*
    * 表示凭证{密码}是否过期
    * */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /*
    * 是否可用
    * */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

3.3自定义加密方法

实际项目中我们不会把密码明文存储在数据库中。

默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。

Spring Security封装了如bcrypt, PBKDF2, scrypt, Argon2等主流适应性单向加密方法( adaptive one-way functions),用以进行密码存储和校验。单向校验安全性高,但开销很大,单次密码校验耗时可能高达1秒,故针对高并发性能要求较强的大型信息系统,Spring Security更推荐选择如:session, OAuth,Token等开销很小的短期加密策略(short term credential)实现系统信息安全。

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析
器。

我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10.

我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    // 自定义加密方法
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

3.4自定义登录接口

第一步:放行登录接口

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 自定义加密方法
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    /*
     * 访问路径拦截
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()  // 关闭csrf
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // 不通过session获取SecurityContext
                .and()
                .authorizeRequests()
                .antMatchers("/api/LoginController/Login").anonymous()  // 允许登录接口匿名访问
                .anyRequest().authenticated();  // 除上述之外的全部请求都需要鉴权认证
    }
}

第二步:登录接口controller层

    @Autowired
    private LoginService loginService;

    /*
    * 登录接口
    * */
    @PostMapping("/Login")
    public ResultUtils Login(@RequestBody Users users) {
        // 认证通过,返回给前端jjwt
        String jjwtStr = loginService.Login(users);
        return ResultUtils.success(RespStaticEnum.SUCCESS, jjwtStr);
    }

第三步:登录接口service层

import java.util.Objects;

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisService redisService;

    @Override
    public String Login(Users users) {
        // 进行用户认证,会调用之前写的SecurityLoginService中认证方法
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                new UsernamePasswordAuthenticationToken(users.getUsername(), users.getPassword());
        Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        // 认证通过:authenticate!=null  认证不通过:authenticate==null
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }
        // 认证通过,生成jjwt
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        // 添加用户id 和 字符串”lalala“ 生成jjwt
        String jwtToken = JwtUtil.getJwtToken(loginUser.getUsers().getId(), "lalala");
        // 将用户信息存入redis中,用户id作为键
        redisService.setCacheObject(loginUser.getUsers().getId(), loginUser);
        return jwtToken;
    }
}

第四步:测试

http://localhost:8080/api/LoginController/Login

以上部分实现了登录下面自定义认证过滤器

3.5JWT认证过滤器代码实现

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

使用userid去redis中获取对应的LoginUser对象。

然后封装Authentication对象存入SecurityContextHolder

第一步:自定义jwt过滤器

@Component
public class JwtSecurityFilter extends OncePerRequestFilter {

    @Autowired
    private RedisService redisService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        // 校验token是否失效,如果失效,直接放行.后面还有其他过滤链条会进行拦截
        if (!JwtUtil.checkToken(request.getHeader("token"))) {
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        Map<String, String> memberIdByJwtToken = JwtUtil.getMemberIdByJwtToken(request);
        String userId = memberIdByJwtToken.get("username");
        // 获取redis中用户信息
        LoginUser loginUser = redisService.getCacheObject(userId);
        // 校验用户信息
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("用户未登录");
        }
        // 存入 SecurityContextHolder   参数一:用户信息   参数二:  参数三:权限认证
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                                    new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        // 放行
        filterChain.doFilter(request, response);
    }
}

第二步:将jwt过滤器放置在过滤链中

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 自定义加密方法
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    @Autowired
    private JwtSecurityFilter jwtSecurityFilter;

    /*
     * 访问路径拦截
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()  // 关闭csrf
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // 不通过session获取SecurityContext
                .and()
                .authorizeRequests()
                .antMatchers("/api/LoginController/Login").anonymous()  // 允许登录接口匿名访问
                .anyRequest().authenticated();  // 除上述之外的全部请求都需要鉴权认证

        http    // 将自定义JWT校验过滤链方法UsernamePasswordAuthenticationToken过滤链之前
                .addFilterBefore(jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

第三步:测试

4、 SpringSecurity退出登录

我们只需要定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。

第一步:controller

    /*
    * 退出登录接口与
    * */
    @GetMapping("/loginOut")
    public ResultUtils loginOut() {
        loginService.loginOut();
        return ResultUtils.success(RespStaticEnum.SUCCESS);
    }

第二步:service

    @Override
    public void loginOut() {
        // SecurityContextHolder中获取userId
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser principal = (LoginUser) authentication.getPrincipal();
        // 删除redis中用户信息缓存
        redisService.deleteObject(principal.getUsers().getId());
    }

第三步:测试

 随后访问其他接口,错误代码403无权限

五、 SpringSecurity权限管理

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

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。

然后设置我们的资源所需要的权限即可。

1、基于注解的权限校验

1.1配置类添加注解,开启权限校验

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@Configuration
// 开启security权限配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

}

1.2封装权限信息

@Data
public class LoginUser implements UserDetails {

    /*
    * 自定义用户对象
    * */
    private Users users;

    /*
    * 权限信息集合
    * */
    private List<String> authorityList;


    public LoginUser(Users users, List<String> authorityList) {
        this.users = users;
        this.authorityList = authorityList;
    }

    /*
    * 权限信息
    * */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 将权限信息集合封装为GrantedAuthority集合
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        authorityList.forEach((authorityStr) -> {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authorityStr);
            grantedAuthorityList.add(simpleGrantedAuthority);
        });
        return grantedAuthorityList;
    }

1.3从数据库查询权限信息,并储存权限信息

@Service
public class SecurityLoginService  implements UserDetailsService{

    @Autowired
    private LoginMapper loginMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<Users> usersLambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 根据用户名称查询用户信息
        usersLambdaQueryWrapper.eq(Users::getUsername, username);
        Users users = loginMapper.selectOne(usersLambdaQueryWrapper);
        // users==null登录失败,users!=null登陆成功
        if(Objects.isNull(users)) {
            throw new UsernameNotFoundException("用户名或密码错误!");
        }
        // 查询数据库获取用户权限信息
        List<String> authorityList = loginMapper.getAuthority(users.getId());
        return new LoginUser(users, authorityList);
    }
}

数据权限sql

    <select id="getAuthority" resultType="java.lang.String">
        SELECT
            permission
        FROM
            menu
        WHERE
                id IN ( SELECT mid FROM role_menu WHERE rid IN ( SELECT rid FROM `role_user` WHERE uid = '1' ) )
    </select>

1.4权限信息存入UsernamePasswordAuthenticationToken

@Component
public class JwtSecurityFilter extends OncePerRequestFilter {

    @Autowired
    private RedisService redisService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        // 校验token是否失效,如果失效,直接放行.后面还有其他过滤链条会进行拦截
        if (!JwtUtil.checkToken(request.getHeader("token"))) {
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        Map<String, String> memberIdByJwtToken = JwtUtil.getMemberIdByJwtToken(request);
        String userId = memberIdByJwtToken.get("username");
        // 获取redis中用户信息
        LoginUser loginUser = redisService.getCacheObject(userId);
        // 校验用户信息
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("用户未登录");
        }
        // 存入 SecurityContextHolder   参数一:用户信息   参数二:  参数三:权限认证
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                                    new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        // 放行
        filterChain.doFilter(request, response);
    }
}

1.5接口中添加注解,开启权限校验

    /*
     * 权限测试接口
     * */
    @PreAuthorize("hasAnyAuthority('menu:user')")
    @GetMapping("/authority")
    public ResultUtils authority(){
        return ResultUtils.success(RespStaticEnum.SUCCESS, "权限校验成功");
    }

1.6测试

2、其他权限校验方法

我们前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。SpringSecurity还为我们提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole等。

这里我们先不急着去介绍这些方法,我们先去理解hasAuthority的原理,然后再去学习其他方法你就更容易理解,而不是死记硬背区别。并且我们也可以选择定义校验方法,实现我们自己的校验逻辑。

hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority,大家只要断点调试既可h3知道它内部的校验原理。

它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。

2.1hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源  

    @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
    public String hello(){
        return "hello";
    }

2.2hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以

    @PreAuthorize("hasRole('system:dept:list')")
    public String hello(){
        return "hello";
    }

2.3hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以

    @PreAuthorize("hasAnyRole('admin','system:dept:list')")
    public String hello(){
        return "hello";
    }

3、自定义权限校验方法

我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。

3.1自定义权限校验方法

@Component("esPermissionHandlers")
public class PermissionHandlers {

    /*
    * 自定义权限校验
    *
    * 当同满足两个权限
    * authorities1 第一个权限
    * authorities2 第二个权限
    * */
    public boolean hasAnyAuthority(String authorities1, String authorities2) {
        // 获取权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> authorityList = loginUser.getAuthorityList();

        // 权限校验
        boolean contains = authorityList.contains(authorities1);
        boolean contains1 = authorityList.contains(authorities2);
        // 两个权限不一样,并且用户存在这两个权限
        return !authorities1.equals(authorities2) && contains && contains1;
    }
}

3.2接口测试

    /*
    * 自定义权限测试接口
    * */
    @PreAuthorize("@esPermissionHandlers.hasAnyAuthority('menu:system', 'menu:user')")
    @GetMapping("/permission")
    public ResultUtils permission() {
        return ResultUtils.success(RespStaticEnum.SUCCESS, "自定义权限测试接口");
    }

4、基于配置的权限校验方法

六、 自定义失败处理

1、自定义异常处理流程

我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

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

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

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

1.1创建这两个接口的实现类

/*
* 认证失败处理器
* */
@Component
public class SecurityAuthenticationEntryPointExceptionHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //  SECURITY_CODE(500205, "用户认证失败");
        ResultUtils success = ResultUtils.success(RespStaticEnum.SECURITY_CODE);
        String s = JSON.toJSONString(success);
        // 处理异常
        WebUtils.renderString(response, s);
    }
}
/*
* 权限校验处理器
* */
@Component
public class SecurityAccessDeniedHandlerExceptionHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //  NO_AUTHORITY(500206, "权限校验失败");
        ResultUtils success = ResultUtils.success(RespStaticEnum.NO_AUTHORITY);
        String s = JSON.toJSONString(success);
        // 处理异常
        WebUtils.renderString(response, s);
    }
}
WebUtils工具类:(1条消息) WebUtils_S Y H的博客-CSDN博客

1.2将自定义的异常处理类交给Security

    @Autowired
    private JwtSecurityFilter jwtSecurityFilter;

    // 认证失败处理器
    @Autowired
    private SecurityAuthenticationEntryPointExceptionHandler securityAuthenticationEntryPointExceptionHandler;

    // 权限校验处理器
    @Autowired
    private SecurityAccessDeniedHandlerExceptionHandler securityAccessDeniedHandlerExceptionHandler;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .exceptionHandling()
                .authenticationEntryPoint(securityAuthenticationEntryPointExceptionHandler)  // 认证失败处理器
                .accessDeniedHandler(securityAccessDeniedHandlerExceptionHandler);  // 权限校验处理器
    }

1.3 测试

测试发现权限校验处理器没有生效,最后使用的全局异常处理器捕获的AccessDeniedException异常。有懂哥欢迎留言解答

import org.springframework.security.access.AccessDeniedException;


/*
* 全局异常处理类
* */
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /*
    * 全局异常捕获
    * */
    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public ResultUtils javaExceptionHandler(Exception ex){
        return ResultUtils.fail(RespStaticEnum.FAIL, ex.getMessage());
    }

    /*
    * Security框架权限校验失败异常捕获
    * */
    @ResponseBody
    @ExceptionHandler(AccessDeniedException.class)
    public ResultUtils unauthorizedException(Exception ex){
        log.error("无权限:" + ex.getLocalizedMessage());
        // NO_AUTHORITY(500206, "权限校验失败");
        return ResultUtils.fail(RespStaticEnum.NO_AUTHORITY);
    }
}

七、 配置跨域

@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * 跨域配置添加
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
              //  .allowedOrigins("*")
                .allowedOriginPatterns("*")
                // 是否允许证书 不再默认开启
                .allowCredentials(true)
                // 设置允许的方法
                .allowedMethods("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}

八、CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

CSRF攻击与防御(写得非常好)_擒贼先擒王的博客-CSDN博客

SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。

项目中关闭CSRF原因:

我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

;