spring security 实践 + 源码分析
前言
本文将从示例、原理、应用3个方面介绍 spring data jpa。
以下分析基于spring boot 2.0 + spring 5.0.4版本源码
概述
Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。当前版本为 5.0.5。
Spring Security 5 相比 4,主要有以下几点升级:
- 支持 OAuth 2.0
- 支持 Spring WebFlux
- 可以使用 Reactor 的 StepVerifier 进行测试
示例
pom配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置非常简单,和 spring security 有关的就是 spring-boot-starter-security,web 和 thymeleaf 的引入是为了构建页面,便于演示
application.properties 配置
spring.thymeleaf.cache=false
spring.security.user.name=user
spring.security.user.password=password
spring.security.user.roles=USER
同样很简单,禁用thymeleaf的缓存功能,另外配置了一个角色为 USER 的用户,用户名/密码:user/password
security config 配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.authorizeRequests()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().fullyAuthenticated()
.and()
.formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
.and()
.logout().permitAll();
// @formatter:on
}
}
security 的配置很简单,可以继承WebSecurityConfigurerAdapter
,WebSecurityConfigurerAdapter
是默认情况下 spring security 的 http 配置。通常情况下,都会存在部分 url 请求不需要过安全验证,此时可以通过configure()
方法将不需要进行权限校验的 url 排除掉。上面的例子,指定了 静态资源、login 链接不需要过安全验证,其余 url 均需要
至此,整个 security 最简单的功能就已经实现了,是不是非常简单。下面我们用一个例子来试验下。定义一个 HomeController
@Controller
public class HomeController implements WebMvcConfigurer { @GetMapping("/")
public String home(Map<String, Object> model) {
model.put("message", "Hello World");
model.put("title", "Hello Home");
model.put("date", new Date());
return "home";
} @Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}
Spring 的 WebMvcConfigurer 接口提供了很多方法让我们来定制 SpringMVC 的配置,这里通过 addViewControllers 将 /login 请求映射到了资源 login.html
附上 WebMvcConfigurer 提供的配置方法
好了,启动 web 应用,可以体验安全验证的效果了。
如何实现多个用户呢
上面最简单的示例,用户权限信息是直接再配置文件中写死的,那么如何实现多个用户呢?多个角色呢?
通过自定义 UserDetailsService 实现,这里列举使用内存存放用户信息的方式。在上述的SecurityConfig
中增加配置:
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() throws Exception {
return new InMemoryUserDetailsManager(
User.withDefaultPasswordEncoder().username("admin").password("admin")
.roles("ADMIN", "USER", "ACTUATOR").build(),
User.withDefaultPasswordEncoder().username("user").password("user")
.roles("USER").build());
}
上述配置添加了2个用户,admin 和 user
如何实现方法级别的权限控制呢?
答案是也很方便,只要加上一个注解配置即可。在SecurityConfig
类上增加如下配置
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
开启注解配置的方式,开启方法执行前后的安全校验
写个简单的 service 做测试:
@Service
public class SimpleSecureService { @Secured("ROLE_USER")
public String secure() {
return "Hello User Security";
} @PreAuthorize("hasRole('ADMIN')")
public String authorized() {
return "Hello Admin Security";
}
}
通过配置,即实现了方法级别的安全校验,@Secured 和 @PreAuthorize 最大区别是后者支持 spring EL,前者不支持,故后者比前者功能更强大
如何实现权限集成呢?
像上面的例子 admin 只能访问 admin 授权的接口,而不能访问 user 的接口,而我们的业务场景往往是 admin 拥有最高权限,可访问其他所有用户的资源,故这里涉及到一个权限继承的问题(当然你可以在所有方法上都标记 admin 可访问)。
spring 提供了 RoleHierarchy 接口来实现权限的级联。
假设需要的级联关系是
A > B
B > C
C > D
D > E
D > F
那么对应的一级map配置
A --> [B]
B --> [C]
C --> [D]
D --> [E,F]
构造完之后的关系
A --> [B,C,D,E,F]
B --> [C,D,E,F]
C --> [D,E,F]
D --> [E,F]
原理介绍
核心组件
SecurityContextHolder
SecurityContextHolder 用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限,这些都被保存在 SecurityContextHolder 中。SecurityContextHolder 默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security 在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
如何获取当前用户的信息?
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
getAuthentication()
返回了认证信息,getPrincipal()
返回了身份信息,UserDetails 便是 Spring 对身份信息封装的一个接口。
Authentication
先看下接口定义
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
Authentication 是 spring security 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中的。可以见得,Authentication 在 spring security 中是最高级别的身份/认证的抽象。
由这个顶级接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。接口详细解读如下:
- getAuthorities(),权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是代表权限信息的一系列字符串。
- getCredentials(),密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
- getDetails(),细节信息,web 应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。
- getPrincipal(),最重要的身份信息,大部分情况下返回的是 UserDetails 接口的实现类,也是框架中的常用接口之一。
AuthenticationManager
初次接触 Spring Securit y的朋友相信会被 AuthenticationManager,ProviderManager ,AuthenticationProvider,这么多相似的 Spring 认证类搞得晕头转向,但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录),所以说 AuthenticationManager 一般不直接认证,AuthenticationManager 接口的常用实现类 ProviderManager 内部会维护一个 List<AuthenticationProvider> 列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个 AuthenticationProvider。在默认策略下,只需要通过一个 AuthenticationProvider 的认证,即可被认为是登录成功。
ProviderManager 中的 List,会依照次序去认证,认证成功则立即返回,若认证失败则返回 null,下一个AuthenticationProvider 会继续尝试认证,如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。
到这里,如果不纠结于 AuthenticationProvider 的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器 SecurityContextHolder,身份信息的抽象 Authentication,身份认证器 AuthenticationManager 及其认证流程。姑且在这里做一个分隔线。下面来介绍下 AuthenticationProvider 接口的具体实现。
DaoAuthenticationProvider
AuthenticationProvider 最最最常用的一个实现便是 DaoAuthenticationProvider。顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在 Spring Security 中。提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了 UserDetailsService,在 DaoAuthenticationProvider 中,对应的方法便是 retrieveUser,返回一个 UserDetails。还需要完成 UsernamePasswordAuthenticationToken 和 UserDetails密码的比对,这便是交给 additionalAuthenticationChecks 方法完成的,如果这个 void 方法没有抛异常,则认为比对成功。比对密码的过程,用到了 PasswordEncoder 和 SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。
DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
UserDetails与UserDetailsService
上面不断提到了 UserDetails 这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
它和 Authentication 接口很类似,比如它们都拥有 username,authorities,区分他们也是本文的重点内容之一。Authentication 的getCredentials()
与 UserDetails 中的getPassword()
需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication 中的getAuthorities()
实际是由 UserDetails 的getAuthorities()
传递而形成的。还记得Authentication 接口中的getUserDetails()
方法吗?其中的 UserDetails 用户详细信息便是经过了 AuthenticationProvider 之后被填充的。
UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。UserDetailsService 常见的实现类有 JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现 UserDetailsService,通常这更加灵活。
概览图
在此我向大家推荐一个架构学习交流QQ群:725633148 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!
总结
用户登陆,会被 AuthenticationProcessingFilter 拦截,调用 AuthenticationManager 的实现,AuthenticationManager 会调用ProviderManager来获取用户验证信息(不同的 Provider 调用的服务不同,因为这些信息可以是在数据库上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。
访问资源(即授权管理)时,会通过 AbstractSecurityInterceptor 拦截器拦截,其中会调用 FilterInvocationSecurityMetadataSource 的方法来获取被拦截 url 所需的全部权限,在调用授权管理器 AccessDecisionManager,这个授权管理器会通过 spring 的全局缓存 SecurityContextHolder 获取用户的权限信息,还会获取被拦截的url及所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不足页面。
原文链接:https://my.oschina.net/u/3800605/blog/1825151
-END-
spring security 实践 + 源码分析的更多相关文章
- Spring Security OAuth2 源码分析
Spring Security OAuth2 主要两部分功能:1.生成token,2.验证token,最大概的流程进行了一次梳理 1.Server端生成token (post /oauth/token ...
- Spring IOC 容器源码分析 - 填充属性到 bean 原始对象
1. 简介 本篇文章,我们来一起了解一下 Spring 是如何将配置文件中的属性值填充到 bean 对象中的.我在前面几篇文章中介绍过 Spring 创建 bean 的流程,即 Spring 先通过反 ...
- Spring IOC 容器源码分析系列文章导读
1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本.经过十几年的迭代,现在的 Spring 框架已经非常成熟了.Spring ...
- Spring Security 解析(七) —— Spring Security Oauth2 源码解析
Spring Security 解析(七) -- Spring Security Oauth2 源码解析 在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因 ...
- Spring Developer Tools 源码分析:二、类路径监控
在 Spring Developer Tools 源码分析一中介绍了 devtools 提供的文件监控实现,在第二部分中,我们将会使用第一部分提供的目录监控功能,实现对开发环境中 classpath ...
- Spring IOC 容器源码分析 - 余下的初始化工作
1. 简介 本篇文章是"Spring IOC 容器源码分析"系列文章的最后一篇文章,本篇文章所分析的对象是 initializeBean 方法,该方法用于对已完成属性填充的 bea ...
- Spring IOC 容器源码分析 - 循环依赖的解决办法
1. 简介 本文,我们来看一下 Spring 是如何解决循环依赖问题的.在本篇文章中,我会首先向大家介绍一下什么是循环依赖.然后,进入源码分析阶段.为了更好的说明 Spring 解决循环依赖的办法,我 ...
- Spring IOC 容器源码分析 - 创建原始 bean 对象
1. 简介 本篇文章是上一篇文章(创建单例 bean 的过程)的延续.在上一篇文章中,我们从战略层面上领略了doCreateBean方法的全过程.本篇文章,我们就从战术的层面上,详细分析doCreat ...
- Spring IOC 容器源码分析 - 创建单例 bean 的过程
1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑.对于已实例化好的单例 bean,getBean(String) 方法并不会再一次去 ...
随机推荐
- [leetcode]244. Shortest Word Distance II最短单词距离(允许连环call)
Design a class which receives a list of words in the constructor, and implements a method that takes ...
- c#关于Mysql MySqlBulkLoader 批量上传
有个list表有几万数据 用insert插入,速度跟蜗牛爬行, 几十个表,传起来可就需要时间了. 搜搜,发现有 MySqlBulkLoader 这个人家mysql 的dll 里边已经提供了这个方法 ...
- javaweb开发.常用的第三方包
序号 开发包名称 描述 1 dom4j-1.6.1.jar dom4j用于操作XML文件 2 jaxen-1.1-beta-6.jar 用于解析XPath表达式 3 commons-beanuti ...
- Hello SIP Protocol
SIP Request Line Request-Line = Method SP Request-URI SP SIP-Version CRLFMethod: 1. REGISTER ...
- css初始
css概念及作用 css即层叠样式表的英文缩写 作用:1 渲染页面 2 页面布局 css语法 CSS 规则由两个主要的部分构成:选择器,以及一条或多条声明. 格式: selector{ prope ...
- java特殊字符分隔符
点,string.split("[.]") . 竖线, string.split("\\|"). 星号, string.split("\\*" ...
- SpringMvc在返回数据之前进行统一处理
这里其实有多种解决方案 如果你不需要获取request对象 可以采用aop(环绕通知)的方式来统一修改 如果你需要获取request对象,那么就需要采用下面的方式 0自己定义一个注解,内容如下 @Ta ...
- AX_Dialog
Dialog dialog = new Dialog("@SYS1052"); DialogField ...
- js图片预加载、有序加载
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8&qu ...
- Java:内部接口
1.什么是内部接口 内部接口也称为嵌套接口,即在一个接口内部定义另一个接口.举个例子,Entry接口定义在Map接口里面,如下代码: public interface Map { interface ...