集成Spring Security
Spring Security
概要
- https://spring.io/projects/spring-security
- Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 Spring Security 重要核心功能。
- 用户认证 指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。
- 用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
- 通俗点说就是系统认为用户是否能登录
- 用户授权 指的是验证某个用户是否有权限执行某个操作。
- 在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。
- 一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
- 通俗点讲就是系统判断用户是否有权限去做某些事情。
产品对比
- Spring Security:Spring 技术栈的组成部分。通过提供完整可扩展的认证和授权支持保护你的应用程序
- 和 Spring 无缝整合
- 全面的权限控制
- 专门为 Web 开发而设计,旧版本不能脱离 Web 环境使用
- 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块,单独引入核心模块就可以脱离 Web 环境
- 重量级
- Shiro:Apache 旗下的轻量级权限控制框架。特点:
- 轻量级。Shiro 主张的理念是把复杂的事情变简。针对对性能有更高要求的互联网应用有更好表现。
- 好处:不局限于 Web 环境,可以脱离 Web 环境使用
- 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制
- Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 SpringBoot 出现之前,Spring Security 就已经发展多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。
- 自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security。因此,一般来说,常见的安全管理技术栈的组合是这样的:
- SSM + Shiro
- Spring Boot/Spring Cloud + Spring Security
- 单纯从技术上来说,无论怎么组合,都是可以运行的
世界你好
新建项目
新建一个 maven 项目,配置基本依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pers.fulsun</groupId>
<artifactId>ruoyi-study</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.13.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- SpringBoot 核心包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- SpringBoot 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
引入依赖
pom.xml 中添加 Spring Security 依赖
1
2
3
4
5<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>编写一个测试 Controller
1
2
3
4
public String hello(){
return "hello";
}当访问 http://localhost: 8080/hello 就会出现让你登录的页面,也可以证明 Spring Security 生效了
- Spring Security 默认有一个用户名: user
- 密码是随机生成的, 控制台会答应出来:
Using generated security password: 7ec56d22-bfd7-4b61-9948-3821385fea0c
设置登录系统的账号、密码
application.properties
1
2spring.security.user.name=admin
spring.security.user.password=admin配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(
AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("admin");
auth.inMemoryAuthentication().withUser("admin").password(password).roles("admin");
}
//注入 PasswordEncoder 类到 Spring 容器中
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
基本原理
相关概念
- 主体: 英文单词:principal
- 使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统谁就是主体。
- 认证: 英文单词:authentication
- 权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁。
- 笼统的认为就是以前所做的 登录操作
- 授权: 英文单词:authorization
- 将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。
- 所以简单来说,授权就是 给用户分配权限。
原理分析
SpringSecurity 本质上就是一个过滤器链, 现在对这条过滤器链的各个进行说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44# 将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
# 在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信息就是这个过滤器处理的。
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
# 用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的 usernameParameter 和 passwordParameter 两个参数的值进行修改。
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
# 主要是包装请求对象 request。
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
# 检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
# 管理 session 的过滤器
org.springframework.security.web.session.SessionManagementFilter
# 处理 AccessDeniedException 和 AuthenticationException 异常。
org.springframework.security.web.access.ExceptionTranslationFilter
# 可以看做过滤器链的出口。
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
# 当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
RememberMeAuthenticationFilter
流程图
客户端发起一个请求,进入 Security 过滤器链。
当到 LogoutFilter 的时候判断是否是登出路径
如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则交由 ExceptionTranslationFilter
如果不是登出路径则直接进入下一个过滤器。
当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径
- 如果是,则进入该过滤器进行登录操作,登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理
- 如果不是登录请求则不进入该过滤器。
当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
认证流程
- 用户提交用户名、密码被 SecurityFilterChain 中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求 Authentication,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
- 然后过滤器将 Authentication 提交至认证管理器(AuthenticationManager)进行认证 。
- 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
- SecurityContextHolder 安全上下文容器将第 3 步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
可以看出 AuthenticationManager 接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为 ProviderManager。
而 Spring Security 支持多种认证方式,因此 ProviderManager 维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider 完成的。
咱们知道 web 表单的对应的 AuthenticationProvider 实现类为 DaoAuthenticationProvider,它的内部又维护着一个 UserDetailsService 负责 UserDetails 的获取。最终 AuthenticationProvider 将 UserDetails 填充至 Authentication。
授权策略
Spring Security 可以通过 http.authorizeRequests() 对 web 请求进行授权保护。Spring Security 使用标准 Filter 建立了对 web 请求的拦截,最终实现对资源的授权访问。授权流程如下:
Security 配置
- 在
WebSecurityConfigurerAdapter
这个类里面可以完成上述流程图的所有配置
配置类
1 |
|
AuthenticationManagerBuilder
configure(AuthenticationManagerBuilder auth)
- 配置 AuthenticationManagerBuilder 会让 Security 自动构建一个 AuthenticationManager(该类的功能参考流程图);
- 如果想要使用该功能你需要配置一个 UserDetailService 和 PasswordEncoder。
- UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户
- PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用 PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。
- Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 serDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。
WebSecurity
configure(WebSecurity auth)
- 这个配置方法用于配置静态资源的处理方式,可使用 Ant 匹配规则。
配置了登录页请求路径,密码属性名,用户名属性名,和登录请求路径,permitAll()代表任意用户可访问。
1
2
3
4
5
6
7http
.formLogin()
.loginPage("/login_page")
.passwordParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/sign_in")
.permitAll()权限相关的配置
1
2
3
4
5
6
7
8http
.authorizeRequests()
// 配置了一个 /test url 该有什么权限才能访问,
.antMatchers("/test").hasRole("test")
// anyRequest() 表示所有请求, authenticated() 表示已登录用户才能访问,
.anyRequest().authenticated()
// accessDecisionManager() 表示绑定在 url 上的鉴权管理器
.accessDecisionManager(accessDecisionManager());1
2
3
4http.authorizeRequests()
.antMatchers("/tets_a/**","/test_b/**").hasRole("test")
.antMatchers("/a/**","/b/**").authenticated()
.accessDecisionManager(accessDecisionManager())登出相关配置
1
2
3
4
5
6http
.logout()
// 登出 url 登出成功处理器
.logoutUrl("/logout")
// 登出成功处理器
.logoutSuccessHandler(new MyLogoutSuccessHandler())配置鉴权失败的处理器
1
2
3http
.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());在过滤器链中插入自己的过滤器
- addFilterBefore 加在对应的过滤器之前,addFilterAfter 加在对应的过滤器之后,addFilterAt 加在过滤器同一位置, 事实上框架原有的 Filter 在启动 HttpSecurity 配置的过程中,都由框架完成了其一定程度上固定的配置,是不允许更改替换的。
- 根据测试结果来看,调用 addFilterAt 方法插入的 Filter ,会在这个位置上的原有 Filter 之前执行。
1
2http.addFilterAfter(new MyFittler(), LogoutFilter.class);
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);HttpSecurity 使用的是链式编程,其中
http.xxxx.and.yyyyy
这种写法和http.xxxx;http.yyyy
写法意义一样。
自定义 AuthenticationManager
重写 authenticationManagerBean() 方法,并构造一个 authenticationManager:给 authenticationManager 配置了两个认证器,执行过程参考流程图。
1
2
3
4
5
6
7
public AuthenticationManager authenticationManagerBean() throws Exception {
ProviderManager authenticationManager = new ProviderManager(
Arrays.asLis(getMyAuthenticationProvider(),daoAuthenticationProvider())
);
return authenticationManager;
}
自定义 AccessDecisionManager
定义构造 AccessDecisionManager 的方法并在配置类中调用, 投票管理器会收集投票器投票结果做统计,最终结果大于等于 0 代表通过;
每个投票器会返回三个结果:-1(反对),0(通过),1(赞成)。
1
2
3
4
5
6
7
8
9public AccessDecisionManager accessDecisionManager(){
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new MyExpressionVoter(),
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter());
return new UnanimousBased(decisionVoters);
}
Security 权限接口
UserDetails:Security 中的用户接口,我们自定义用户类要实现该接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public interface UserDetails extends Serializable {
//表示获取登录用户的权限
Collection<? extends GrantedAuthority> getAuthorities();
//表示获取密码
String getPassword();
//表示获取用户名
String getUsername();
//表示判断账户是否过期
boolean isAccountNonExpired();
//表示判断账户是否被锁定
boolean isAccountNonLocked();
//表示凭证(密码)是否过期
boolean isCredentialsNonExpired();
//表示当前用户是否可用
boolean isEnabled();
}GrantedAuthority:Security 中的用户权限接口,自定义权限需要实现该接口:
authority 表示权限字段,需要注意的是在 config 中配置的权限会被加上 ROLE_ 前缀,
比如我们的配置 authorizeRequests().antMatchers(“/test”).hasRole(“test”),配置了一个 test 权限
但我们存储的权限字段(authority)应该是 ROLE_test 。
1
2
3public class MyGrantedAuthority implements GrantedAuthority {
private String authority;
}
UserDetailsService: Security 中的用户 Service,自定义用户服务类需要实现该接口:
1
2
3
4
5
6
7
8
public class MyUserDetailService implements UserDetailsService {
// 根据用户名查询用户对象
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return.....
}
}SecurityContextHolder: 用户在完成登录后 Security 会将用户信息存储到这个类中,之后其他流程需要得到用户信息时都是从这个类中获得
用户信息被封装成 SecurityContext ,而实际存储的类是 SecurityContextHolderStrategy
默认的 SecurityContextHolderStrategy 实现类是 ThreadLocalSecurityContextHolderStrategy 它使用了 ThreadLocal 来存储了用户信息。
手动填充 SecurityContextHolder 示例:
1
2UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);
SecurityContextHolder.getContext().setAuthentication(token);- 对于使用 token 鉴权的系统,我们就可以验证 token 后手动填充 SecurityContextHolder,填充时机只要在执行投票器之前即可,或者干脆可以在投票器中填充,然后在登出操作中清空 SecurityContextHolder。
PasswordEncoder: 数据加密接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14public interface PasswordEncoder {
//表示把参数按照特定的解析规则进行解析
String encode(CharSequence var1);
//表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。
// 如果密码匹配,则返回 true;如果不匹配,则返回 false,
// 第一个参数表示需要被解析的密码,第二个表示存储的密码
boolean matches(CharSequence var1, String var2);
//表示如果解析的密码能够再次进行解析且能达到更安全的结果则返回 true,否则返回 false。默认返回 false
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10
1
2
3
4
5
6
7
8
9
10
11
12
13
public void test01() {
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 对密码进行加密
String password = bCryptPasswordEncoder.encode("123456");
// 打印加密之后的数据
System.out.println("加密之后数据:\t" + password);
//判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("123456", password);
// 打印比较结果
System.out.println("比较结果:\t" + result);
}
Security 扩展
鉴权失败处理器
- Security 鉴权失败默认跳转登录页面,我们可以实现 AccessDeniedHandler 接口,重写 handle() 方法来自定义处理逻辑;然后参考配置类说明将处理器加入到配置当中。
验证器
- 实现 AuthenticationProvider 接口来实现自己验证逻辑。需要注意的是在这个类里面就算你抛出异常,也不会中断验证流程,而是算你验证失败,
- 我们由流程图知道,只要有一个验证器验证成功,就算验证成功,所以你需要留意这一点。
登录成功处理器
- 在 Security 中验证成功默认跳转到上一次请求页面或者路径为 “/“ 的页面,
- 我们同样可以自定义:继承 SimpleUrlAuthenticationSuccessHandler 这个类或者实现 AuthenticationSuccessHandler 接口。
- 我这里建议采用继承的方式, SimpleUrlAuthenticationSuccessHandler 是默认的处理器,采用继承可以契合里氏替换原则,提高代码的复用性和避免不必要的错误。
投票器
投票器可继承 WebExpressionVoter 或者实现 AccessDecisionVoter 接口;
WebExpressionVoter 是 Security 默认的投票器;我这里同样建议采用继承的方式;添加到配置的方式参考 上文;
注意:投票器 vote 方法返回一个 int 值;-1 代表反对,0 代表弃权,1 代表赞成;
投票管理器收集投票结果,如果最终结果大于等于 0 则放行该请求。
自定义 token 处理过滤器
- 自定义 token 处理器继承自 OncePerRequestFilter 或者 GenericFilterBean 或者 Filter 都可以
- 在这个处理器里面需要完成的逻辑是:获取请求里的 token,验证 token 是否合法然后填充 SecurityContextHolder ,
- 虽然说过滤器只要添加在投票器之前就可以,但我这里还是建议添加在 http.addFilterAfter(new MyFittler(), LogoutFilter.class);
登出成功处理器
- 实现 LogoutSuccessHandler 接口,添加到配置的方式参考上文。
登录失败处理器
- 登录失败默认跳转到登录页,我们同样可以自定义。
- 继承 SimpleUrlAuthenticationFailureHandler 或者实现 AuthenticationFailureHandler,建议采用继承。
自定义 UsernamePasswordAuthenticationFilter
我们自定义 UsernamePasswordAuthenticationFilter 可以极大提高我们 Security 的灵活性(比如添加验证验证码是否正确的功能)。
我们直接继承 UsernamePasswordAuthenticationFilter ,然后在配置类中初始化这个过滤器,给这个过滤器添加登录失败处理器,登录成功处理器,登录管理器,登录请求 url 。
这里配置略微复杂,贴一下代码清单
初始化过滤器:
1
2
3
4
5
6
7
8MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){
MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter(redisService);
myUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler());
myUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
myUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/sign_in");
myUsernamePasswordAuthenticationFilter.setAuthenticationManager(getAuthenticationManager());
return myUsernamePasswordAuthenticationFilter;
}添加到配置:
1
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
注解使用
- 使用注解先要开启注解功能!启动类(配置类)开启注解
@EnableGlobalMethodSecurity(securedEnabled = true)
@Secured
判断是否有角色,另外需要是否注意的是这里匹配的字符串需要添加前缀 “ROLE_“。
1
2
3
4
5
public String hello(){
return "hello";
}
@PreAuthorize
注解适合进入方法前的权限验证,@PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中
1
2
3
4
5
6//@Secured({”ROLE_TEST“,”ROLE_dev“})
public String hello(){
return "hello";
}
@PostAuthorize
- @PostAuthorize 注解使用的并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限
@PreFilter
- @PreFilter:进入控制器之前对数据进行过滤
@PostFilter
@PostFilter:权限验证之后对数据进行过滤
留下用户名是 admin 的数据
1
2
3
4
5
6
7
8
public List<User> getAllUser() {
ArrayList<User> users = new ArrayList<User>();
users.add(new User("admin1", 18));
users.add(new User("admin2", 19));
return users;
}
跨域请求
跨域请求:
- (自己理解)不是同一个地址发的请求;
- (官方)当前发起请求的域与该请求指向的资源所在的域不同时的请求。这里的域指的是这样的一个概念:我们认为如果 “协议 + 域名 + 端口号” 均相同,那么就是同域。
浏览器的同源策略: 同源策略(Same origin policy)是一种约定,它是浏览器最核心也是最基本的安全功能。出于安全考虑,浏览器限制从 JS 脚本发起的跨源 HTTP 请求。 例如,XMLHttpRequest 和 Fetch API 都遵循同源策略。
即 Web 应用为了安全考虑,禁止跨域请求,但是在前后端分离的大前提下,一般都是跨域请求,所以就需要通过配置去允许跨域请求的访问。
Srping Boot 的跨域请求配置:(也可以通过在控制器类上或者方法上标注@CrossOrigin 注解来实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 解决跨域请求问题的配置
*/
public class CrosConfig implements WebMvcConfigurer {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}以上的 Spring Boot 配置只能在没有 Spring Security 的前提下生效,一旦引入 Security,Security 也会对跨域请求进行安全验证,所以也需要配置
1
2
3
4
5
6
7
8
9
10
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity security) throws Exception {
security
// 开启跨域共享
.cors();
}
}以上全部配置,就能解决跨域请求的问题了,但是 Security 对安全的要求更高,所以会出现另一个问题,就是跨站请求伪造的问题
跨站信息伪造
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。CSRF 攻击者在用户已经登录目标网站之后,诱使用户访问一个攻击页面,利用目标网站对用户的信任,以用户身份在攻击页面对目标网站发起伪造用户操作的请求,达到攻击目的。
CSRF 攻击的原理大致描述如下:有两个网站,其中 A 网站是真实受信任的网站,而 B 网站是危险网站。在用户登陆了受信任的 A 网站是,本地会存储 A 网站相关的 Cookie,并且浏览器也维护这一个 Session 会话。这时,如果用户在没有登出 A 网站的情况下访问危险网站 B,那么危险网站 B 就可以模拟发出一个对 A 网站的请求(跨域请求)对 A 网站进行操作,而在 A 网站的角度来看是并不知道请求是由 B 网站发出来的(Session 和 Cookie 均为 A 网站的),这时便成功发动一次 CSRF 攻击。
因而 CSRF 攻击可以简单理解为:攻击者盗用了你的身份,以你的名义发送请求。CSRF 能够做的事情包括:以你的名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。
因此,大多数浏览器都会对跨域请求作出限制,这是从浏览器层面上的对 CSRF 攻击的一种防御,但是需要注意的是在复杂的网络环境中借助浏览器来防御 CSRF 攻击并不足够,还需要从服务端或者客户端方面入手防御,所以 Security 为了防止这种情况,就禁止跨站请求,你如果想跨站访问,就必须符合 Security 的访问规则.
但是为了学习方便,可以禁止跨站检测,即降低安全性,不去验证跨站请求。配置如下:
1
2
3
4
5
6
7
8
9
10
11
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity security) throws Exception {
security
.and()
// 跨域伪造请求限制.无效
.csrf().disable();
}
}
总结
对于 Security 的扩展配置关键在于 configure(HttpSecurityhttp) 方法;
扩展认证方式可以自定义 authenticationManager 并加入自己验证器,在验证器中抛出异常不会终止验证流程;
扩展鉴权方式可以自定义 accessDecisionManager 然后添加自己的投票器并绑定到对应的 url(url 匹配方式为 ant)上,
投票器
vote(Authenticationauthentication,FilterInvocationfi,Collection<ConfigAttribute>attributes)
方法返回值为三种:-1 0 1,分别表示反对弃权赞成。对于 token 认证的校验方式
- 可以暴露一个获取的接口
- 或者重写 UsernamePasswordAuthenticationFilter 过滤器和扩展登录成功处理器来获取 token
- 然后在 LogoutFilter 之后添加一个自定义过滤器,用于校验和填充 SecurityContextHolder。
另外,Security 的处理器大部分都是重定向的,我们的项目如果是前后端分离的话,我们希望无论什么情况都返回 json , 那么就需要重写各个处理器了。