前言

本文将从示例、原理、应用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 的配置很简单,可以继承WebSecurityConfigurerAdapterWebSecurityConfigurerAdapter是默认情况下 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 实践 + 源码分析的更多相关文章

  1. Spring Security OAuth2 源码分析

    Spring Security OAuth2 主要两部分功能:1.生成token,2.验证token,最大概的流程进行了一次梳理 1.Server端生成token (post /oauth/token ...

  2. Spring IOC 容器源码分析 - 填充属性到 bean 原始对象

    1. 简介 本篇文章,我们来一起了解一下 Spring 是如何将配置文件中的属性值填充到 bean 对象中的.我在前面几篇文章中介绍过 Spring 创建 bean 的流程,即 Spring 先通过反 ...

  3. Spring IOC 容器源码分析系列文章导读

    1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本.经过十几年的迭代,现在的 Spring 框架已经非常成熟了.Spring ...

  4. Spring Security 解析(七) —— Spring Security Oauth2 源码解析

    Spring Security 解析(七) -- Spring Security Oauth2 源码解析   在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因 ...

  5. Spring Developer Tools 源码分析:二、类路径监控

    在 Spring Developer Tools 源码分析一中介绍了 devtools 提供的文件监控实现,在第二部分中,我们将会使用第一部分提供的目录监控功能,实现对开发环境中 classpath ...

  6. Spring IOC 容器源码分析 - 余下的初始化工作

    1. 简介 本篇文章是"Spring IOC 容器源码分析"系列文章的最后一篇文章,本篇文章所分析的对象是 initializeBean 方法,该方法用于对已完成属性填充的 bea ...

  7. Spring IOC 容器源码分析 - 循环依赖的解决办法

    1. 简介 本文,我们来看一下 Spring 是如何解决循环依赖问题的.在本篇文章中,我会首先向大家介绍一下什么是循环依赖.然后,进入源码分析阶段.为了更好的说明 Spring 解决循环依赖的办法,我 ...

  8. Spring IOC 容器源码分析 - 创建原始 bean 对象

    1. 简介 本篇文章是上一篇文章(创建单例 bean 的过程)的延续.在上一篇文章中,我们从战略层面上领略了doCreateBean方法的全过程.本篇文章,我们就从战术的层面上,详细分析doCreat ...

  9. Spring IOC 容器源码分析 - 创建单例 bean 的过程

    1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑.对于已实例化好的单例 bean,getBean(String) 方法并不会再一次去 ...

随机推荐

  1. [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 ...

  2. c#关于Mysql MySqlBulkLoader 批量上传

    有个list表有几万数据 用insert插入,速度跟蜗牛爬行, 几十个表,传起来可就需要时间了. 搜搜,发现有  MySqlBulkLoader  这个人家mysql 的dll 里边已经提供了这个方法 ...

  3. javaweb开发.常用的第三方包

      序号 开发包名称 描述 1 dom4j-1.6.1.jar dom4j用于操作XML文件 2 jaxen-1.1-beta-6.jar 用于解析XPath表达式 3 commons-beanuti ...

  4. Hello SIP Protocol

    SIP Request Line Request-Line = Method SP Request-URI SP SIP-Version CRLFMethod:        1. REGISTER ...

  5. css初始

    css概念及作用 css即层叠样式表的英文缩写 作用:1 渲染页面   2 页面布局 css语法 CSS 规则由两个主要的部分构成:选择器,以及一条或多条声明. 格式: selector{ prope ...

  6. java特殊字符分隔符

    点,string.split("[.]") . 竖线, string.split("\\|"). 星号, string.split("\\*" ...

  7. SpringMvc在返回数据之前进行统一处理

    这里其实有多种解决方案 如果你不需要获取request对象 可以采用aop(环绕通知)的方式来统一修改 如果你需要获取request对象,那么就需要采用下面的方式 0自己定义一个注解,内容如下 @Ta ...

  8. AX_Dialog

    Dialog                      dialog  = new Dialog("@SYS1052");  DialogField                 ...

  9. js图片预加载、有序加载

    <!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8&qu ...

  10. Java:内部接口

    1.什么是内部接口 内部接口也称为嵌套接口,即在一个接口内部定义另一个接口.举个例子,Entry接口定义在Map接口里面,如下代码: public interface Map { interface ...