目录
一、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
官网: Spring Security Reference
jjwt工具类:(1条消息) jjwt工具类_S Y H的博客-CSDN博客_jjwt工具类
redis配置和工具类 :(1条消息) redis操作模板_S Y H的博客-CSDN博客_redis模板
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的 成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方 案。 正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控 制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权 (Authorization)两个部分,这两点也是 Spring Security 重要核心功能。 (1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问 该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认 证过程。通俗点说就是系统认为用户是否能登录 (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 同款产品对比
SpringSecurity 特点:
ApacheShiro
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 以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
com.baomidou
mybatis-plus-boot-starter
3.5.2
com.mysql
mysql-connector-j
org.projectlombok
lombok
org.springframework.boot
spring-boot-starter-test
org.springframework.security
spring-security-test
io.jsonwebtoken
jjwt
0.9.1
org.apache.commons
commons-lang3
3.12.0
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-data-redis
2.5.14
com.alibaba
fastjson
2.0.19
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 );
@RestController
@RequestMapping("/api/TestController")
public class TestController {
@GetMapping("/test1")
public ResultUtils test1() {
return ResultUtils.success(RespStaticEnum.SUCCESS);
}
}
默认用户名:user
密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都回发生变化!
Security密码校验方式优先级 配置文件 --> 配置类 --> UserDetailsService实现类
通过访问项目测试发现,前两种方式输入正确的账号和密码都能正常访问。但是实际工作中肯定不能这样使用
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 usersLambdaQueryWrapper = new LambdaQueryWrapper<>();
// 根据用户名称查询用户信息
usersLambdaQueryWrapper.eq(Users::getUsername, username);
Users users = loginMapper.selectOne(usersLambdaQueryWrapper);
// users==null登录失败,users!=null登陆成功
if(users == null) {
throw new UsernameNotFoundException("登录失败!");
}
List 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的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
是个异常过滤器,处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
是一个方法级的权限过滤器, 基本位于过滤链的最底部
当什么也没有配置的时候,账号和密码是由 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 getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();
}
(2)以下是 UserDetails 实现类
以后我们只需要使用 User 这个实体类即可!
方法参数 username,表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无法接收。
(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);
}
@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;
}
@Service
public class SecurityLoginService implements UserDetailsService{
@Autowired
private LoginMapper loginMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper 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 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;
}
}
实际项目中我们不会把密码明文存储在数据库中。
默认使用的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();
}
}
第一步:放行登录接口
@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
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的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 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);
}
}
第三步:测试
我们只需要定义一个登陆接口,然后获取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中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限即可。
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@Configuration
// 开启security权限配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
@Data
public class LoginUser implements UserDetails {
/*
* 自定义用户对象
* */
private Users users;
/*
* 权限信息集合
* */
private List authorityList;
public LoginUser(Users users, List authorityList) {
this.users = users;
this.authorityList = authorityList;
}
/*
* 权限信息
* */
@Override
public Collection getAuthorities() {
// 将权限信息集合封装为GrantedAuthority集合
List grantedAuthorityList = new ArrayList<>();
authorityList.forEach((authorityStr) -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authorityStr);
grantedAuthorityList.add(simpleGrantedAuthority);
});
return grantedAuthorityList;
}
@Service
public class SecurityLoginService implements UserDetailsService{
@Autowired
private LoginMapper loginMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper 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 authorityList = loginMapper.getAuthority(users.getId());
return new LoginUser(users, authorityList);
}
}
数据权限sql
@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 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);
}
}
/*
* 权限测试接口
* */
@PreAuthorize("hasAnyAuthority('menu:user')")
@GetMapping("/authority")
public ResultUtils authority(){
return ResultUtils.success(RespStaticEnum.SUCCESS, "权限校验成功");
}
我们前面都是使用@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";
}
我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。
@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 authorityList = loginUser.getAuthorityList();
// 权限校验
boolean contains = authorityList.contains(authorities1);
boolean contains1 = authorityList.contains(authorities2);
// 两个权限不一样,并且用户存在这两个权限
return !authorities1.equals(authorities2) && contains && contains1;
}
}
/*
* 自定义权限测试接口
* */
@PreAuthorize("@esPermissionHandlers.hasAnyAuthority('menu:system', 'menu:user')")
@GetMapping("/permission")
public ResultUtils permission() {
return ResultUtils.success(RespStaticEnum.SUCCESS, "自定义权限测试接口");
}
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
/*
* 认证失败处理器
* */
@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博客
@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); // 权限校验处理器
}
测试发现权限校验处理器没有生效,最后使用的全局异常处理器捕获的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是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
CSRF攻击与防御(写得非常好)_擒贼先擒王的博客-CSDN博客
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
项目中关闭CSRF原因:
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。