【spring boot 系列】spring security 实践 + 源码分析

前言

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

以下分析基于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,通常这更加灵活。

概览图

图片描述

总结

用户登陆,会被 AuthenticationProcessingFilter 拦截,调用 AuthenticationManager 的实现,AuthenticationManager 会调用ProviderManager来获取用户验证信息(不同的 Provider 调用的服务不同,因为这些信息可以是在数据库上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。

访问资源(即授权管理)时,会通过 AbstractSecurityInterceptor 拦截器拦截,其中会调用 FilterInvocationSecurityMetadataSource 的方法来获取被拦截 url 所需的全部权限,在调用授权管理器 AccessDecisionManager,这个授权管理器会通过 spring 的全局缓存 SecurityContextHolder 获取用户的权限信息,还会获取被拦截的url及所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不足页面。

参考文档

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot 是一个用于构建微服务的开源框架,它能够快速搭建项目并且提供了许多便捷的功能和特性。Spring Security 是一个用于处理认证和授权的框架,可以保护我们的应用程序免受恶意攻击。JWT(JSON Web Token)是一种用于身份验证的开放标准,可以被用于安全地传输信息。Spring MVC 是一个用于构建 Web 应用程序的框架,它能够处理 HTTP 请求和响应。MyBatis 是一个用于操作数据库的框架,可以简化数据库操作和提高效率。Redis 是一种高性能的键值存储系统,可以用于缓存与数据存储。 基于这些技术,可以搭建一个商城项目。Spring Boot 可以用于构建商城项目的后端服务,Spring Security 可以确保用户信息的安全性,JWT 可以用于用户的身份验证,Spring MVC 可以处理前端请求,MyBatis 可以操作数据库,Redis 可以用于缓存用户信息和商品信息。 商城项目的后端可以使用 Spring BootSpring Security 来搭建,通过 JWT 来处理用户的身份验证和授权。数据库操作可以使用 MyBatis 来简化与提高效率,同时可以利用 Redis 来缓存一些常用的数据和信息,提升系统的性能。前端请求则可以通过 Spring MVC 来处理,实现商城项目的整体功能。 综上所述,借助于 Spring BootSpring Security、JWT、Spring MVC、MyBatis 和 Redis 这些技术,可以构建出一个高性能、安全可靠的商城项目,为用户提供良好的购物体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值