SpringSecurity

快速入门

  1. 介绍

springsecurity是安全框架,准确来说是安全管理框架。相比与另外一个安全框架Shiro,springsecurity提供了更丰富的功能,社区资源也比Shiro丰富

springsecurity框架用于Web应用的需要进行认证和授权

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作。认证和授权也是SpringSecurity作为安全框架的核心功能

认证和授权也是SpringSecurity作为安全框架的核心功能

认证

  1. springsecurity的权限管理,是先认证后授权,所以我们先学习认证这一部分
    流程图如下,注意下图的jwt指的是 json web token,jwt是登录校验的时候用的技术,可以根据指定的算法进行信息的加密和解密
  2. image-20240330220101525
  3. springsecurity原理
    1. SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。例如快速入门案例里面使用到的三种过滤器,如下图 (监听器 -> 过滤器链 -> dispatcherservlet(前置拦截器 -> mapperHandle -> 后置拦截器 -> 最终拦截器)
    2. image-20240330220241389
    3. 一、UsernamePasswordAuthenticationFilter: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责
      二、ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
      三、FilterSecurityInterceptor:负责权限校验的过滤器
      注意上图,橙色部分表示认证,黄色部分表示异常处理,红色部分表示授权
      如何查看security提供的过滤器有哪几个,或者叫哪几种,如下
    4. image-20240330220339383

认证流程

我们来详细学一下上面 ‘1. springsecurity原理’ 的橙色部分,也就是认证那部分的知识
一、Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息
二、AuthenticationManager接口:定义了认证Authentication的方法
三、UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法
四、UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中

image-20240330220441851

image-20240331171316538

image-20240331171335813

自定义security的思路

在 ‘快速入门’ 中,我们在Boot项目里面引入了Security依赖,实现了当我们访问某个业务接口时,会被Security的login接口拦截,但是如果我们不想使用Security默认的登录页面,那么怎么办呢,还有,springsecurity的校验,我们希望是根据==数据库来做校验==,那么怎么实现呢。我们需要实现如下
【登录-未实现】
①、自定义登录接口。用于调用ProviderManager的方法进行认证 如果认证通过生成jwt,然后把用户信息存入redis中
②、自定义UserDetailsService接口的实现类。在这个实现类中去查询数据库
【校验-未实现】
①、定义Jwt认证过滤器。用于获取token,然后解析token获取其中的userid,还需要从redis中获取用户信息,然后存入SecurityContextHolder

自定义通过数据库查询登录认证

上面我们已经把准备工作做好了,包括搭建、代码、数据库。接下来我们会实现让security在认证的时候,根据我们数据库的用户和密码进行认证,也就是被security拦截业务接口,出现登录页面之后,我们需要通过输入数据库里的用户和密码来登录,而不是使用security默认的用户和密码进行登录
思路: 只需要新建一个实现类,在这个实现类里面实现Security官方的UserDetailsService接口,然后重写里面的loadUserByUsername方法
注意: 重写好loadUserByUsername方法之后,我们需要把拿到 ‘数据库与用户输入的数据’ 进行比对的结果,也就是user对象这个结果封装成能被 ‘Security官方的UserDetailsService接口’ 接收的类型,例如可以封装成我们下面写的LoginUser类型。然后才能伪装好数据,给Security官方的认证机制去对比user对象与数据库的结果是否匹配。Security官方的认证机制会拿LoginUser类的方法数据(数据库拿,不再用默认的),跟我们封装过去的user对象进行匹配,要使匹配一致,就证明认证通过,也就是用户在浏览器页面输入的用户名和密码能被Security认证通过,就不再拦截该用户去访问我们的业务接口

  1. 第一步: 在domain目录新建LoginUser类,作为UserDetails接口(Security官方提供的接口)的实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.huanf.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
* @author 35238
* @date 2023/7/11 0011 20:59
*/
@Data //get和set方法
@NoArgsConstructor //无参构造
@AllArgsConstructor //带参构造
//实现UserDetails接口之后,要重写UserDetails接口里面的7个方法
public class LoginUser implements UserDetails {

private User xxuser;

@Override
//用于返回权限信息。现在我们正在学'认证','权限'后面才学。所以返回null即可
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
//用于获取用户密码。由于使用的实体类是User,所以获取的是数据库的用户密码
public String getPassword() {
return xxuser.getPassword();
}

@Override
//用于获取用户名。由于使用的实体类是User,所以获取的是数据库的用户名
public String getUsername() {
return xxuser.getUserName();
}

@Override
//判断登录状态是否过期。把这个改成true,表示永不过期
public boolean isAccountNonExpired() {
return true;
}

@Override
//判断账号是否被锁定。把这个改成true,表示未锁定,不然登录的时候,不让你登录
public boolean isAccountNonLocked() {
return true;
}

@Override
//判断登录凭证是否过期。把这个改成true,表示永不过期
public boolean isCredentialsNonExpired() {
return true;
}

@Override
//判断用户是否可用。把这个改成true,表示可用状态
public boolean isEnabled() {
return true;
}
}
  1. 第二步: 在 src/main/java/com.huanf 目录新建 service.impl.MyUserDetailServiceImpl 类,写入如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.huanf.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.huanf.domain.LoginUser;
import com.huanf.domain.User;
import com.huanf.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
* @author 35238
* @date 2023/7/11 0011 20:39
*/
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {

@Autowired
private UserMapper userMapper;

@Override
//UserDetails是Security官方提供的接口
public UserDetails loadUserByUsername(String xxusername) throws UsernameNotFoundException {

//查询用户信息。我们写的userMapper接口里面是空的,所以调用的是mybatis-plus提供的方法
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
//eq方法表示等值匹配,第一个参数是数据库的用户名,第二个参数是我们传进来的用户名,这两个参数进行比较是否相等
queryWrapper.eq(User::getUserName,xxusername);
User user = userMapper.selectOne(queryWrapper);
//如果用户传进来的用户名,但是数据库没有这个用户名,就会导致我们是查不到的情况,那么就进行下面的判断。避免程序安全问题
if(Objects.isNull(user)){//判断user对象是否为空。当在数据库没有查到数据时,user就会为空,也就会进入这个判断
throw new RuntimeException("用户名或者密码错误");
}

//把查询到的user结果,封装成UserDetails类型,然后返回。
//但是由于UserDetails是个接口,所以我们先需要在domino目录新建LoginUser类,作为UserDetails的实现类,再写下面那行
return new LoginUser(user);
}
}

5.

密码加密校验问题

  1. 上面我们实现了自定义Security的认证机制,让Security根据数据库的数据,来认证用户输入的数据是否正确。但是当时存在一个问题,就是我们在数据库存入用户表的时候,插入的huanf用户的密码是 {noop}112233,为什么用112233不行呢

  2. 原因: SpringSecurity默认使用的PasswordEncoder要求数据库中的密码格式为:{加密方式}密码。对应的就是{noop}112233,实际表示的是112233
    但是我们在数据库直接暴露112233为密码,会造成安全问题,所以我们需要把加密后的1234的密文当作密码,此时用户在浏览器登录时输入1234,我们如何确保用户能够登录进去呢,答案是==SpringSecurity默认的密码校验,替换为SpringSecurity为我们提供的BCryptPasswordEncoder==

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.huanf.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @author 35238
* @date 2023/7/11 0011 22:02
*/
@Configuration
//实现Security提供的WebSecurityConfigurerAdapter类,就可以改变密码校验的规则了
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
//把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验
//注意也可以注入PasswordEncoder,效果是一样的,因为PasswordEncoder是BCry..的父类
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

}

登录接口的分析

在上面学习的 ‘认证’ 的 ‘3. 自定义security的思路’ 当中,我们有一个功能需求是自定义登录接口,这个功能还没有实现,我们需要实现这个功能,但是,实现这个功能需要使用到jwt,我们刚刚也学习了使用jwt来实现加密校验,那么下面就正式学习如何实现这个登录接口,首先是分析,如下

  1. 我们需要自定义登陆接口,也就是在controller目录新建LoginController类,在controller方法里面去调用service接口,在service接口实现AuthenticationManager去进行用户的认证,注意,我们定义的controller方法要让SpringSecurity对这个接口放行(如果不放行的话,会被SpringSecurity拦截),让用户访问这个接口的时候不用登录也能访问
  2. 在service接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器
  3. 认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

认证过滤器

  1. 在上面学习的 ‘认证’ 的 ‘3. 自定义security的思路’ 当中,我们有一个功能需求是定义Jwt认证过滤器,这个功能还没有实现,下面就正式学习如何实现这个功能。要实现Jwt认证过滤器,我们需要获取token,然后解析token获取其中的userid,还需要从redis中获取用户信息,然后存入SecurityContextHolder
    为什么要有redis参与: 是为了防止过了很久之后,浏览器没有关闭,拿着token也能访问,这样不安全
  2. 认证过滤器的作用是什么: 上面我们实现登录接口的时,当某个用户登录之后,该用户就会有一个token值,我们可以通过认证过滤器,由于有token值,并且token值认证通过,也就是证明是这个用户的token值,那么该用户访问我们的业务接口时,就不会被Security拦截。简单理解作用就是登录过的用户可以访问我们的业务接口,拿到对应的资源

JWT认证过滤器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    @Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = httpServletRequest.getHeader("token");
if (StrUtil.isEmpty(token)) {
// 如果token为空放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
// 解析token
String userId;
try {
userId = String.valueOf(JwtUtil.parseJWT(token));
} catch (Exception e) {
throw new RuntimeException("用户未登录");
}
// 认证用户,从redis中获取用户信息
String key = "login:"+userId;
LoginUser loginUser = redisCache.getCacheObject(key);
// 存入SecurityContextHolder中
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}

1
2
3
4
5
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
// 添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);

退出登录

上面我们既测试了登录认证,又实现了基于密文的token认证,到此就完整地实现了我们在 ‘认证’ 的 ‘3. 自定义security的思路’ 里面的【登录】和【校验】的功能
那么,我们怎么退出登录呢,也就是让某个用户的登录状态消失,也就是让token失效 ?
实现起来也比较简单,只需要定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可
注意: 我们的token其实就是用户密码的密文,token是存在redis里面

退出登录实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 登出
* @return 200
*/
@Override
public ResponseResult logout() {
// 从SecurityContextHolder中取出LoginUser,得到userid;
UsernamePasswordAuthenticationToken authentication
= (UsernamePasswordAuthenticationToken) SecurityContextHolder
.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String userId = loginUser.getUser().getId().toString();

//根据key从Redis中删除用户信息
String key = "login:"+userId;
redisCache.deleteObject(key);

return new ResponseResult<>(200,"注销成功");
}

授权

权限系统的作用

为什么要设计权限系统 ?

  1. 例如要设计一个图书管理系统,普通学生账号的权限不能使用书籍编辑、删除的功能,普通学生能使用的功能仅仅是浏览页面,但是,如果是图书管理员用户,那么就能使用所有权限。简单理解就是我们需要不同的用户使用不同的功能,这就是权限系统要实现的效果

  2. 虽然前端也可以去判断用户的权限来选择是否显示某些功能的页面或组件,但是不安全,因为如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作,所以我们还需要在后端进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作

前端防君子,后端防小人

授权的基本流程

  1. 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限
  2. image-20240331180409956
  3. 所以我们在项目中只==需要把当前登录用户的权限信息也存入Authentication,然后设置我们的资源所需要的权限即可==

自定义访问路径的权限

  1. 第一步: 在SecurityConfig配置类添加如下,作用是开启相关配置
1
@EnableGlobalMethodSecurity(prePostEnabled = true)
  1. 第二步: 开启了相关配置之后,就能使用@PreAuthorize等注解了。在HelloController类的hello方法,添加如下注解,其中test表示自定义权限的名字
1
@PreAuthorize("hasAuthority('test')")

带权限访问的实现

==权限信息: 有特殊含义的字符串==

  1. 我们前面在登录时,会调用到MyUserDetailServiceImpl类的loadUserByUsername方法,当时我们写loadUserByUsername方法时,只写了查询用户数据信息的代码,还差查询用户权限信息的代码。在登录完之后,因为携带了token,所以需要在JwtAuthenticationTokenFilter类添加'获取权限信息封装到Authentication中'的代码,添加到UsernamePasswordAuthenticationToken的第三个参数里面,我们当时第三个参数传的是null。
  2. 第一步: 在上面的’3. 自定义访问路径的权限’,我们给HelloController类的 /hello 路径添加了权限限制,只有用户具有叫test的权限,才能访问这个路径
  3. image-20240331202821732
  4. image-20240331202836844
  5. image-20240331202852958
  6. image-20240331202939739

授权-RBAC权限模型

  1. 介绍

​ 刚刚我们实现了只有当用户具备某种权限,才能访问我们的某个业务接口。但是存在一个问题,我们在给用户设置权限的时候,是写死的,在真正的开发中,我们是需要从数据库查询权限信息,下面就来学习如何从数据库查询权限信息,然后封装给用户。这个功能需要先准备好数据库和java代码,所以,下面的 ‘授权-RBAC权限模型' 都是在围绕这个功能进行学习,直到实现这个功能

  1. image-20240331203158097
  2. RBAC权限模型 (Role-Based Access Control) ,是权限系统用到的经典模型,基于角色的权限控制。该模型由以下五个主要组成部分构成:
    一、用户: 在系统中代表具体个体的实体,可以是人员、程序或其他实体。==用户需要访问系统资源==
    二、角色: 角色是权限的集合,用于定义一组相似权限的集合。角色可以被赋予给用户,从而授予用户相应的权限
    三、权限: 权限表示系统中具体的操作或功能,例如==读取、写入、执行==等。每个权限定义了对系统资源的访问规则
    四、用户-角色映射: 用户-角色映射用于表示用户与角色之间的关系。通过为用户分配适当的角色,用户可以获得与角色相关联的权限
    五、角色-权限映射: 角色-权限映射表示角色与权限之间的关系。每个角色都被分配了一组权限,这些权限决定了角色可执行的操作
    截止目前,我们数据库只有1张表,在上面 ‘认证’ 的 ‘6. 自定义security的数据库’ 里面创建的 sys_user 用户表,下面我们会新增4张表,分别是权限表(每条数据是单个’粒度细的权限’)、角色表(每条数据是多个’粒度细的权限’)、角色表与权限表的中间表、用户表与角色表的中间表。总共5张表,组成了RBAC模型,中间表的作用是维护两张表的多对多关系
  3. image-20240331204136777

授权-RBAC权限模型 实现

image-20240331213324279

image-20240331213417654

image-20240331213431938

image-20240331213446477

image-20240331213505239

自定义异常处理

  1. 上面的我们学习了 ‘认证’ 和 ‘授权’,实现了基本的权限管理,然后也学习了从数据库获取授权的 ‘授权-RBAC权限模型’,实现了从数据库获取用户具备的权限字符串。到此,我们完整地实现了权限管理的功能,但是,当认证或授权出现报错时,我们希望响应回来的json数据有实体类的code、msg、data这三个字段,怎么实现呢
  2. 我们需要学习Spring Security的异常处理机制,就可以在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理

image-20240331213704980

  1. 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到,如上图。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常,其中有如下两种情况
    一、如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
    二、如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
  2. ==总结==: 如果我们需要自定义异常处理,我们只需要创建AuthenticationEntryPointAccessDeniedHandler的实现类对象,然后==配置给SpringSecurity即可==

自定义异常处理代码实现

  1. 自定义认证和授权异常的统一响应结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 自定义认证失败的响应结果,使响应结果统一
* @param httpServletRequest request
* @param httpServletResponse Response
* @param e e
*
*/
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 封装统一响应结果,并转换成JSON字符串返回
ResponseResult result = new ResponseResult(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,请重新登录");
String jsonString = JSON.toJSONString(result);
// 调用工具类,将响应结果返回给前端
WebUtils.renderString(httpServletResponse,jsonString);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 自定义授权失败的响应结果,使响应结果统一
* @param httpServletRequest request
* @param httpServletResponse Response
* @param e e
*
*/
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 封装统一响应结果,并转换成JSON字符串返回
ResponseResult result = new ResponseResult(HttpStatus.HTTP_FORBIDDEN, "您的权限不足");
String jsonString = JSON.toJSONString(result);
// 调用工具类,将响应结果返回给前端
WebUtils.renderString(httpServletResponse,jsonString);
}
1
2
3
4
5
// 依赖注入AuthenticationEntryPoint,AccessDeniedHandler
// 自定义异常处理,我们只需要创建`AuthenticationEntryPoint`和
// `AccessDeniedHandler`的实现类对象,然后配置给SpringSecurity即可
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);

跨域

我这里跟教程视频的不一致,教程视频大概想讲的是只有同时配置springboot和security的跨域,那么外界用户才能访问我们的接口,但是我实际操作的却不一致,不清楚是不是版本的问题,总之都学一下吧,我的操作证明出来的是security的跨域写了跟没写都无效,只要是boot配置了跨域,那么跨域问题就解决了

跨域的后端解决

  1. 由于我们的SpringSecurity负责所有请求和资源的管理,当请求经过SpringSecurity时,如果SpringSecurity不允许跨域,那么也是会被拦截,所以下面我们将学习并解决跨域问题。前面我们在测试时,是在postman测试,因此没有出现跨域问题的情况,postman只是负责发请求跟浏览器没关系
    浏览器出于安全的考虑,使用 XMLHttpRequest 对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题
  2. 我们要实现如下两个需求 (我实际做出的效果跟教程视频不一致,第二个需求其实没必要存在,boot解决了跨域就都解决了):
    1、开启SpringBoot的允许跨域访问
    2、开启SpringSecurity的允许跨域访问
    第一步: 开启SpringBoot的允许跨域访问。在 config 目录新建 CorsConfig 类,写入如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author 35238
* @date 2023/7/14 0014 20:17
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
//重写spring提供的WebMvcConfigurer接口的addCorsMappings方法
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}

第二步: 开启SpringSecurity的允许跨域访问。在把 SecurityConfig 修改为如下

1
http.cors();

授权-权限校验的方法

  1. 学的是HelloController类的 @PreAuthorize注解 的三个方法
    我们前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。SpringSecurity还为我们提供了其它方法例如: hasAnyAuthorityhasRolehasAnyRole

image-20240331223847151

自定义权限校验的方法

在上面的源码中,我们知道security校验权限的PreAuthorize注解,其实就是获取用户权限,然后跟业务接口的权限进行比较,最后返回一个布尔类型。自定义一个权限校验方法的话,就需要新建一个类,在类里面定义一个方法,按照前面学习的三种方法的定义格式,然后返回值是布尔类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 自定义权限类
*/
@Component("ex")
public class MyExpressionRoot {

public boolean hasAuthority(String authority){
// 获取用户权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
// 判断用户是否拥有该权限
return permissions.contains(authority);
}
}
1
2
3
// 使用自定义权限功能
//需要在SPEL表达式来获取容器中bean的名字
@PreAuthorize("@ex.hasAuthority('system:dept:list')")

基于配置的权限控制

image-20240401170036777

防护CSRF攻击

在SecurityConfig类里面的configure方法里面,有一个配置如下,我们上面都没有去学习,下面就来了解一下

1
http.csrf().disable(); 

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

image-20240401170815133

防护: SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了.

认证-自定义处理器

上面我们已经完整学习了认证和授权的完成流程,如果自己待的公司不是用上面学习的那一套认证和授权的流程,那么就是使用的自定义处理器,下面是认证的3个自定义处理器,学完之后就会发现自定义处理器的操作其实都是大同小异

自定义successHandler

successHandler表示 ‘登录认证成功的处理器’,登录认证失败的处理器也是同理
实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。我们也可以自己去自定义成功处理器进行成功后的相应处理

image-20240401172902366

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author 35238
* @date 2023/7/16 0016 21:13
*/
@Component
//官方提供的AuthenticationSuccessHandler接口的实现类,用于自定义'登录成功的处理器'
public class MySuccessHandler implements AuthenticationSuccessHandler {

@Override
//实现security官方提供的AuthenticationSuccessHandler接口的下面这个抽象方法
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//response是认证登录的请求响应对象,authentication是认证之后的对象
//我们只验证认证成功之后被调用到下面那行代码就行,如果是要自定义'登录成功的处理器',那么就在下面写具体代码即可
System.out.println("登录认证成功了^V^");

}
}

到security配置类中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
//SpringSecurity的配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
//注入security官方提供的AuthenticationSuccessHandler接口
private AuthenticationSuccessHandler xxsuccessHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http); //默认写的就这一行

//上面注入的接口作为successHandler方法的参数写进来。注意最重要的是'formLogin登录表单',不然链式编程写不下去
//这样就相当于把刚刚在MySuccessHandler类里面的"自定义'登录成功的处理器'",配置给security了
http.formLogin().successHandler(xxsuccessHandler);

//其它默认的认证接口,例如业务接口的认证限制,要配,因为你重写自定义了之后,原有的配置都被覆盖,不写的话业务接口就没有security认证拦截的功能了
//我的截图里面没有下面这一行的代码,希望你们不要漏写
http.authorizeRequests().anyRequest().authenticated();
}

}

自定义LogoutSuccessHandler

LogoutSuccessHandlerr ‘登出成功的处理器’
有个过滤器叫DefaultLogoutPageGeneratingFilter,是登出操作的过滤器,可以指定登出(也就是退出登录的意思)成功后的处理。有个接口叫LogoutSuccessHandler接口,我们需要新建一个类,然后实现这个接口,重新接口里面的方法,就能实现自定义LogoutSuccessHandler
具体操作如下
第一步: 在 handler 目录新建 MyLogoutSuccessHandler 类,写入如下

1
2
3
4
5
6
7
8
9
@Component
//官方提供的LogoutSuccessHandler接口的实现类,用于自定义'登出成功的处理器'
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//我们只验证登出成功之后被调用到下面那行代码就行,如果是要自定义'登出成功的处理器',那么就在下面写具体代码即可
System.out.println("退出登录成功");
}
}

第二步: 把刚刚创建的MyLogoutSuccessHandler实现类,配置给security。把 SecurityConfig 类,修改为如下,主要就是添加了登出成功的处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Configuration
//SpringSecurity的配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
//注入security官方提供的AuthenticationSuccessHandler接口
private AuthenticationSuccessHandler xxsuccessHandler;

@Autowired
//注入security官方提供的AuthenticationSuccessHandler接口
private AuthenticationFailureHandler xxfailureHandler;

@Autowired
//注入security官方提供的LogoutSuccessHandler接口
private LogoutSuccessHandler xxlogoutSuccessHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http); //默认写的就这一行

//上面注入的接口作为successHandler方法的参数写进来。注意最重要的是'formLogin登录表单',不然链式编程写不下去
//这样就相当于把刚刚在MySuccessHandler类里面的"自定义'登录成功的处理器'",配置给security了
http.formLogin()
//登录认证成功的处理器
.successHandler(xxsuccessHandler)
//登录认证失败的处理器
.failureHandler(xxfailureHandler);

//登出成功的处理器的配置
http.logout()
//登出成功的处理器
.logoutSuccessHandler(xxlogoutSuccessHandler);


//其它默认的认证接口,例如业务接口的认证限制,要配,因为你重写自定义了之后,原有的配置都被覆盖,不写的话业务接口就没有security认证拦截的功能了
//我的截图里面没有下面这一行的代码,希望你们不要漏写
http.authorizeRequests().anyRequest().authenticated();
}

}