SpringSecurity简介
一、基本概念
1.1 认证(Authentication)方式
系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源
什么是认证(登录)?
用户认证就是判断一个用户的身份是否合法的过程
常见的用户身份认证方式
- 用户名密码登录
- 二维码登录
- 手机短信登录
- 指纹认证
- 人脸识别
- …
1.2 会话(Session)介绍
下面这个文章中有对session的理解:基于Session实现短信登录_c# 登录session怎么使用_我爱布朗熊的博客-CSDN博客
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。
会话就是系统为了保持当前用户的登录状态所提供的机制
常见的有基于session方式、基于token方式等
-
基于session的认证方式
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
有一些用户在使用浏览器的时候会禁用Cookie,这会导致发一次请求,用户登录一次
-
基于token的认证方式
它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。可以使用Redis 存储用户信息(分布式中共享session)。
基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
1.3 授权(Authorization)介绍
为什么要授权(控制资源被访问)?
因为不同的用户可以访问的资源不一样
什么是授权(给用户颁发权限)?
授权是用户认证通过后,根据用户的权限来控制用户访问资源的过程
拥有资源的访问权限则正常访问,没有权限则拒绝访问
1.4 RBAC
RBAC(Role-Based Access Control) 基于角色的访问控制
用户,角色,权限 本质:就是把权限打包给角色(角色拥有一组权限),又将角色分配给用户(用户拥有多个角色)。
最少包括五张表 (用户表、角色表、用户角色表、权限表、角色权限表)
二、SpringSecurity入门
声明式(注解)的安全访问控制解决方案的安全框架
提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作
2.1 快速入门
2.1.1 Maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.2</version>
</dependency>
2.1.2 接口
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("/hello")
private String hello(){
return "hello";
}
}
访问上面的接口就会出现下面的页面(登录)
默认的用户名是User,密码是在控制台上的UUID
登录上后便可以访问接口。
之后不会使用这种方式
我们也可以退出,访问IP:端口号/logout
2.1.3 源码
为什么默认的用户名是user?并且密码是一串UUID?
如下代码所示
2.2 配置文件配置用户名和密码
在配置文件中配置如下信息
spring:
security:
user:
name: admin
password: 123456
此时我们再使用默认的用户就不行了,而且控制台也不会输出UUID形式的密码了
Spring Security配置文件中默认配置用户是单一的用户,大部分系统都有多个用户,多个用户如何配置?
可以使用基于内存的多用户管理
2.3 基于内存的多用户管理
2.2 中配置的用户名和密码只能配置一个,但是我们的系统有许多的用户名和密码。
/**
* 自定义类实现用户详情服务接口
*
* 系统中默认是有这个UserDetailsService的,也就是默认的用户名(user)和默认密码(控制台生成的)
* 如果在yaml文件中配置了用户名和密码,那在系统中的就是yaml文件中的信息
*
* 我们自定义了之后,就会把系统中的UserDetailsService覆盖掉
*/
@Configuration
public class MySecurityUserConfig {
/**
* 根据用户名把用户的详情从数据库中获取出来,封装成用户细节信息UserDetailsService(包括用户名、密码、用户所拥有的权限)
*
* UserDetails存储的是用户的用户名、密码、去权限信息
*/
@Bean
public UserDetailsService userDetailsService() {
// 用户细节信息,创建两个用户
// 此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
UserDetails user1 =User.builder().username("zhangjingqi1").password("123456").roles("student").build();
UserDetails user2 = User.builder().username("zhangjingqi2").password("123456789").roles("teacher").build();
// InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsService
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
return userDetailsManager;
}
}
上面的程序启动会报错,原因:
SpringSecurity强制要使用密码加密,当然我们也可以不加密,但是官方要求是不管你是否加密,都必须配置一个密码编码(加密)器
/**
* 自定义用户必须配置密码加密器。
* NoOpPasswordEncoder.getInstance() 此实例表示不加密
*/
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
此时用户名和密码就是user1与user2中配置的用户名和密码(此时配置文件中的用户名和密码会失效)
UserDetails类如下所示:
三、加密
3.1 密码加密学习
- 密码为什么要加密?
CSDN网站六百万用户信息外泄-月光博客 (williamlong.info)
在加解密时一定要注意,我们要使用明文能加密成密文但是密文解密不成明文的加密算法
如果密文能解密,那我们加密的效果其实不算太好,黑客照样还是可以破译
-
加密的方式有哪些?涉及到密码加密问题
密码加密一般使用散列函数,又称散列算法,哈希函数,这些函数都是单向函数(从明文到密码,反之不行)
常见的散列算法有MD5和SHA
SpringSecurity提供了多种密码加密方案,基本上都实现了PasswordEncode接口,官方推荐使用BCryptPasswordEncode
-
NoOpPasswordEncoder类已经过期了,而且还没有加密,怎么解决?
-
以学生身份登录,发现不但可以访问学生的页面,还可以访问教师的页面和管理员的页面,如何解决?
权限问题,后面解决
-
如果要动态的创建用户,或者修改密码等(不是把用户名和密码写死到代码中),怎么办?
热疹信息存储到数据库中
3.2 BCryptPasswordEncoder
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
String encode3 = passwordEncoder.encode("123456");
System.out.println(encode1);//$2a$10$hkaTFSnsEBYcZxXMrDpMQu.IiPM5ZAIQ63Vvkq01.oxxv0yVRmKly
System.out.println(encode2);//$2a$10$nuPqwjhW0e/RZ.h3L1gZx.KanwUNQd4GEB2YoeB/LeVOhavcoBS7O
System.out.println(encode3);//$2a$10$SPHV9tuI6JhgOwDO2hMR4eq43E5BGmYJDQJ5GltIuK7WSQqi1sHzm
// 参数1:原文 参数2:密文
boolean result1 = passwordEncoder.matches("123456", encode1);
boolean result2 = passwordEncoder.matches("123456", encode2);
System.out.println(result1);//true
System.out.println(result2);//true
补充断言的知识:
如果result1的结果是true的话,运行时的标记就是”√“,反之则是“×”
assertTrue(result1);//期望result1为true
assertFalse(result2);//期望result2为true
以下就是错误的情况
我们可以将PasswordEncoder放入到Bean工厂中
/**
* 配置密码加密器
* NoOpPasswordEncoder.getInstance() 此实例表示不加密
* BCryptPasswordEncoder() 会加密
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
修改我们之前的程序
@Bean
public UserDetailsService userDetailsService() {
// 用户细节信息,创建两个用户
// 此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
UserDetails user1 = User.builder().username("zhangjingqi-1").password(passwordEncoder().encode("123456")).roles("student").build();
UserDetails user2 = User.builder().username("zhangjingqi-2").password(passwordEncoder().encode("123456")).roles("teacher").build();
// InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsService
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
return userDetailsManager;
}
四、权限信息
4.1 获取登录用户信息
关于已经验证实体的详情被存储到安全性上下文中(SpringContext)
如果用户名和密码正确,说明认证成功
此时会把认证信息放到Authentication中,再把Authentication放到SecurityContext中
然后可以通过安全上下文持有器SecurityContextHolder获取安全上下文
/**
* 获取用户登录信息的方式
*/
@RestController
@Slf4j
public class CurrentLoginUserController {
/**
* import org.springframework.security.core.Authentication;
* 一旦登录成功,访问下面的请求就可以得到authentication
*
* Authentication 继承 Principal
*
*/
@GetMapping("/getLoginUser1")
public Authentication getLoginUser1(Authentication authentication) {
return authentication;
}
/**
*import java.security.Principal;
* 一旦登录成功,访问下面的请求就可以得到principal
*/
@GetMapping("/getLoginUser2")
public Principal getLoginUser2(Principal principal) {
return principal;
}
/**
* 一旦我们登陆成功,框架就会把我们的信息放到安全性上文中SpringContext
* 所以我们可以通过安全性上文SpringContext获取用户信息
*/
@GetMapping("/getLoginUser3")
public Principal getLoginUser3() {
// 通过安全上下文持有器获取安全上下文
return SecurityContextHolder.getContext().getAuthentication();
}
}
访问:localhost:8080/getLoginUser1
“name”用户名
“credentials”凭据,其实就是密码,这里是null的原因就是直接给展示密码太不安全了,所以显示为null
“authorities”权限信息,授权,权限
“principal”中的内容就是代码UserDetails中定义的内容
"accountNonExpired"账户是否未过期,true表示未过期
"accountNonLocked"账户是否未锁定,true表示未锁定
"credentialsNonExpired"凭证是否未过期,true表示未过期,这里的凭证一般是指密码
**“enabled”**账户是否可用,true表示可用只有当"accountNonExpired"、“accountNonLocked”、“credentialsNonExpired”、"enabled"都为true时,账户才能使用
4.2 配置用户权限
如下图所示,便是给用户配置权限
其中roles可以配置多个角色,其有一个可变数组
,这样来说一个用户可以配置多个角色
UserDetails user1 = User.builder().username("zhangjingqi-1").password(passwordEncoder().encode("123456")).roles("student","manager").build();
下面来配置一下用户的权限
@Bean
public UserDetailsService userDetailsService() {
// 用户细节信息,创建两个用户
// 此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
UserDetails user1 = User.builder()
.username("zhangjingqi-1")
.password(passwordEncoder().encode("123456"))
// 配置用户角色
.roles("student", "manager") //角色到系统中会变成权限的,比如这里会变成ROLE_student,ROLE_manager
// 配置用户权限. student:delete权限处理学生的删除,student:add权限处理学生的添加
.authorities("student:delete","student:add")
.build();
UserDetails user2 = User.builder()
.username("zhangjingqi-2")
.password(passwordEncoder().encode("123456"))
// 配置权限
.authorities("teacher:delete", "teacher:add")
// 配置角色
.roles("teacher")
.build();
// InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsService
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
return userDetailsManager;
}
登录zhangjingqi-1账号,访问localhost:8080/getLoginUser3
我们发现,JSON串中“authorities”的内容是中配置的”authorities“,而没有将“roles”变成权限
之后再登录zhangjingqi-2账号访问localhost:8080/getLoginUser3,我们发现下面的JSON串中”authorities“是在代码中配置的“roles”角色信息转换成的权限
所以配置用户权限我们得出一个结论:
-
在代码中配置“authorities“与“roles”时,谁再后面谁执行,后者会覆盖前者;
-
配置的”roles“角色信息,在前面添加一个“ROLE_”前缀就会变成权限信息
-
系统不会给配置的”authorities“权限信息添加任何前缀
4.3 针对URL授权
此控制级别只在Controller层
虽然我们实现了认证功能,但是受保护的资源是默认的,默认所有认证(登录)用户均可以访问所有资源,不能根据实际情况进行角色管理
要实现授权功能,需重新WebSecurityConfigurerAdapter中的一个configure方法
虽然之前我们配置了用户角色和用户权限,但是用户登录上之后依然可以随便访问,比如说“student”角色可以访问“teacher”角色对应的功能。
4.3.1 拒绝任何请求
如下代码所示:表示拒绝任何http请求
虽然可以使用zhangjingqi-1或zhangjingqi-2认证成功登录,但是Controller不让访问,并且包括springSecurity框架中的login也会被屏蔽掉
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 重写 configure(HttpSecurity http)方法
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//授权http请求
.anyRequest()//任何请求
.denyAll();//拒绝
}
访问http://localhost:8080/login如下所示
4.3.2 允许登录表单
为了可以访问http://localhost:8080/login,我们可以使用下面的一个配置
/**
* 重写 configure(HttpSecurity http)方法
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//授权http请求
.anyRequest()//任何请求
.denyAll();//拒绝
http.formLogin().permitAll();//允许表单登录
}
此时就可以看到下面的登录界面了,但是如果访问其他请求,就会被拒绝
4.3.3 允许任何请求
/**
* 重写 configure(HttpSecurity http)方法
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//授权http请求
.anyRequest()//任何请求
.permitAll();//允许任何请求
http.formLogin().permitAll();//允许表单登录
}
4.3.4 匹配请求
zhangjingqi-1对应的权限:student:delete、student:add
zhangjingqi-2对应的权限:ROLE_teacher
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 重写 configure(HttpSecurity http)方法
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//授权http请求
.mvcMatchers("/student/**")// 匹配/student开头的请求
.hasAnyAuthority("student:add")// 拥有student:add权限的用户可以访问上面的url
.mvcMatchers("/teacher/**") // 匹配/teacher/**开头的请求
.hasAnyAuthority("ROLE_teacher")//拥有ROLE_teacher权限的用户可以访问/teacher/**开头的url
.anyRequest()// 任何请求
.authenticated()//需要验证。注意:没有配置的url,只要登录成功就可以访问
;
http.formLogin().permitAll();//允许表单登录
}
}
使用zhangjingqi-1访问一下localhost:8080/student/hello
使用zhangjingqi-1访问一下localhost:8080/hello/hello
使用zhangjingqi-1访问一下localhost:8080/teacher/hello
使用zhangjingqi-2访问一下http://localhost:8080/student/hello
使用zhangjingqi-2访问一下localhost:8080/hello/hello
使用zhangjingqi-2访问一下localhost:8080/teacher/hello
4.3.5 总结
- 匹配请求的三种方式
.mvcMatchers("/student/**")
.regexMatchers("/student/**")
.anyMatchers("/student/**")
- 判断权限的五种方式
推荐第一种
.hasAuthority("ROLE_teacher") //是否有单个权限,参数只能写一个权限
.hasAnyAuthority("ROLE_teacher","student:add")//是否有其中的任意一个权限
.access( "hasAuthority('student:query') or hasAuthority('student:add')")
.hasRole("student") // 是否有单个角色
.hasAnyRole("student") //是否有其中的某个角色
- 先写匹配路径,再写对应的权限
4.4 针对方法进行授权
下面 通过更灵活的配置方法安全,我们先通过@EnableGlobalMethodSecurity开启基于注解的安全配置
4.4.1 抽象类
public interface TeacherService {
String add();
String update();
String delete();
String query();
}
4.4.2 实现类
@Slf4j
@Service
public class TeacherServiceImpl implements TeacherService {
//预授权注解,此处采用表达式的形式。
//如果有teacher:add权限,才会执行下面这个方法;反之不会访问
@PreAuthorize("hasAuthority('teacher:add')")
@Override
public String add() {
log.info("添加教师成功");
return "添加教师成功";
}
//只要有teacher:update或teacher:add权限其中之一,便可以执行下面的方法
@PreAuthorize("hasAnyAuthority('teacher:update','teacher:add')")
@Override
public String update() {
log.info("修改教师成功");
return "修改教师成功";
}
@PreAuthorize("hasAuthority('teacher:delete')")
@Override
public String delete() {
log.info("删除教师成功");
return "删除教师成功";
}
@PreAuthorize("hasAuthority('teacher:query')")
@Override
public String query() {
log.info("查询教师成功");
return "查询教师成功";
}
}
4.4.3 teacher的Controller
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@Resource
private TeacherService teacherService;
@GetMapping("/query")
public String queryInfo() {
return teacherService.query();
}
@GetMapping("/add")
public String addInfo() {
return teacherService.add();
}
@GetMapping("/update")
public String updateInfo() {
return teacherService.update();
}
@GetMapping("/delete")
public String deleteInfo() {
return teacherService.delete();
}
}
4.4.4 用户详情服务接口配置类
@Configuration
public class MySecurityUserConfig {
/**
* 根据用户名把用户的详情从数据库中获取出来,封装成用户细节信息UserDetails(包括用户名、密码、用户所拥有的权限)
* <p>
* UserDetails存储的是用户的用户名、密码、去权限信息
*/
@Bean
public UserDetailsService userDetailsService() {
// 用户细节信息,创建两个用户
// 此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
UserDetails user1 = User.builder()
.username("zhangjingqi-1")
.password(passwordEncoder().encode("123456"))
// 配置用户角色
.roles("student") //角色到系统中会变成权限的,比如这里会变成ROLE_student,ROLE_manager
.build();
UserDetails user2 = User.builder()
.username("zhangjingqi-2")
.password(passwordEncoder().encode("123456"))
// 配置权限
.authorities("teacher:query")
.build();
UserDetails user3 = User.builder()
.username("admin")
.password(passwordEncoder().encode("123456"))
// 配置权限
.authorities("teacher:query","teacher:add","teacher:update","teacher:delete")
.build();
// InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsService
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
userDetailsManager.createUser(user3);
return userDetailsManager;
}
/**
* 配置密码加密器
* NoOpPasswordEncoder.getInstance() 此实例表示不加密
* BCryptPasswordEncoder() 会加密
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.4.5 安全配置
WebSecurityConfig这个地方其实是URL的权限配置
预授权:在访问方法之前判断有无权限(使用量最大)
后授权:访问完方法后再判断有无权限
//@Configuration
//开启全局方法安全。 prePostEnabled = true 表示预授权和后授权开启
//此注解中包含@Configuration注解,所以不用重复标识
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//授权http请求
.anyRequest() //任何请求
.authenticated();//都需要认证
http.formLogin().permitAll();//允许表单登录
// 或者下面这种形式
// http.authorizeRequests()//授权http请求
// .anyRequest() //任何请求
// .authenticated()//都需要认证
// .and()
// .formLogin().permitAll();
}
}
4.4.6 测试
用户admin,访问localhost:8080/teacher/query
用户admin,访问localhost:8080/teacher/add
用户zhangjingqi-2,访问localhost:8080/teacher/query
用户zhangjingqi-2,访问localhost:8080/teacher/add
用户zhangjingqi-1,访问localhost:8080/teacher/query