目录
3.4.2 修改配置类
4.3.2 hasAnyAuthority(String ...)
4.6.2 @PreAuthorize/@PostAuthorize
一、SpringSecurity 简介
1.1 概念
Spring Security is a powerful and highly customizable authentication and access-control framework.
Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC,DI 和 AOP 功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。 Spring Security 的主要核心功能为 认证和授权,整个架构也是基于这两个核心功能去实现的。
1.2 原理
众所周知 想要对 Web 资源进行保护,最好的办法莫过于 Filter 和 Interceptor。
所以 SpringSecurity 在我们进行用户认证以及授予权限的时候,通过各种各样的拦截器来控制权限的访问,从而实现安全。 如下为其主要过滤器 :
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- LogoutFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- AnonymousAuthenticationFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
- UsernamePasswordAuthenticationFilter
- BasicAuthenticationFilter
1.3 核心组件简介
Spring Security bean 的核心类
- SecurityContextHolder:提供对 SecurityContext 的访问
- SecurityContext,:持有 Authentication 对象和其他可能需要的信息
- AuthenticationManager: 其中可以包含多个 AuthenticationProvider
- ProviderManager:对象为 AuthenticationManager 接口的实现类
- AuthenticationProvider:主要用来进行认证操作的类 调用其中的authenticate()方法去进行认证操作
- Authentication:Spring Security 方式的认证主体
- GrantedAuthority:对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
- UserDetails:构建 Authentication 对象必须的信息,可以自定义,可能需要访问 DB 得到
- UserDetailsService:通过username构建UserDetails对象,通过loadUserByUsername 根据 userName 获取UserDetail对象(可以在这里基于自身业务进行自定义的实现 如通过数据库,xml,缓存获取等)
1.4 验证和授权的过程及本质
用户一次完整的登录验证和授权,是一个请求经过 层层拦截器从而实现权限控制,整个 web 端配置为 DelegatingFilterProxy,它并不实现真正的过滤,而是所有过滤器链的代理类,真正执行拦截处理的是由 spring 容器管理的各个filter bean 组成的 filterChain。
二、简单应用
2.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.9</version>
</dependency>
2.2 访问页面
导入 spring-boot-starter-security 启动器后,Spring Security 已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。
在resources下建立static目录,然后再static目录下创建index.html
在浏览器输入:http://localhost:8080后会显示下面页面
默认的 username 为 user,password 打印在控制台中。以上为默认值。
在浏览器中输入账号和密码后登录成功后会显示 index.html 页面内容。
三、认证相关 API
3.1 UserDetailsService 详解
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
该接口抽象方法的返回值 UserDetails 是一个接口,定义如下:
要想返回 UserDetails 的实例就只能返回接口的实现类。SpringSecurity 中提供了如下的实例。对于我们只需要使用里面的 User 类即可。
注意 User 的全限定路径是:
org.springframework.security.core.userdetails.User 此处经常和系统中自己开发的 User 类弄混。
在 User 类中提供了很多方法和属性:
构造方法参数说明:
- username:用户名
- password:密码
- authorities:用户具有的权限。此处不允许为 null
此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security 会根据 User 中的 password 和客户端传递过来的 password 进行比较。如果相同则表示认证通过,如果不相同表示认证失败。 authorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403 。通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建 authorities 集合对象的。参数是一个字符串,多个权限使用逗号分隔。
注意:
username 是客户端表单传递过来的数据。默认情况下参数名必须 username,否则无法接收。
3.2 PasswordEncoder 密码编码器接口详解
Spring Security 要求容器中必须有 PasswordEncoder 实例。所以当自定义登录逻辑时要求必须给容器注入 PaswordEncoder 的 bean 对象。
接口介绍:
- encode():把参数按照特定的解析规则进行解析。
- matches()验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
- upgradeEncoding():如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false。
PaswordEncoder 实现类有很多:
比较常用的BCryptPasswordEncoder
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码编码器,平时多使用这个编码器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过 strength 控制加密强度,默认10。
示例代码:
package com.tjetc;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest
public class MyTest {
@Test
public void test() {
//1、创建BCryptPasswordEncoder对象
PasswordEncoder encoder = new BCryptPasswordEncoder();
//2、把原始密码加密
String encodePassword = encoder.encode("123456");
System.out.println("123456密码加密后:" + encodePassword);
//3、原始密码和加密后的密码进行比对
boolean matches = encoder.matches("123456", encodePassword);
System.out.println("是否匹配:" + matches);
}
}
3.3 自定义认证逻辑
3.3.1 编写配置类
3.3.2 编写认证服务实现类
该类需要实现 UserDetailsService 接口
package com.tjetc.service.Impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 提供一个实现UserDetails接口的对象,包含:正确的用户名、密码、权限列表
* loadUserByUsername 不比较用户传来的密码的正确性,比较交给spring security框架
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1、从数据库根据用户名查询username是否存在,
// 没有用户抛出异常UsernameNotFoundException,
// 有的话继续执行代码
// 模拟查询,用户名不是admin,抛异常
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户名不存在");
}
//2、从数据库根据用户名查询正确的密码
// 模拟正确密码 $2a$10$vdGrOfmKlfcVga4JCIwzJOpJYEpDC/kF1LQx3OcBNx2zph3YVfziK
// 这个数据是passwordEncoder生成的
String password = passwordEncoder.encode("123456");
//3、从数据库查询用户的权限信息
// 模拟:权限信息字符串,生成权限对象
// AuthorityUtils.commaSeparatedStringToAuthorityList 用途是 把用逗号分割的权限字符串转换成权限对象的集合
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
//4、返回UserDetails实现类对象(例如:User是security定义)
User user = new User(username, password, authorities);
return user;
}
}
3.3.3 测试
重启项目,在浏览器中输入账号:admin,密码:123456。后可以正确进入到 index.html 页面。