SpringSecurity安全框架的核心功能是认证和授权:
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。
授权:经过认证后判断当前用户是否具有进行某个操作的权限。
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
二、入门案例:
2.1、创建Springboot项目:勾选SpringSecurity依赖
在SpringBoot项目中使用SpringSecurity我们只需要引入依赖即可实现入门案例:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入依赖后,启动Springboot应用,尝试去访问hello接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user, 密码会输出在控制台,必须登陆之后才能对接口进行访问:
@RestController
public class SecurityController {
@RequestMapping("/hello")
public String helloController(){
return "欢迎使用SpringSecurity";
}
}
2.2、启动应用,访问接口:
当我们访问:http://localhost:8080/hello 时,会自动跳转到默认的登陆页面:
2.3、入门案例中 SpringSecurity 的流程:
SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器 ,这里我们可以看看入门案例中的过滤器:图中只展示了核心过滤器,其它的非核心过滤器没有展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名、密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor: 负责权限校验的过滤器
我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序:
入门案例中的SpringSecurity过滤器链里有15个过滤器:
{DefaultLoginPageGeneratingFilter@6504} 这个Filter功能是用来显示默认的登录页,如果说你不想要默认登录页,到时候不要加他就可以了,
{DefaultLogoutPageGerneratingFilter@6505}这个Filter功能是用来显示默认的注销页面
2.4、入门认证流程详解:
概念速查:
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。它用来表示的是当前访问系统的用户,可以在里面封装一些用户的相关信息,比如说用户名、密码,然后包括用户的一些权限都可以。但是在这个情况下,你看他只提交了用户名、密码,那他封装的对象里面也只有用户名和密码信息
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。会把这些用户信息、权限信息又封装成一个UserDetails对象,这也是一个接口,它的核心提供了用户信息,我们可以把查到的用户的一些信息封装到这个接口的一个对象里面去。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
2.5、入门案例的缺点分析:
1、在我们前后端分离的项目中,我们不需要springsecurity提供的默认登录页,给它删除,我们需要自定义登录接口;
2、入门案例里面用户名和密码,用户名默认为user,密码是它随机生成的,这肯定是不行的。我们应该是让他去数据库里面去进行用户名、密码的校验;
3、入门案例它是基于session去实现这种登录。前后端不分离的时候可以用session,前后端分离,就不能使用session了,当用户的用户名、密码校验成功后,我们应该①:以用户id生成jwt,作为token响应给前端;② 同时以用户id作为key,用户信息作为value,存入Redis中;(实际项目中用不用 "用户id" 作为jwt、作为key看具体的项目需求)
4、当我们校验用户有没有权限去访问当前他所请求的这个资源时,我们应该去获取请求头中的token进行解析,去判断有没有权限访问当前资源
三、自定义登录的认证:
实际前后端分离的项目中,去进行认证,核心其实依赖的就是token,你可以理解成它是一个加密之后的一个字符串,我通过前端是否携带这个token,去判断你是不是我系统的用户、你究竟是哪一个用户,这是它的一个核心思路
3.1、自定义的登录认证需要实现两大功能:登录、校验
登录
①自定义登录接口:
把用户输入的用户名和密码封装成Authentication对象
调用ProviderManager的authenticate()方法进行认证
如果认证没通过给用户对应的提示信息
如果认证通过则生成jwt作为token返回给前端,并且把用户信息存入redis中
②自定义UserDetailsService接口的实现类
在这个实现类中去查询数据库
校验:
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
3.2、"自定义登录接口"的具体思路、流程:
①、默认情况下定义的接口,都会被我们SpringSecurity保护,要求必须认证后,才能够访问,而这个登录接口,必须设置为让SpringSecurity放行,让用户访问登录接口的时候不需要登录也能够访问,否则会相互矛盾
②、我们自定义一个登录接口,其实就是定义一个Controller去接受用户输入的用户名和密码,然后在Controller中去调用Service,在Service当中进行具体的业务操作,这个登录接口的请求方式为Post,所以要使用@PostMapping
③、具体的业务操作包括:
把用户名和密码封装成Authentication对象
调用AuthenticationManager的authenticate()方法进行认证
密码加密存储PassWordEncoder
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为: {id}password。它会根据id去判断密码的加密方式,如果没加密,就是{noop}password。但是我们一般不会采用这种方式。所以需要替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder,我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用BCryptPasswordEncoder来进行密码校验。
BCryptPasswordEncoder简介:
1 、BCryptPasswordEncoder是采用SHA-256 +随机盐+密钥对明文密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。BCrypt 是一种强哈希算法,它能很好地防止被暴力破解。
2、加密(encode):注册用户时,采用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。因为每次的随机盐不同,导致对同一明文密码加密后得到的加密结果都不一样。密文中本身包含了很多信息,包括 salt 和 使用 salt 加密后的 hash。
3、密码匹配(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。
4、这正是为什么处理密码时要用hash算法,而不用加密算法。因为这样处理即使数据库泄漏,黑客也很难破解密码!
BCryptPasswordEncoder举例:
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter:
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
@SpringBootTest
class Boot3SpringsecurityApplicationTests {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Test
public void testBCryptPasswordEncoder(){
// BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//传入密码的明文,它就会帮你进行加密,然后返回加密之后的字符串
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
System.out.println("encode1: "+encode1);
System.out.println("encode2: "+encode2);
//第一个参数传入的是登录时候用户输入的密码,明文,然后和你数据库当中查到的加密后的密文比较:
boolean flag1 = passwordEncoder.matches("123456",encode1);
boolean flag2 = passwordEncoder.matches("123456",encode2);
System.out.println("flag1为"+flag1);
System.out.println("flag2为"+flag2);
}
}
LoginUser implements UserDetails:
到时候框架里面会调用loginUser的getPassword()方法来获取当前用户的密码之类的
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
自定义登录接口: