初识SpringSecurity

时间:2022-02-16 01:24:06

初识SpringSecurity

前言

本文主要是对SpringSecurity的一个粗略的学习,大致学习使用SpringBoot整合SpringSecurity和Redis实现一个简单的登录认证以及授权功能,同时熟悉登录认证和授权的一个大概的流程是怎么样的,其次还会学习使用CORS解决跨域问题。至于更加详细的内容会在后面进一步学习
PS: 相关代码请参考博主的 Gitee仓库或者Github仓库

1、SpringSecurity概述

  • 什么是Spring Security

    Spring Security是一个基于Spring框架的安全性框架,提供了在Java应用程序中进行身份验证、授权和其他安全性功能的支持。它在Web应用程序和非Web应用程序中都可以使用,并且支持多种身份验证和授权机制。Spring Security可以轻松地与Spring应用程序集成,为开发人员提供了一种可靠、可扩展和易于使用的方式来保护其应用程序。

  • Spring Security有哪些作用

    • 身份验证:提供了多种身份验证机制,如基于表单、基于HTTP基本认证、基于LDAP等。
    • 授权:提供了丰富的授权机制,可以对用户进行角色和权限分配,并支持细粒度的授权控制。
    • 安全漏洞防护:提供了一些安全漏洞的防护措施,如CSRF保护、跨域资源共享(CORS)等。
    • 单点登录:支持单点登录(SSO)和集成其他身份认证系统。
    • 记录管理:提供了日志记录功能,记录用户的登录信息和操作信息。
    • 集成第三方安全框架:Spring Security可以与其他安全框架集成,如OAuth2、OpenID Connect等。

    一般的Web项目都需要进行认证鉴权

    • 认证:验证当前访问系统的是不是本系统的用户,并且确认具体是哪一个用户
    • 授权:经过认证后判断当前用户是否具有权限进行某一个操作
  • Spring Security有哪些特点

    • 基于Spring框架:Spring Security是基于Spring框架构建的安全性框架,可以轻松地与Spring应用程序集成。
    • 灵活的身份验证和授权机制:Spring Security提供了多种身份验证和授权机制,如表单身份验证、HTTP基本认证、LDAP身份验证等,并且支持细粒度的授权管理。
    • 安全漏洞防护:Spring Security提供了许多防护措施来保护Web应用程序免受安全漏洞的攻击,如CSRF保护、CORS等。
    • 集成第三方安全框架:Spring Security可以很容易地与其他安全框架集成,如OAuth2、OpenID Connect等。
    • 支持单点登录(SSO):Spring Security支持单点登录,可以集成其他身份认证系统。
    • 易于扩展和定制:Spring Security提供了许多可扩展和可定制的接口和类,可以根据业务需求进行灵活的定制。
  • Apache Shiro和Spring Security的比较:

    • 功能复杂度:Spring Security提供了更丰富的安全功能,如OAuth、SAML、OpenID Connect等,而Apache Shiro提供的功能比较简单,不能满足一些复杂场景的需求。
    • 集成难度:相对而言,Apache Shiro集成到应用程序中的难度相对较小,而Spring Security的集成可能需要更多的配置和学习。
    • 学习曲线:Apache Shiro的学习曲线相对较低,适合初学者快速上手,而Spring Security的学习曲线可能相对较高,需要花费更多的时间和精力。
    • 社区活跃度:Spring Security的社区活跃度较高,有更多的开发者贡献代码和提供支持,而Apache Shiro的社区活跃度相对较低,缺乏一些新的特性和改进。

    综上所述,Apache Shiro和Spring Security都是Java安全框架,各自有其优缺点和适用场景。如果应用程序需要更复杂的安全功能,且开发者有足够的时间和精力学习和配置,可以选择Spring Security。如果应用程序的安全需求相对简单,或者需要快速实现安全功能,可以选择Apache Shiro。

2、SpringSecurity初体验

示例

在项目中引入SpringSecurity

  • Step1:搭建环境

    1)创建一个SpringBoot项目

    2)导入依赖

            <!--测试环境-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <!--web环境-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <!--SpringSecurity-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    
  • Step2:编写Controller

    package com.hhxy.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author ghp
     * @date 2023/3/7
     * @title
     * @description
     */
    @RestController
    @RequestMapping("/hello")
    public class HelloController {
    
        @GetMapping
        public String hello(){
            return "hello";
        }
    }
    
  • Step3:启动项目

    访问链接:http://localhost:8080/hello,会自动跳转到http://localhost:8080/login

    初识SpringSecurity

    账号默认是user,密码在控制台输出

    初识SpringSecurity

    登陆后,就能成功访问到 hello

    初识SpringSecurity

3、认证和授权

3.1 登录校验流程

初识SpringSecurity

  • 完整流程

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

    初识SpringSecurity

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

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

    • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedExceptionAuthenticationException

    • FilterSecurityInterceptor:负责权限校验的过滤器

    初识SpringSecurity

  • 认证流程详解

    初识SpringSecurity

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

3.2 登录认证实战

前置知识

  • 密码明文存储

    密码明文存储,需要在密码前添加{noop}

    初识SpringSecurity

    此时账号使用 张三 , 密码使用 123 ,就可以登录了

  • 密码加密存储

    实际项目中我们都是将密码进行加密存储的,保障数据的安全性。SpringSecurity默认使用的PasswordEncoder加密方式,要求数据库中的密码格式为:{id}password ,当我们将 id 设置为 noop 时,Security就知道我们的密码是没有进行加密的,这也是为什么密码明文存储要在密码前面加上{noop}的原因。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。

    我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该 对象 来进行密码校验。我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。

  • 大致思路

    • 登录

      ①自定义登录接口

      调用ProviderManager的方法进行认证 如果认证通过生成jwt

      把用户信息存入redis中(减轻数据库压力,提高性能)

      ②自定义UserDetailsService

      在这个实现类中去查询数据库

    • 校验

      ①定义Jwt认证过滤器

​ 获取token

​ 解析token获取其中的userid

​ 从redis中获取用户信息

​ 存入SecurityContextHolder

  • 具体实现思路

    • 登录:用户访问登录页面,提交登录请求(访问的请求是不被拦截的)

      ①在Controller层自定义一个登录接口(/user/login),

      ②调用Service层方法,将前端传过来的账号、密码封装成 token,

      ③然后调用 AuthenticationManager 类 的 authenticate 方法,对账号和密码进行校验(可以参考上面那张图)最终会返回一个Authentication 对象。(authenticate 底层是会直接调用 UserDetailsService 的方法,返回一个 UserDetails对象,然后通过 PasswordEncoder 对 UserDetails 中的账号密码进行校验,正确就将 UserDetails 存储到Authentictication 中,不正确就返回空)

      Authentication 为空则直接报一个异常,该异常会被全局异常处理器给捕获,然后返回相应的报错信息给前端;

      ④Authentication 不为空,根据 userId 生成 jwt,同时会讲Authentication对象存储到SecurityContextHolder.getContext()中(它是Security框架的一个上下文,类似于Session,用于后续认证),然后封装成一个map返回给前端

      ⑤在返回前需要将详细的用户数据存储到 redis 中

      注意事项

      1. /user/login 接口需要作放行处理,具体参考 com.hhxy.config.SecurityConfig#configure 方法
      2. AuthenticationManager 类需要被 IOC 容器管理,才能够使用
      3. 需要自定义 UserDetailsService ,这样就能够从数据库中获取用户数据了,因为SpringSecurity默认是从内存(Session)中获取数据
      4. 需要自定义 UserDetails ,用于 UserDetailsService 从数据库中查询后数据的封装
      5. 需要自定义 PasswordEncoder ,因为 SpringSecurity 默认的 PasswordEncoder 安全性不够高
    • 认证:用户访问非登录页面,请求被拦截进行认证(访问的请求是被拦截的)

      ①编一些一个Controller层的接口,用户访问 /user/hello

      ②会被自定义的拦截器 JwtAuthenticationTokenFilter 进行拦截,然后进行一系列的认证,判断用户当前是否含有token,不含有token说明未登录就直接放行,然后后续的 FilterSecurityInterceptor 会监测到该请求未获得权限,然后抛出异常

      ③用户拥有token,说明用户当前登录,然后会根据token 查询 redis ,获取用户信息,然后再将用户信息封装成 一个UsernamePasswordAuthenticationToken,最后存入SecurityContextHolder(刷新它里面的数据),最终就放行,用户完成权限认证

      注意事项

      1. fastjson版本要统一
      2. 要将自定义的过滤器设置在UsernamePasswordAuthenticationFilter之前
    • 退出功能

      ①编写一个Controller层的接口,用户访问 /user/logout

      ②通过SucurityContextHolder获取Authentication对象,然后获取其中的用户信息(用户id),根据用户id清空Redis中的用户信息,成功退出

准备工作

建库建表
创建工程
导入依赖
准备工具类
编写配置文件

搭建环境

1)建库(spring-security-study)建表(sys_user)

初识SpringSecurity

2)创建有SpringBoot工程

目录结构:

3)导入依赖


注意:SpringBoot版本不能太高,我使用2.6.x就报错了,使用2.5.x就没有报错了

3)编写配置文件


4)准备工具类

代码实现

编写Mapper
编写Service
编写Controller
编写Interceptor
编写配置类
编写实体类
  • Step1:编写配置类

    1)SecurityConfig

    2)RedisConfig

  • Step2:编写实体类

    1)User

    2)LoginUser

  • Step3:编写Mapper

    UserMapper

  • Step4:编写Service

    1)UserDetailsServiceImpl

    2)UserService、UserServiceImpl

    3)LoginService、LoginServiceImpl

  • Step5:编写Controller

    LoginController

3.3 授权

  • 主要步骤

    ①访问Controller,Controller的方法上要添加 @PreAuthorize("hasAuthority('xxx')"),用来鉴别当前登录用户所拥有的角色是否有’xxx‘权限,如果未拥有就直接拒绝访问。(底层是直接被自定义的 Jwt 过滤器给拦截,使用三个参数构造方法生成带有授权信息的token

    ②然后会经过 UserDetailsSeriveImpl , 到了这里会查询数据库获取授权信息,然后将他封装到 LoginUser 中,然后对比注解上的权限信息和数据库中查询到的权限信息,如果发生异常,就会被ExceptionTranslationFilter处理并返回对应的信息)

    ③配置两个自定义的异常处理器,AccessDeniedImpl(处理权限不足时产生的异常),AuthenticationEntryPointImpl(处理权限如认证失败时产生的异常)

    注意点

    1. @EnableGlobalMethodSecurity(prePostEnabled = true)开启基于方法的安全认证机制
    2. 对于对象类型的成员变量,我们需要忽略 JSON 序列,因为这里我没有配置Redis的序列化
    3. 要将自定义的异常处理器添加到SpringSecurity中
  • RBAC权限模型:RBAC(Role-Based Access Control)权限模型是一种广泛使用的访问控制机制,用于确定用户对计算机资源的访问权限。在RBAC中,访问控制是基于角色而不是基于个人的。每个用户被分配一个或多个角色,每个角色都有特定的权限,用户可以访问拥有其角色的权限。

    初识SpringSecurity

    一般需要使用五张表来存储,包括:用户表、用户角色关系表、角色表、角色权限关系表、权限表

4、跨域问题

在学习Vue时已经学习过了,这里不再赘述。之前在学习Vue时,主要是利用VueCLI解决跨域问题(本质是使用代理,此外还可以使用Nginx),这里我们将学习使用CORS来解决跨域问题。

  • CORS(Cross-Origin Resource Sharing,跨源资源共享)是一个系统,它由一系列传输的 HTTP 标头组成,这些 HTTP 标头决定浏览器是否阻止前端 JavaScript 代码获取跨源请求的响应。CORS 给了 web 服务器这样的权限,即服务器可以选择,允许跨源请求访问到它们的资源。

    特点:后端解决,需要浏览器和后端同时支持,请求分为复杂请求和简单请求

  • 实现思路

    ①编写跨域配置类

    package com.hhxy.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * @author ghp
     * @date 2023/3/7
     * @title 跨域配置类
     * @description 用于项目初始化测试
     */
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    
        /**
         * 配置跨域映射
         * @param registry
         */
        @Override
        public void addCorsMappings(CorsRegistry registry) {
          // 设置允许跨域的路径
            registry.addMapping("/**")
                    // 设置允许跨域请求的域名
                    .allowedOriginPatterns("*")
                    // 是否允许cookie
                    .allowCredentials(true)
                    // 设置允许的请求方式
                    .allowedMethods("GET", "POST", "DELETE", "PUT")
                    // 设置允许的header属性
                    .allowedHeaders("*")
                    // 跨域允许时间,单位 ms
                    .maxAge(3600);
        }
    }
    

    ②开启SpringSecurity的跨域访问

            //允许跨域
            http.cors();
    

5、其他相关知识

5.1 权限校验方法

前面我们认识了@PreAuthorize注解的hasAnyAuthority方法,它的作用是验证当前用户是否含有某种权限,此外该注解还提供了其它两种方法:hasAnyAuthorityhasRolehasAnyRole

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

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

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

自定义权限校验方法

前面我们直到SpringSecurity提供了4中权限校验方法给我们开发者使用,但是这些方法并不能满足所有的场景,比如我们要想在权限校验方法的时候使用通配符,这时候我们就需要自定义一个权限校验方法

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

@Component("ex")
public class SGExpressionRoot {

    public boolean hasAuthority(String authority){
        //获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
}

​ 在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法

    @RequestMapping("/hello")
    @PreAuthorize("@ex.hasAuthority('system:dept:list')")
    public String hello(){
        return "hello";
    }

5.2 CSRF

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

​ https://blog.csdn.net/freeking101/article/details/86537087

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

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

5.3 其它授权和认证方式

前面我们是使用 SpringSecurity+JWT 实现认证和授权。现在我们剥离出JWT,只是单纯使用SpringSecurity。

需要注意的是:单独使用SpringSecurity需要依赖于UsernamePasswordAuthenticationFilter(SpringSecurity提供的),而前面我们在使用JWT的时候,并没有使用到这个类,而是单独定义了一个拦截器,用来做 JWT 校验。

主流的方案:SpringSecurity+JWT

  • 方案一:SpringSecurity+JWT,自定义一个JwtAuthenticationTokenFilter
  • 方案二:使用SpringSecurity默认提供的方案,依赖于UsernamePasswordAuthenticationFilter
  • 方式三:重写UsernamePasswordAuthenticationFilter,然后进行JWT认证