spring security 实践 + 源码分析

 

前言

本文将从示例、原理、应用3个方面介绍 spring data jpa。html

如下分析基于spring boot 2.0 + spring 5.0.4版本源码java

概述

Spring Security 是一个可以为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组能够在 Spring 应用上下文中配置的 Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减小了为企业系统安全控制编写大量重复代码的工做。当前版本为 5.0.5。nginx

Spring Security 5 相比 4,主要有如下几点升级:web

  • 支持 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 的引入是为了构建页面,便于演示spring

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 均须要apache

至此,整个 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

 

相关文章
相关标签/搜索