本文主要是对SpringSecurity的一个粗略的学习,大致学习使用SpringBoot整合SpringSecurity和Redis实现一个简单的登录认证以及授权功能,同时熟悉登录认证和授权的一个大概的流程是怎么样的,其次还会学习使用CORS解决跨域问题。至于更加详细的内容会在后面进一步学习
PS: 相关代码请参考博主的 Gitee仓库或者Github仓库
什么是Spring Security?
Spring Security是一个基于Spring框架的安全性框架,提供了在Java应用程序中进行身份验证、授权和其他安全性功能的支持。它在Web应用程序和非Web应用程序中都可以使用,并且支持多种身份验证和授权机制。Spring Security可以轻松地与Spring应用程序集成,为开发人员提供了一种可靠、可扩展和易于使用的方式来保护其应用程序。
Spring Security有哪些作用?
一般的Web项目都需要进行认证和鉴权
Spring Security有哪些特点?
Apache Shiro和Spring Security的比较:
综上所述,Apache Shiro和Spring Security都是Java安全框架,各自有其优缺点和适用场景。如果应用程序需要更复杂的安全功能,且开发者有足够的时间和精力学习和配置,可以选择Spring Security。如果应用程序的安全需求相对简单,或者需要快速实现安全功能,可以选择Apache Shiro。
示例:
在项目中引入SpringSecurity
Step1:搭建环境
1)创建一个SpringBoot项目
2)导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
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
完整流程
SpringSecurity的本质其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
备注:图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示
UsernamePasswordAuthenticationFilter
:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责
ExceptionTranslationFilter
:处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
FilterSecurityInterceptor
:负责权限校验的过滤器
认证流程详解
Authentication
接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息AuthenticationManager
接口:定义了认证Authentication的方法UserDetailsService
接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法UserDetails
接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中前置知识
密码明文存储
密码明文存储,需要在密码前添加
{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 中
注意事项:
认证:用户访问非登录页面,请求被拦截进行认证(访问的请求是被拦截的)
①编一些一个Controller层的接口,用户访问 /user/hello
②会被自定义的拦截器 JwtAuthenticationTokenFilter 进行拦截,然后进行一系列的认证,判断用户当前是否含有token,不含有token说明未登录就直接放行,然后后续的 FilterSecurityInterceptor 会监测到该请求未获得权限,然后抛出异常
③用户拥有token,说明用户当前登录,然后会根据token 查询 redis ,获取用户信息,然后再将用户信息封装成 一个UsernamePasswordAuthenticationToken,最后存入SecurityContextHolder(刷新它里面的数据),最终就放行,用户完成权限认证
注意事项:
退出功能:
①编写一个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
主要步骤
①访问Controller,Controller的方法上要添加 @PreAuthorize("hasAuthority('xxx')")
,用来鉴别当前登录用户所拥有的角色是否有’xxx‘权限,如果未拥有就直接拒绝访问。(底层是直接被自定义的 Jwt 过滤器给拦截,使用三个参数构造方法生成带有授权信息的token
②然后会经过 UserDetailsSeriveImpl , 到了这里会查询数据库获取授权信息,然后将他封装到 LoginUser 中,然后对比注解上的权限信息和数据库中查询到的权限信息,如果发生异常,就会被ExceptionTranslationFilter处理并返回对应的信息)
③配置两个自定义的异常处理器,AccessDeniedImpl(处理权限不足时产生的异常),AuthenticationEntryPointImpl(处理权限如认证失败时产生的异常)
注意点:
@EnableGlobalMethodSecurity(prePostEnabled = true)
开启基于方法的安全认证机制RBAC权限模型:RBAC(Role-Based Access Control)权限模型是一种广泛使用的访问控制机制,用于确定用户对计算机资源的访问权限。在RBAC中,访问控制是基于角色而不是基于个人的。每个用户被分配一个或多个角色,每个角色都有特定的权限,用户可以访问拥有其角色的权限。
一般需要使用五张表来存储,包括:用户表、用户角色关系表、角色表、角色权限关系表、权限表
在学习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();
前面我们认识了@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";
}
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攻击也就不用担心了。
前面我们是使用 SpringSecurity+JWT 实现认证和授权。现在我们剥离出JWT,只是单纯使用SpringSecurity。
需要注意的是:单独使用SpringSecurity需要依赖于
UsernamePasswordAuthenticationFilter
(SpringSecurity提供的),而前面我们在使用JWT的时候,并没有使用到这个类,而是单独定义了一个拦截器,用来做 JWT 校验。主流的方案:SpringSecurity+JWT