文章部分图片来源网络,侵删,感谢
看完本文你可以了解到的知识点:
- SpringSecurity是什么,如何配置以及认证流程
- 如何基于Token(JWT)进行认证
- 如何对用户进行鉴权,基于RBAC模型
介绍
SpringSecurity 是基于Spring开发的WEB服务认证授权框架,它的核心功能包括:
用户认证(你是谁)资源授权(你能做什么)防止CSRF跨站请求,session攻击等
安装
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.0</version>
</dependency>
复制代码
其实当你引入依赖以后,什么配置都不需要做,你的项目就已经被Spring Security管理了,默认使用的是Http basic模式,当你启动项目你可以在控制台看到这行message,这是Spring Security生成的随机密码,每次启动都会重新生成。
此时,如果你再去访问api接口,那么会自动跳转login请求并弹出这个页面:
这是SpringSecurity帮我们做的,默认的username为user,Password也就是控制台的随机密码,输入即可访问到我们的api接口。当然我们也可以自定义username和password,只需要在application.yml中这样配置:
spring:
security:
user:
name: admin
password: 123456
复制代码
原理
刚刚我们已经了解了Spring SecurityHttp Basic模式的,这是最基本最简单的一种模式,它的原理是将用户名和密码通过Base64进行加密,然后在请求头中通过Authorization传递,服务器在BasicAuthenticationFilter过滤器中对加密字符串进行解析校验然后返回结果,这种方式非常的不安全,而且用户只能在配置文件中定义,并且不可以定制登录页面,所以基本上使用的场景不多。(用的不多还说半天?这不是先熟悉一下SpringSecurity嘛!),除了Http Basic模式以外还提供了formLogin模式,但是由于目前基本都是前后端分离,所以基于Session的认证我在这里就不写了,本文主要还是基于Token方式认证,也就是不使用这两种模式。
在学习如何使用之前,我们需要先了解一下SpringSecurity的流程,了解了流程方便后面入手。
核心组件
SpringSecurity有几个核心的组件需要我们先了解:
Authentication:认证主体,存储详细的用户信息SecurityContext:存储认证主体SecurityContextHolder:构建SecurityContext,它是线程安全的,使用ThreadLocal管理。
也就是说用户经过认证以后,我们可以在程序中通过这行代码来获取用户的信息。
SecurityContextHolder.getContext().getAuthentication()
复制代码
上面提到了BasicAuthenticationFilter,那么它是什么。
SpringSecurity认证的核心是过滤器链,整个认证授权过程其实就是在过滤器中完成的
- 当用户请求Api时,首先会经过一系列的认证过滤器(图中第二部分)进行认证,如果某个过滤器认证通过,那么会设置Authentication对象。
以上的认证过滤器并不是全部,并且我们可以自定义过滤器
- 最后一个过滤器是FilterSecurityInterceptor,它会去判断用户有没有认证,如果已认证,则可以访问Api
- 如果经过所有的过滤器用户都还没有认证,那么FilterSecurityInterceptor会抛出异常,然后由ExceptionTranslationFilter捕获对异常进行处理,处理方式比如跳转到登录页面或者响应JSON
配置类
SpringSecurity怎么知道哪些api需要认证才能访问,哪些用户可以认证通过,并且有权限的呢?自然是需要我们告诉SpringSecurity,所以需要创建一个SpringSecurity的配置类,继承WebSecurityConfigurerAdapter,重写三个方法,用于配置用户、资源权限规则等等。先了解即可。
@EnableWebSecurity(debug = true) //开启 debug开启可以查看详细的日志
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public WebSecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
//配置资源权限规则
@Override
protected void configure(HttpSecurity http) throws Exception {
}
//配置用户
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}
//忽略静态资源,不会参与认证
@Override
public void configure(WebSecurity web) throws Exception {
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
复制代码
密码编码器
先说点题外话,关于密码的发展史,一开始是使用明文进行存储密码(想起了2011年CSDN几百万用户密码泄漏事件),后来开始使用单向的hash算法(MD5、SHA256),单向hash代表hash后的值不能被反向解析,但是这种方式有很多缺点,比如同样的值经过hash后得到的值是相同的,会有hash碰撞,计算速度很快。基于这三个缺点,黑客们可以对20位以内的数字+字母的随机组合生成一个字典表,解密的时候搜索一下就可以得到原始值了。再之后呢就是对密码“加盐”,对于用户的每个密码添加全球唯一的值,这样就没有现成的字典表使用使用。
但是目前来说,最好的办法是使用慢一点的算法比如Bcrypt,而不是MD5这样快速的算法。制作8位密码长度的MD5字典表需要5个月,而BCrypt需要几十年。而且BCrypt的盐值是经过多次计算的。
SpringSecurity提供了密码编码器PasswordEncoder,并且提供了BCryptPasswordEncoder实现类。用户注册时使用BCrypt进行加密,当登录时会使用用户输入的明文密码进行匹配。我们需要将它注入到Spring容器中作为全局Bean。
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
复制代码
但是有一种场景,就是可能开发的项目很久了,一开始是使用的MD5算法,那现在如果想使用BCrypt该怎么解决呢。也就是如果对于老用户MD5同样可以匹配成功,新用户BCrypt可以匹配成功,如果老用户修改密码再使用BCrypt算法进行加密,相当于一个替换的过程,SpringSecurity5提供了DelegatingPasswordEncoder,它可以定义密码编码器的集合。
@Bean
public PasswordEncoder passwordEncoder() {
String idForDefault = "bcrypt"; //默认编码
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(idForDefault, new BCryptPasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
return new DelegatingPasswordEncoder(idForDefault, encoders);
}
复制代码
但是这种情况的话我们数据库中的密码应该使用{算法}密码的形式存储,比如:
{bcrypt}$2a$10$G9URrvDzboJXt4fOawOohOD2QbtnniwVD9IEmxNrbHvPtoCnPIXSq
{MD5}4xAGajgDhbSnUgAZ+5jbJOKNwAbJ3fPMwNsaIohE2Ko=
复制代码
因为当我们配置了多密码编码器,SpringSecurity加密后的密码是这个形式的,不然无法匹配。
定制化数据库
SpringSecurity的用户来源可以是内存或者数据库(SpringSecurity支持硬编码配置用户信息),但是实际中基本上我们的用户是在数据库中的,所以我们需要一种方式可以将数据库的用户加载进来,然后SpringSecurity根据这些用户进行认证匹配。SpringSecurity提供了UserDetails和UserDetailsService,前者就是用户信息,后者则是用于加载UserDetails,也就是去查询数据库返回UserDetails。
UserDetails
该实体对应的就是数据库中的用户表,包含用户名、密码、邮箱等等信息(这些都是用户表中的字段),以及一个authorities权限集合,这个用户拥有哪些权限。而数据库中会有一个角色表和中间表。
@Entity
@Getter
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private boolean enabled; // 账户是否可用
@ManyToMany
@Fetch(value = FetchMode.JOIN)
@JoinTable(name = "user_role",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, //关联
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} //反向指Role
)
private Set<Role> authorities; //权限集合
//以下三个字段都是与账户是否可用有关,如果不需要设置为true即可,一般来说只用enable就好了。
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
复制代码
@Entity
@Getter
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "role_name")
private String authority;
}
复制代码
UserDetailsService
UserDetailsService是用于加载用户信息,它只有一个方法,返回上面定义的UserDetails即可。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepo userRepo;
@Override
public User loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepo.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("未找到用户名为" + username + "的用户"));
}
}
复制代码
@Repository
public interface UserRepo extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
复制代码
SecurityConfig
定义好了UserDetails和UserDetailsService之后,我们还需要在SecurityConfig中进行配置,告诉SpringSecurity使用该方式加载用户信息。
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
复制代码
认证流程
以用户名和密码登录举例:
- 根据用户名和密码构建一个
UsernamePasswordAuthenticationToken对象,它继承了Authentication - 然后将Authentication对象交给
过滤器链去认证 - 通过
AuthenticationManager的实现类Providers去认证 - 如果是去数据库加载数据源会通过
DaoAuthenticationProvider进行认证,会调用UserDetailsService加载用户的信息 - 如果认证失败,那么Authetication对象会设置为false,
FilterSecurityInterceptor会抛出异常。
最后
本文参考内容:




近期评论