文章目录
初识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
账号默认是
user
,密码在控制台输出登陆后,就能成功访问到 hello
3、认证和授权
3.1 登录校验流程
-
完整流程
SpringSecurity的本质其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
备注:图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示
-
UsernamePasswordAuthenticationFilter
:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责 -
ExceptionTranslationFilter
:处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
-
FilterSecurityInterceptor
:负责权限校验的过滤器
-
-
认证流程详解
-
Authentication
接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息 -
AuthenticationManager
接口:定义了认证Authentication的方法 -
UserDetailsService
接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法 -
UserDetails
接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中
-
3.2 登录认证实战
前置知识
密码明文存储
密码明文存储,需要在密码前添加
{noop}
:此时账号使用 张三 , 密码使用 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 中
注意事项:
- /user/login 接口需要作放行处理,具体参考 com.hhxy.config.SecurityConfig#configure 方法
- AuthenticationManager 类需要被 IOC 容器管理,才能够使用
- 需要自定义 UserDetailsService ,这样就能够从数据库中获取用户数据了,因为SpringSecurity默认是从内存(Session)中获取数据
- 需要自定义 UserDetails ,用于 UserDetailsService 从数据库中查询后数据的封装
- 需要自定义 PasswordEncoder ,因为 SpringSecurity 默认的 PasswordEncoder 安全性不够高
-
认证:用户访问非登录页面,请求被拦截进行认证(访问的请求是被拦截的)
①编一些一个Controller层的接口,用户访问 /user/hello
②会被自定义的拦截器 JwtAuthenticationTokenFilter 进行拦截,然后进行一系列的认证,判断用户当前是否含有token,不含有token说明未登录就直接放行,然后后续的 FilterSecurityInterceptor 会监测到该请求未获得权限,然后抛出异常
③用户拥有token,说明用户当前登录,然后会根据token 查询 redis ,获取用户信息,然后再将用户信息封装成 一个UsernamePasswordAuthenticationToken,最后存入SecurityContextHolder(刷新它里面的数据),最终就放行,用户完成权限认证
注意事项:
- fastjson版本要统一
- 要将自定义的过滤器设置在UsernamePasswordAuthenticationFilter之前
-
退出功能:
①编写一个Controller层的接口,用户访问 /user/logout
②通过SucurityContextHolder获取Authentication对象,然后获取其中的用户信息(用户id),根据用户id清空Redis中的用户信息,成功退出
-
准备工作
搭建环境
1)建库(spring-security-study)建表(sys_user)
2)创建有SpringBoot工程
目录结构:
3)导入依赖
注意:SpringBoot版本不能太高,我使用2.6.x就报错了,使用2.5.x就没有报错了
3)编写配置文件
4)准备工具类
代码实现
-
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(处理权限如认证失败时产生的异常)
注意点:
-
@EnableGlobalMethodSecurity(prePostEnabled = true)
开启基于方法的安全认证机制 - 对于对象类型的成员变量,我们需要忽略 JSON 序列,因为这里我没有配置Redis的序列化
- 要将自定义的异常处理器添加到SpringSecurity中
-
-
RBAC权限模型:RBAC(Role-Based Access Control)权限模型是一种广泛使用的访问控制机制,用于确定用户对计算机资源的访问权限。在RBAC中,访问控制是基于角色而不是基于个人的。每个用户被分配一个或多个角色,每个角色都有特定的权限,用户可以访问拥有其角色的权限。
一般需要使用五张表来存储,包括:用户表、用户角色关系表、角色表、角色权限关系表、权限表
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
方法,它的作用是验证当前用户是否含有某种权限,此外该注解还提供了其它两种方法:hasAnyAuthority
、hasRole
,hasAnyRole
-
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认证