文章目录
- 前言
- 一、SpringSecurity Web方案
- 🍓Test Controller 测试请求控制器
- 🤣SpringSecurity 基本原理
- 🌍代码底层流程:重点看三个过滤器
- FilterSecurityInterceptor 方法级的权限过滤器
- ExceptionTranslationFilter 异常过滤器
- UsernamePasswordAuthenticationFilter 对/login 的 POST 请求做拦截
- 🍇过滤器是如何进行加载的 DelegatingFilterProxy
- Q:过滤器是如何进行加载的 ?
- 🍔两个重要接口 UserDetailsService / PasswordEncoder
- UserDetailsService 接口
- PasswordEncoder 接口
- 🍕配置文件配置默认的用户名和密码
- 💖配置类配置默认用户名和密码
- 🚑总结适配器模式:WebSecurityConfigurerAdapter
- 🥩自定义类实现用户名和密码
- 🍧自定义其他配置,如有改动请移步官方文档
- 自定义登录页面
- 🥑基于角色和权限进行访问控制
- 1、在配置类设置路径请求需要权限
- 2、在 UserDetailsService 接口设置用户权限
- 😢自定义403 页面 以及 完整配置类
- 😘SpringSecurity 注解开发
- 启动类添加注解
- @Secured({"ROLE_sale","ROLE_manager"})
- @PreAuthorize 适合进入方法前的权限验证
- @PostAuthorize 方法执行后进行校验
- @PostFilter 对方法的返回数据进行过滤
- @PreFilter 进入控制器方法之前对数据进行过滤
- 🎶实现用户注销
- 🤦♂️基于数据库实现 “记住我”
- 1、创建数据表
- 2、设置配置类
- 3、添加一个复选框
- 4、测试
- 🥜CSRF 跨站请求伪造
- 1、开启 CSRF 防护
- 2、添加表单隐藏域
- 3、CsrfFilter 源码分析
- 二、 SpringSecurity 微服务权限方案 :请移步谷粒学院
- 🥚认证授权过程分析
- 🍏需要的数据模型: 最基本的五张表
- 🥩开发环境
- 三、SpringSecurity源码分析
- 🍭 认证流程 UsernamePasswordAuthenticationFilter
- 🍖权限认证流程
- 🥙认证信息共享
- 🍤总结认证流程
前言
一、SpringSecurity Web方案
创建一个springBooot工程,引入SpringSecurity场景依赖,然后编写一个测试controller进行请求测试
🍓Test Controller 测试请求控制器
package com.example.springsecurity.controller;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("hello")
public String hello() {
return "hello security";
}
@GetMapping("index")
public String index() {
return "hello index";
}
}
启动boot 访问controller 的 test / hello 路径
🤣SpringSecurity 基本原理
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
ter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
🌍代码底层流程:重点看三个过滤器
FilterSecurityInterceptor 方法级的权限过滤器
是一个方法级的权限过滤器, 基本位于过滤链的最底部, 由于是责任链模式,前面处理不了的请求会寻找下一个责任人去处理任务,直到找到能处理的责任为止。
ExceptionTranslationFilter 异常过滤器
是个异常过滤器,用来处理在认证授权过程中抛出的异常
UsernamePasswordAuthenticationFilter 对/login 的 POST 请求做拦截
对/login 的 POST 请求做拦截,校验表单中用户名,密码。就像我们上面提到的 SpringSecurity 为我们提供的登录表单一样
🍇过滤器是如何进行加载的 DelegatingFilterProxy
自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方
案,可以使用更少的配置来使用 Spring Security。
springBoot 帮我们自动装配了一个过滤器 DelegatingFilterProxy 授权过滤代理类,如果不使用Springboot整合,我们需要从 DelegatingFilterProxy 开始配置 SpringSecurity
Q:过滤器是如何进行加载的 ?
问 :过滤器是如何进行加载的 ?
答
: 如果没有SpringBoot 加持要从 DelegatingFilterProxy 开始进行配置,当然使用SpringBoot 整合 Spring Security 也是从 DelegatingFilterProxy 进行自动装载,由于 SpringSecurity 是一系列的过滤器链,所以 DelegatingFilterProxy 也是间接实现了 Filter 接口,然后会从 doFilter 开始,判断一系列的非空,然后拿到一个 WebApplicationContext 对象WebApplicationContext wac = this.findWebApplicationContext();
然后调用
this.initDelegate(wac);
初始化方法,来到初始化方法会使用 getBean方法获取到SpringBoot整合 SPringSecurity 的 SecurityFilterAutoConfiguration 自动装配对象,然后从这个自动装配的对象中获取到 beanNameprivate static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";
从getBean 方法出来后会获取到一个 FilterChainProxy 对象Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
这里是使用 父类型 进行接收,FilterChainProxy对象也间接实现了Filter 接口,FilterChainProxy中的 doFilter 方法会将所有的过滤器链封装到一个 List 集合中,至此过滤器链加载结束。
WebSecurity最终会创建FilterChainProxy对象,并作为springSecurityFilterChain返回,
SecurityFilterAutoCOnfiguration自动配置类引入了SecurityAutoConfiguration配置类,
SecurityAutoConfiguration配置类引入了WebSecurityEableConfiguration配置类(需要满足条件),
WebSecurityEnableConfiguration配置类中构造springSecurityFilterChain时,调用了WebSecurity的build方法。
后面的源码就需要deBug了,直接DeBug 走起
🍔两个重要接口 UserDetailsService / PasswordEncoder
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中
账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。
如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下
这个类是系统默认的用户“主体”
UserDetailsService 接口
// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();
以后我们只需要使用 User 这个实体类即可!
PasswordEncoder 接口
// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹
配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个
参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析
器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单
向加密。可以通过 strength 控制加密强度,默认 10.
🍕配置文件配置默认的用户名和密码
重启项目,控制台并没有输出密码
💖配置类配置默认用户名和密码
编写一个配置类继承 WebSecurityConfigurerAdapter ,Adapter(适配器)可见 WebSecurityConfigurerAdapter是一个适配器模式的类,一会我们跟进去看一下
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//对密码进行加密
String password = passwordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
}
// 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错
//因为上面 new 出来的对象不归 spring 管理
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
回到 WebSecurityConfigurerAdapter 抽象适配器类,会有三个重载的方法对我们传入的不同的对象进行适配
disableLocalConfigureAuthenticationBldr 是默认为false ,只有我们没有重写这个方法时,会对其赋值为 true,一旦我们自定义用户名和密码,进行方法重写disableLocalConfigureAuthenticationBldr 就一直为 flase
这里是空方法应该是给子类进行实现的
🚑总结适配器模式:WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter 是一个抽象适配器,实现了WebSecurityConfigurer 接口,里面的方法 init(初始化) 和 configure(配置),很显然是接口适配器模式,然后 WebSecurityConfigurerAdapter 又重载了三个方法 configure ,根据不同的参数进行对用户配置的适配,其中一个传入 AuthenticationManagerBuilder 建造者对象进行自定义的用户名和密码进行设置,这样就覆盖掉了抽象适配器的 configure 方法,使用我们自己定义的用户名和密码进行登录。至此完成了用户和密码的自定义配置,使用接口适配器对用户传入的参数进行适配,和建造者模式 让用户自定义用户和密码。
🥩自定义类实现用户名和密码
SpringSeucrity 在认证过程中,首先会去找配置文件和配置类,如果配置文件和配置类中有用户名和密码,就会使用这里面的用密码和密码进行配置,如果都没有找到,就会去找一个接口 UserDetailsService ,通过这个接口的实现类找到用户自定义的用户名和密码,然后进行认证过程
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义实现用户名和密码
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
// 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
从数据库查用户名和密码,只需要在 MyUserDetailsService 的 loadUserByUsername 添加相应的业务逻辑就行了
先把数据写死测试一下
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
//@Autowired
//private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
return new User("jack",
new BCryptPasswordEncoder().encode("123"),auths);
}
}
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//调用usersMapper方法,根据用户名查询数据库
QueryWrapper<Users> wrapper = new QueryWrapper();
// where username=?
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
//判断
if(users == null) {//数据库没有用户名,认证失败
throw new UsernameNotFoundException("用户名不存在!");
}
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
//从查询数据库返回users对象,得到用户名和密码,返回
return new User(users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
}
🍧自定义其他配置,如有改动请移步官方文档
自定义登录页面
package com.example.springsecurity.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import javax.sql.DataSource;
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//注入数据源
@Autowired
private DataSource dataSource;
//配置对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义实现用户名和密码
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
// 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/success.html").permitAll() //登录成功之后,跳转路径
.failureUrl("/unauth.html")//登陆失败跳转页面
.and().authorizeRequests() //对哪些请求进行验证
.antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
.anyRequest().authenticated()//除了上面的 "/","/test/hello","/user/login" 其他请求都需要进行验证 /** 才表示所有请求
.and().csrf().disable(); //关闭csrf防护
}
}
🥑基于角色和权限进行访问控制
1、在配置类设置路径请求需要权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/success.html").permitAll() //登录成功之后,跳转路径
.failureUrl("/unauth.html")//登陆失败跳转页面
.and().authorizeRequests() //对哪些请求进行验证
.antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
//当前登录用户,只有具有admins权限才可以访问这个路径
//1 hasAuthority方法 当前用户要用有 admins 权限才能访问
// .antMatchers("/test/index").hasAuthority("admins")
//2 hasAnyAuthority方法 当前用户要用有 任 一 权限才能访问
.antMatchers("/test/index").hasAnyAuthority("admins,manager")
//3 hasRole方法 ROLE_sale 使用这个方法时,给用户设定角色时需要加一个 ROLE_ 的前缀
.antMatchers("/test/index").hasRole("sale")
.anyRequest().authenticated()//除了上面的 "/","/test/hello","/user/login" 其他请求都需要进行验证 /** 才表示所有请求
.and().csrf().disable(); //关闭csrf防护
}
2、在 UserDetailsService 接口设置用户权限
上面两个地方互相打配合就可以达到用户授权的效果
😢自定义403 页面 以及 完整配置类
诸君自行搜索
完整配置类
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//注入数据源
@Autowired
private DataSource dataSource;
//配置对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义实现用户名和密码
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
// 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/success.html").permitAll() //登录成功之后,跳转路径
.and().authorizeRequests() //对哪些请求进行验证
.antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
//当前登录用户,只有具有admins权限才可以访问 /test/index 这个路径
//1 hasAuthority方法 当前用户要用有 admins 权限才能访问
.antMatchers("/test/index").hasAuthority("admins")
//2 hasAnyAuthority方法 当前用户要用有 任 一 权限才能访问
// .antMatchers("/test/index").hasAnyAuthority("admins,manager")
//3 hasRole方法 ROLE_sale 使用这个方法时,给用户设定角色时需要加一个 ROLE_ 的前缀
// .antMatchers("/test/index").hasRole("sale")
.anyRequest().authenticated()//除了上面的 "/","/test/hello","/user/login" 其他请求都需要进行验证 /** 才表示所有请求
.and().csrf().disable(); //关闭csrf防护
}
}
😘SpringSecurity 注解开发
启动类添加注解
@Secured({“ROLE_sale”,“ROLE_manager”})
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“
当前用户拥有 sale 和 manger 角色才能访问这个方法
@PreAuthorize 适合进入方法前的权限验证
启动类开启注解
@PostAuthorize 方法执行后进行校验
控制台输出,确定了是方法执行后再进行校验
@PostFilter 对方法的返回数据进行过滤
只返回了一个用户,说明确实对数据进行了过滤
@PreFilter 进入控制器方法之前对数据进行过滤
🎶实现用户注销
目前为止的 配置文件
package com.example.springsecurity.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import javax.sql.DataSource;
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//注入数据源
@Autowired
private DataSource dataSource;
//配置对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义实现用户名和密码
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
// 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//退出
http.logout().logoutUrl("/logout").
logoutSuccessUrl("/test/hello").permitAll();
//配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/success.html").permitAll() //登录成功之后,跳转路径
.and().authorizeRequests() //对哪些请求进行验证
.antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
//当前登录用户,只有具有admins权限才可以访问 /test/index 这个路径
//1 hasAuthority方法 当前用户要用有 admins 权限才能访问
.antMatchers("/test/index").hasAuthority("admins")
//2 hasAnyAuthority方法 当前用户要用有 任 一 权限才能访问
// .antMatchers("/test/index").hasAnyAuthority("admins,manager")
//3 hasRole方法 ROLE_sale 使用这个方法时,给用户设定角色时需要加一个 ROLE_ 的前缀
// .antMatchers("/test/index").hasRole("sale")
.anyRequest().authenticated()//除了上面的 "/","/test/hello","/user/login" 其他请求都需要进行验证 /** 才表示所有请求
.and().csrf().disable(); //关闭csrf防护
}
}
🤦♂️基于数据库实现 “记住我”
1、创建数据表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2、设置配置类
package com.example.springsecurity.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import javax.sql.DataSource;
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//注入数据源
@Autowired
private DataSource dataSource;
//配置对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true); //设置启动时创建表,这里让 SpringSecurity 可能会有些问题,我们自己创建
return jdbcTokenRepository;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//自定义实现用户名和密码
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
// 需要 将 BCryptPasswordEncoder 注入到容器中,不然报错,因为上面 new 出来的对象不归 spring 管理
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//退出
http.logout().logoutUrl("/logout"). //退出的超链接
logoutSuccessUrl("/test/hello").permitAll();//退出后跳转的路径
//配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/success.html").permitAll() //登录成功之后,跳转路径
.and().authorizeRequests() //对哪些请求进行验证
.antMatchers("/", "/test/hello", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
//当前登录用户,只有具有admins权限才可以访问 /test/index 这个路径
//1 hasAuthority方法 当前用户要用有 admins 权限才能访问
.antMatchers("/test/index").hasAuthority("admins")
//2 hasAnyAuthority方法 当前用户要用有 任 一 权限才能访问
// .antMatchers("/test/index").hasAnyAuthority("admins,manager")
//3 hasRole方法 ROLE_sale 使用这个方法时,给用户设定角色时需要加一个 ROLE_ 的前缀
// .antMatchers("/test/index").hasRole("sale")
.anyRequest().authenticated()//除了上面的 "/","/test/hello","/user/login" 其他请求都需要进行验证 /** 才表示所有请求
.and().rememberMe().tokenRepository(persistentTokenRepository()) //设置记住我
.tokenValiditySeconds(180)//设置有效时长,单位秒
.userDetailsService(userDetailsService); //要使用 userDetailsService 去操作数据库
// .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// .and().csrf().disable(); //关闭csrf防护
}
}
3、添加一个复选框
4、测试
🥜CSRF 跨站请求伪造
浏览器会获取到我们所有的 cookie 当我们进行认证之后,其他网站可以拿到我们的 cookie 信息,进行跨站请求伪造,进行一系列的恶意操作。
1、开启 CSRF 防护
2、添加表单隐藏域
添加一个隐藏域,固定格式,Spring Security 会往这个隐藏域的表单属性自动注入token,由于别的网站获取到了 cookie ,但是每次的 token是不同的 _csrf.token,
利用其他网站伪造的攻击是拿不到这个 token 的,然后会拿着 token 和 session 中保存的另一个同样的 token进行比较
如果一样就表示是本站认证过的请求,如果不一样则表示为其他网站伪造的请求,因为 session 是一次浏览器与服务器的会话,别人仿造请求肯定是两次会话了
两个session 就算伪造的请求带有 token 也不会和 SpringSecurity 生成的 token 一样,因此就完成了 CSRF 跨网站伪造请求攻击。
3、CsrfFilter 源码分析
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
//从请求中获取 session 对象
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
//根据token 是否为 null 赋值 missingToken
boolean missingToken = csrfToken == null;
//由于上面一直为 null 这里也一直为 true
if (missingToken) {
//获取到一个默认的 token
csrfToken = this.tokenRepository.generateToken(request);
// saveToken () 方法会 往session域设置 一个默认的 token,至此session 存入了一个 token,在这里往 session 设置token,每个 session 的token 不一样
this.tokenRepository.saveToken(csrfToken, request, response);
}
//然后将 token 放到 request 域中,将同一个 token 放入不同的地方,后面进行比对
//setAttribute 是一个 K (String),V (Object) 结构,也就等于把 token 绑定到了 request 域中的不同对象上去
request.setAttribute(CsrfToken.class.getName(), csrfToken);
//把 token 绑定到 (请求参数) 表单隐藏域,用户的每次请求都需要获取到这个参数对应的 token,而这个 token只有和服务器自己知道
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//判断 request 请求方式是否需要拦截 (需要拦截的请求在 set 集合中吗? 不在返回 flase 然后 非运算一下,就会将在set 集合中的请求类型进行放行)
if (!this.requireCsrfProtectionMatcher.matches(request)) {
// "GET", "HEAD", "TRACE", "OPTIONS" 不进行拦截
filterChain.doFilter(request, response);
} else {
//从请求头获取 token
String actualToken = request.getHeader(csrfToken.getHeaderName());
//如果没有获取到
if (actualToken == null) {
// 从请求参数(隐藏表单域)获取 token,这个 token是我们程序设置的黑客拿不到,只能拿到一个参数名,黑客如果不设置 token 就会抛异常
actualToken = request.getParameter(csrfToken.getParameterName());
}
//判断 session 的token 和 表单或者请求头的 token 是否一致,别的 session 拿到的表单 token 也是和自己的session 一致,而且token 随机,就不会被伪造
if (!csrfToken.getToken().equals(actualToken)) {
//不一致抛出异常
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
}
} else {
//一致进行放行
filterChain.doFilter(request, response);
}
}
}
总结:
CSRF攻击是在用户已经认证我们系统的基础上,访问了其他恶意连接,导致cookie泄露了出去,从而让黑客有可乘之机,我们只需要在每个用户的请求上加一个token,这个token只有服务器自己知道,这样黑客就算拿到了用户的cookie,没有服务器端的 token也是徒劳。
第一次获取 请求获取token,由于这时request还没有和服务器建立连接,里面判断session是否为空,request.getSession(false)表示当前的request如果没有session就为null,如果为true会创建一个新的session对象,这里肯定不能创建新的session对象的,由于第一次请求没有session对象,这时会返回一个null的token对象,后面再判断这个token是否为空,为空就获得一个默认的token,这时有了默认token,再去获取session对象,当程序走到这里会去创建一个session对象,把token存入session对象中,然后将token渲染到认证的表单,只有服务器的表单能够进行token的渲染,黑客的表单是不能通过服务器进行渲染的,然后我们在需要提交表单的地方都进行隐藏域渲染token,在html的Dom树进行加载时进行渲染。
这样认证的表单会有隐藏域(系服务器自己的表单),有这个隐藏域才会将token渲染上去,这一步是为了让用户进行认证,而黑客的csrf攻击是在用户认证过后,拿到cookie进行恶意操作,所以这里不用担心,接着会查看request的提交方式是否需要进行请求放行,如果不需要csrf防护就直接放行,然后就是获取请求头的token,如果获取不到请求头的token,就从参数获取,然后将token和存入session的token进行比对,如果一致进行放行,不一致抛出异常,由于用户的第一次认证会将渲染在表单的token带到服务器和session的token进行比对,这时用户肯定是可以认证的。
用户的第二次请求,先拿到request,这时用户已经和服务器有了session对象,就会获取到这个session对象的token,而不是去获取默认的token。这时还是会去做表单渲染(POST一般都是表单请求),但由于表单渲染都是在服务器端进行渲染,黑客伪造的网站是不能渲染的,然后用户的请求就可以拿着token,然后判断请求方式是否需要放行,需要进行拦截过滤的话,就会获取request请求头的token,如果获取不到,就去找请求参数的token,拿着request获取的token和session的token进行对比,如果不一致抛出异常,一致进行放行。
由于黑客是拿着用户认证后的cookie进行csrf攻击,虽然在用户认证是服务器将token渲染在了客户端,但渲染也是在服务端进行的,只是返回给浏览器一个html页面,这时当黑客伪拿到用户的cookie伪造了一个请求,就需要往request放入token,才能正确的访问服务端,由于这个token的创建和渲染都是在服务端进行的,黑客伪造的网页是不会渲染到token的,没有这个token,黑客伪造的请求自然而然的也就不能通过验证。
也就是在服务器端进行表单渲染,服务器能够被渲染的页面肯定是自己系统的网站,然后用户拿着渲染后的token,再次提交服务器,这时token一致,验证通过,而黑客伪造的网站并不能渲染到这个token,黑客最多拿到一个参数名,但是token值并不能获取,黑客发出的请求自然而然的不能通过验证,就算黑客的表单渲染到了token,但是 session 不同token不同,黑客的表单和服务器建立session,黑客的session中的token和他拿到用户的token还是不一样的。但如果黑客真的不经意间获取到了这个token,又该怎么办?
用户在与服务器建立链接每次都只能发送一个请求,这时就让改请求的token一次性,就算黑客获取到了token,但token是一次性的,黑客的请求拿着token访问服务器,这时获取session,渲染表单,黑客的表单不能渲染到token,这时黑客获取到的token就成了旧的token,还是不能访问服务器。
二、 SpringSecurity 微服务权限方案 :请移步谷粒学院
🥚认证授权过程分析
(1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找
到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。
(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限
信息中去
🍏需要的数据模型: 最基本的五张表
🥩开发环境
三、SpringSecurity源码分析
🍭 认证流程 UsernamePasswordAuthenticationFilter
大致的认证流程是从
UsernamePasswordAuthenticationFilter
开始, UsernamePasswordAuthenticationFilter 继承了AbstractAuthenticationProcessingFilter
当登录请求过来时会调用 父类 AbstractAuthenticationProcessingFilter 的 doFilter 方法,然后判断if (!this.requiresAuthentication(request, response))
请求是否需要验证,不需要验证直接放行,然后父类 doFilter 继续往下走 会调用abstract Authentication attemptAuthentication
抽象方法,这个方法的具体实现在子类进行,也就是会调用到 UsernamePasswordAuthenticationFilter 的attemptAuthentication( ) 方法,返回一个Authentication
对象用来封装认证结果 authResult,如果该对象不为空,会去配置session 会话策略this.sessionStrategy.onAuthentication
, 如果这个封装的对象为空,说明认证失败,直接 return 了,如果在子类执行 attemptAuthentication( ) 方法时抛出异常,会catch住异常并且调用this.unsuccessfulAuthentication
做一系列的断尾操作, 如果上面操作没有任何异常就会判断if (this.continueChainBeforeSuccessfulAuthentication)
是否为 true,为true就调用chain.doFilter(request, response);
放行,上面操作没有异常会将 continueChainBeforeSuccessfulAuthentication设置为 true的,所以不用担心,最后会调用this.successfulAuthentication( )
方法做一些认证成功的结尾逻辑。
接着继续看子类 UsernamePasswordAuthenticationFilter 的
attemptAuthentication( )
方法尝试验证身份,判断请求方式是否为 POST 不是的话直接抛出异常,然后获取到 userName和passWord进行判断是否为空串,不是空串对userName变量进行去除前后空格,但这些都不是重点,然后调用UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
获取到一个 authRequest 未认证的标记,因为这时还没有去查数据库什么的,然后调用this.setDetails(request, authRequest);
将request 请求的详细参数封装到 authRequest 未认证标记,最后调用return this.getAuthenticationManager().authenticate(authRequest);
AuthenticationManager身份验证管理器将这个为验证的 authRequest 放进去进行验证。
最后值得我们注意的是,authRequest 未认证标记UsernamePasswordAuthenticationToken 对象是一个 Authentication 接口的实现类,其中有一个方法
void setAuthenticated(boolean isAuthenticated)
, UsernamePasswordAuthenticationToken 在构造函数的参数不同对方法 setAuthenticated( ) 设置了认证标记 ,根据不同的参数进行判断当前的用户是否已经认证,不过根据我们的分析现在还没有进行认证,需要调用authenticate(authRequest);
进行验证,而 authenticate( ) 方法来自另一个接口AuthenticationManager
,该接口只有一个方法Authentication authenticate(Authentication authentication)
是验证一个 Authentication ,并将这个 Authentication 返回,至此子类attemptAuthentication( )
方法尝试验证身份结束,最后会将验证过的 Authentication 返回给父类AbstractAuthenticationProcessingFilter
。
如果大家感兴趣还可以去 AuthenticationManager的实现类ProviderManager 的 authenticate()方法是怎么对 UsernamePasswordAuthenticationToken 认证标记对象进行认证的,里面全是if else 和 try catch。
最后我们来到父类 AbstractAuthenticationProcessingFilter,如果子类获取身份验证结果没有任何异常就会调用
this.successfulAuthentication(request, response, chain, authResult);
处理认证成功的业务,SecurityContextHolder.getContext().setAuthentication(authResult);
会将认证结果放到SpringSecurity 的全局上下文对象 SecurityContextHolder供全局使用,然后调用this.rememberMeServices.loginSuccess(request, response, authResult);
实现下次请求无需认证,然后判断发布事件监听器如果不为空就 调用this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
获取一个InteractiveAuthenticationSuccessEvent交互式身份验证成功事件
最后调用this.successHandler.onAuthenticationSuccess(request, response, authResult);
成功处理器,至此SpringSecurity认证流程彻底完毕。
最后 处理认证失败的业务和处理成功的业务大差不差。