1. 背景

Spring Security最主要的两个功能:认证和授权

功能 解决的问题 Spring Security中主要类
认证(Authentication) 你是谁 AuthenticationManager
授权(Authorization) 你可以做什么 AuthorizationManager

之前的文章讲过了使用Spring Security来接入其他单点登录系统,其中单点登录系统来解决你是谁的问题,Spring Security用来解决你可以做什么的问题

这次我们主要使用Spring Security的认证功能

这次的需求是这样的,我们有一个A服务,A服务内部有n个接口,这些接口根据使用方的不同可以分为三部分

  1. C端用户使用的接口,需要c端账户体系鉴权,后续会有网关统一鉴权,目前暂时用账号密码鉴权
  2. B端用户使用的接口,需要b端账户体系鉴权,由其他OAuth2 Server来鉴权
  3. 开发人员使用的接口,账号密码鉴权

Spring Security版本: 5.3.x

2. 目的

本篇文章的目的是通过实际的场景来让大家如何配置Spring Security来实现自己的需求,明白每一行的配置作用

3. 整体配置

@Configuration
@EnableWebSecurity
public class SecurityConfig { @Configuration
@Order(1)
public static class BSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.antMatcher("/b/**")
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.addFilterBefore(new MyTokenJwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
} @Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/b/api/auth/access_token");
}
} @Configuration
@Order(2)
public static class CSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${algo-api.password}")
private String password; @Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/c/**")
.authorizeRequests()
.anyRequest()
.hasRole("C_ADMIN")
.and()
.httpBasic(); }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("cAdmin777")
.password(password)
.roles("ALGO_ADMIN");
}
} @Configuration
@Order(3)
public static class DevSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${swagger.password}")
private String password;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/swagger-ui/**", "/v3/api-docs/**", "/test/**")
.hasRole("SWAGGER_ADMIN")
.antMatchers("/login", "/css/**", "/js/**", "/images/**")
.permitAll()
.anyRequest()
.denyAll()
.and()
.formLogin();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("swaggerAdmin999")
.password(password)
.roles("SWAGGER_ADMIN");
}
}
}

3.1 配置解读

3.1.1 SecurityConfig类

首先是一个SecurityConfig类,带了@Configuration和@EnableWebSecurity注解,代表接入了Springboot和开启了SpringSecurity功能

3.1.2 WebSecurityConfigurerAdapter子类

内部是三个静态内部类BSecurityConfig、CSecurityConfig、DevSecurityConfig分别对应我们的需求的使用方,B端用户、C端用户和开发人员,他们继承了WebSecurityConfigurerAdapter类,继承此类可以重写其中的方法来进行配置,具体Spring Security是如何做的可以看后面的源码分析

Spring Security官方文档中可以看到,使用不同的SecurityFilterChain对不同的url前缀进行安全配置

所以,这里的三个静态内部类就是对应的三个SecurityFilterChain

那么有的同学可能会问,如果url的路径冲突了怎么办,比如两个SecurityFilterChain的路径前缀一样,那就取决于WebSecurityConfigurerAdapter的子类加载顺序,会使用先匹配的SecurityFilterChain,可以看到我们的静态内部类也使用了@Order(3)这样的注解来标识顺序

从断点中可以有4个SecurityFilterChain,且每个SecurityFilterChain内部的过滤器数量不同

为什么多了一个SecurityFilterChain,因为在BSecurityConfig中我们忽略了/b/api/auth/access_token路径的安全配置,所以对于这个路径下一个过滤器也没有

WebSecurityConfigurerAdapter类三个的主要的配置方法,也就是我们需要重写的方法:

protected void configure(AuthenticationManagerBuilder auth)
public void configure(WebSecurity web)
protected void configure(HttpSecurity http)

AuthenticationManagerBuilder这个类是来配置认证相关的,例如使用内存中的账号密码,还是JDBC的数据来认证,从图中可以看出其作用

3.1.3 HttpSecurity配置

这个是我们主要使用的配置类,它的常用方法有:

csrf和cors,具体可以参考我之前的博客

3.1.3.1 Match方法

这些方法用来指定这个配置是对哪些路径生效的

  • antMatcher
  • mvcMatcher
  • regexMatcher
  • requestMatchers

其中requestMatchers是比较灵活的匹配路径的一种方式,适合匹配多个路径

注意: 区分antMatcher和antMatchers

antMatchers antMatcher
所属类 AbstractRequestMatcherRegistry HttpSecurity
参数 多个url通配符 单个url通配符
作用 细粒度的鉴权,每个路径下面需要的鉴权不同,权限也可能不同 粗粒度的鉴权,每个路径是一个SecurityFilterChain,有一组过滤器

3.1.3.2 authorizeRequests方法

返回的是ExpressionUrlAuthorizationConfigurer

用来表示要开启一个细粒度的鉴权,两个方法功能一样,写法不一样,推荐下面这种,可以把某个细粒度的路径的鉴权写到一起,而不是整个链式,难以阅读,后续使用的类是ExpressionInterceptUrlRegistry

  • authorizeRequests
  • authorizeRequests(authorizeRequestsCustomizer)
3.1.3.2.1 match

和HttpSecurity一样,细粒度的圈选路径

  • antMatchers,符合表达式的请求
  • mvcMatchers
  • regexMatchers
  • anyRequest,任意请求
3.1.3.2.2 访问权限

在圈选完路径之后,声明此路径的鉴权方式,也就是需不需要登录,需要什么权限才能方法

  • permitAll,允许访问
  • anonymous,允许匿名
  • rememberMe,记住我
  • denyAll,不允许访问
  • authenticated,允许登录后访问
  • hasRole,需要某个角色可以访问
  • hasAuthority,需要某个权限可以访问

3.1.3.3 其他

3.1.3.3.1 xxxLogin

表示需要此种登录,例如formLogin表示需要进行表单登录,可以指定一些登录页,登录失败页面等,或者默认生成一些页面

oauth2Login表示使用OAuth2.0来做验证,这种就很方便的使用第三方应用来登录,例如支付宝授权登录

3.1.3.3.2 sessionManagement

需要进行session管理

3.1.3.3.3 addFilterxxx

手动的往Spring的过滤器链里面加一个过滤器,实际上,SpringSecurity所有的功能都是在过滤器链里面加过滤器,只不过有现成的稳定的类就不用我们自己写的,当现有的满足不了我们的需要时,我们可以自己来实现

3.1.4 WebSecurity配置

WebSecurity是全局配置类,它的常用方法有:

  • ignoring 配置不做控制的路径
  • httpFirewall 设置防火墙,拦截非法的请求,非法的http method,包含非法的符号等
  • debug 开启debug日志

上面的代码中,我们对/b/api/auth/access_token路径的请求不做控制

4. 源码分析

整体架构如上所示:

  • SecurityBuilder和SecurityConfigurer是核心接口类,前者是构建者模式,后者负责定制化配置
  • HttpSecurity和WebSecurity是SecurityBuilder的两个核心实现类,一个负责收集整体的认证配置,一个负责收集指定路径的认证配置
  • XXXXSecurityConfigurer代表一系列的配置类,包括前面将的CRSF, CORS,登录、session等各种功能的实现
  • WebSecurityConfigurerAdapter是适配器模式,通过定制化HttpSecurity和WebSecurity这两个Builder来定制化真正的配置
  • WebSecurityConfiguration是自动配置类,也是Spring Security的启动类,它调用WebSecurity的init/build方法最终生成一个Filter,作为Servlet容器的一个普通Filter来使用

4.1 EnableWebSecurity

我们在使用Spring Security的时候需要加这个注解,这个注解使用@Import导入了WebSecurityConfiguration配置类

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity { /**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}

4.2 WebSecurityConfiguration

这个类很关键,下面是这个类的核心方法,通过Value注入了List<SecurityConfigurer<Filter, WebSecurity>> 这个就是我们配置的多个WebSecurityConfigurerAdapter的类的父类,也就是把我们的实现类都给收集起来,然后排序,最后放到webSecurity这个对象中去

	public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
// 我们继承WebSecurityConfigurerAdapter的子类被在这被收集起来
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}
// 排序
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
// 放入webSecurity中
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;

下面注册了一个Servlet Filter,这个Filter实现了整个Spring Security的功能

@Bean(
name = {"springSecurityFilterChain"}
)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
// 假如开发者没有定制化过WebSecurityConfigurerAdapter,会默认new一个
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
} return (Filter)this.webSecurity.build();
}

4.2 WebSecurity

其实没有很多逻辑在里面,基本就是构造者的思想,在不断填充各种属性,包括调用了HttpSecurity的 securityFilterChainBuilder.build()

	protected Filter performBuild() throws Exception {
Assert.state(
!securityFilterChainBuilders.isEmpty(),
() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
+ "More advanced users can invoke "
+ WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy;
if (debugEnabled) {
logger.warn("\n\n"
+ "********************************************************************\n"
+ "********** Security debugging is enabled. *************\n"
+ "********** This may include sensitive information. *************\n"
+ "********** Do not use in a production system! *************\n"
+ "********************************************************************\n\n");
result = new DebugFilter(filterChainProxy);
}
postBuildAction.run();
return result;

4.3 HttpSecurity

也比较简单

protected DefaultSecurityFilterChain performBuild() {
this.filters.sort(this.comparator);
return new DefaultSecurityFilterChain(this.requestMatcher, this.filters);
}

它的更多逻辑其实在父类AbstractConfiguredSecurityBuilder的模板方法里面,init方法和configure方法才是真正把我们配置的哪些XXXXSecurityConfigurer类真正实现和生效

   protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = (O)this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
} private void init() throws Exception {
for(SecurityConfigurer<O, B> configurer : this.getConfigurers()) {
configurer.init(this);
} for(SecurityConfigurer<O, B> configurer : this.configurersAddedInInitializing) {
configurer.init(this);
} } private void configure() throws Exception {
for(SecurityConfigurer<O, B> configurer : this.getConfigurers()) {
configurer.configure(this);
}
}

4.4 XXXXSecurityConfigurer

当我们调用HttpSecurity的cors方法后,是怎样把真正的Configurer注册的

可以看到HttpSecurity注册了一个CorsConfigurer类,这个类按照上面说的后面会被init和configure

  public CorsConfigurer<HttpSecurity> cors() throws Exception {
return (CorsConfigurer)this.getOrApply(new CorsConfigurer());
}

下面是CorsConfigurer的configure,可以看到它的实现非常容易理解,生成一个Filter,然后加到了HttpSecurity中,至于Filter内部的逻辑可以不必关注,就是一个普通的Servlet Filter,然后解决跨域访问的问题

    public void configure(H http) {
ApplicationContext context = (ApplicationContext)http.getSharedObject(ApplicationContext.class);
CorsFilter corsFilter = this.getCorsFilter(context);
if (corsFilter == null) {
throw new IllegalStateException("Please configure either a corsFilter bean or a corsConfigurationSourcebean.");
} else {
http.addFilter(corsFilter);
}
}

其他的SecurityConfigurer内部逻辑可能很复杂,但最终逻辑其实都是在过滤器链里面加过滤器,实现拦截或者通过

附录

如何查看和编译Spring Security源码

下载仓库时,可以不下载历史,会更快和占更少的空间,指定分支

git clone --depth 1 https://github.com/spring-projects/spring-security.git -b 5.3.x

在下载依赖时,某个spring的插件怎么也下载不下来,因为spring的插件仓库在2021年关闭了对公众的访问,需要授权才能用(可以理解,但是很难受)

https://spring.io/blog/2020/10/29/notice-of-permissions-changes-to-repo-spring-io-fall-and-winter-2020

https://spring.io/blog/2022/12/14/notice-of-permissions-changes-to-repo-spring-io-january-2023

从公告中看镜像插件应该是可用的,但确实git下载的代码无法同步到依赖

需要修改build.gradle文件,

buildscript {
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute module('org.apache.xerces:xercesImpl') with module('xerces:xercesImpl:2.9.1')
substitute module('org.apache.xerces:resolver') with module('xerces:resolver:2.9.1')
}
}
dependencies {
classpath 'io.spring.gradle:spring-build-conventions:0.0.38-SNAPSHOT'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
classpath 'io.spring.nohttp:nohttp-gradle:0.0.10'
classpath "io.freefair.gradle:aspectj-plugin:4.1.6"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
repositories {
maven { url 'https://maven.aliyun.com/repository/spring-plugin' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
maven {
url = 'https://repo.spring.io/plugins-snapshot'
}
maven {
url = 'https://repo.spring.io/plugins-milestone'
}
maven {
url = 'https://repo.spring.io/plugins-release'
}
maven { url 'https://plugins.gradle.org/m2/' }
}
} apply plugin: 'io.spring.nohttp'
apply plugin: 'locks'
apply plugin: 'io.spring.convention.root'
apply plugin: 'org.jetbrains.kotlin.jvm' group = 'org.springframework.security'
description = 'Spring Security' ext.snapshotBuild = version.contains("SNAPSHOT")
ext.releaseBuild = version.contains("SNAPSHOT")
ext.milestoneBuild = !(snapshotBuild || releaseBuild) dependencyManagementExport.projects = subprojects.findAll { !it.name.contains('-boot') } repositories {
mavenCentral()
} subprojects {
plugins.withType(JavaPlugin) {
project.sourceCompatibility='1.8'
} tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
}

主要修改了两点:

  1. 修改了插件spring-build-conventions的版本
  2. 新增了阿里云插件仓库

总之,是部分包Spring的仓库有,部分包阿里云仓库有,运气好的情况下,正确同步到了依赖

注意,最终还是没完全同步成功完依赖,因为某些spring仓库无法访问的问题,但可以进行部分类的跳转

SpringSecurity配置和源码解析的更多相关文章

  1. Go语言备忘录:net/http包的使用模式和源码解析

    本文是晚辈对net/http包的一点浅显的理解,文中如有错误的地方请前辈们指出,以免误导! 转摘本文也请注明出处:Go语言备忘录:net/http包的使用模式和源码解析,多谢!  目录: 一.http ...

  2. Dubbo原理和源码解析之服务引用

    一.框架设计 在官方<Dubbo 开发指南>框架设计部分,给出了引用服务时序图: 另外,在官方<Dubbo 用户指南>集群容错部分,给出了服务引用的各功能组件关系图: 本文将根 ...

  3. Dubbo原理和源码解析之标签解析

    一.Dubbo 配置方式 Dubbo 支持多种配置方式: XML 配置:基于 Spring 的 Schema 和 XML 扩展机制实现 属性配置:加载 classpath 根目录下的 dubbo.pr ...

  4. Dubbo原理和源码解析之“微内核+插件”机制

    github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...

  5. Dubbo原理和源码解析之服务暴露

    github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...

  6. Go语言备忘录(3):net/http包的使用模式和源码解析

    本文是晚辈对net/http包的一点浅显的理解,文中如有错误的地方请前辈们指出,以免误导! 转摘本文也请注明出处:Go语言备忘录(3):net/http包的使用模式和源码解析,多谢!  目录: 一.h ...

  7. SpringBoot Profile使用详解及配置源码解析

    在实践的过程中我们经常会遇到不同的环境需要不同配置文件的情况,如果每换一个环境重新修改配置文件或重新打包一次会比较麻烦,Spring Boot为此提供了Profile配置来解决此问题. Profile ...

  8. Spring源码解析02:Spring IOC容器之XmlBeanFactory启动流程分析和源码解析

    一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...

  9. Spring源码解析 | 第二篇:Spring IOC容器之XmlBeanFactory启动流程分析和源码解析

    一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...

  10. rest-framework之视图和源码解析

    视图和源码解析 通过使用mixin类编写视图: from rest_framework import mixins from rest_framework import generics class ...

随机推荐

  1. jmeter之请求体类型

    一.当post方法的提交数据类型(content-type)为multipart/form-data,请求体为文件文件上传. fiddler抓包请求体的name对应jmerter文件上传的参数名称,f ...

  2. ArcGIS拼接、镶嵌同一空间位置的不同遥感影像

      本文介绍在ArcGIS下属的ArcMap软件中,对处于同一空间位置的多幅栅格图像加以拼接.融合与叠加等操作的方法.   假如现在我们分别有以下三幅栅格图像,三者分别是独立的三个图层.第一个图层如下 ...

  3. SpringBoot——SSM简单整合v0.1

    学习SpringBoot初次整合SSM,后续需要不断优化 参考SpringBoot3教程[1] 导入依赖 pom.xml <?xml version="1.0" encodi ...

  4. packer 学习笔记

    前言 网上有一个老哥用 packer 制作镜像的博客里开篇就提到[1]. Failure is success in progress. -- Albert Einstein 不要害怕失败,在用 pa ...

  5. JS 构造函数与类

    严格来说, JS 并不是一个面向对象的语言, 类似 Java, Python, C++ 这样的. JS 的独特精妙的设计其实是 原型 prototype 因此这里讲一嘴面向对象其实是为了后面引出原型的 ...

  6. 手把手部署n8n

    n8n 是当前非常热门的开源 AI 工作流平台,在 GitHub 上已获得超过九万颗 star. 通过 n8n,用户可以拖拽节点,轻松搭建复杂的 AI 工作流.每个节点都支持上千种插件,可灵活集成各类 ...

  7. MCP SSE交互完整过程

    有关MCP的介绍文章很多,本文不细说,做一个时序图将完整过程说清楚.MCP协议主要通过两种技术实现:标准输入输出(stdio)和服务器发送事件(SSE),stdio(标准输入输出)是MCP协议在本地或 ...

  8. 推荐一个Elasticsearch ES可视化客户端工具:ES-King,支持win、mac、linux

    ES-King:开源免费,一个现代.实用的ES GUI客户端,支持多平台. 下载地址:https://github.com/Bronya0/ES-King 我之前开源的kafka客户端kafka-ki ...

  9. C#/.NET/.NET Core技术前沿周刊 | 第 39 期(2025年5.19-5.25)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  10. 《HelloGitHub》第 110 期

    兴趣是最好的老师,HelloGitHub 让你对开源感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. github.com/521xueweihan/HelloG ...