文章目录

  • 一、权限管理
    • 认证(是否合法用户)
    • 授权(能否访问资源权限)
    • 解决方案
  • 二、SpringSecurity整体架构
    • 1. 认证
      • 1.1 AuthenticationManager
      • 1.2 Authentication
      • 1.3 SecurityContextHolder
    • 2. 授权 Authorization
      • 2.1 AccessDecisionManager 访问决策管理器
      • 2.2 AccessDecisionVoter 访问决定投票器
      • 2.3 ConfigAttribute
  • 三、环境搭建
  • 四、实现原理
  • 五、内置FIlter以及默认加载Filter
  • 六、自动配置分析
  • 七、生成默认登陆页面
    • 1. 流程分析
  • 八、默认用户生成
    • UserDetailsService
    • UserDetailsServiceAutoConfiguration
  • 九、环境搭建总结

官方文档: https://spring.io/projects/spring-security/
章节

  1. 权限管理
  2. 简介
  3. 认证&原理
  4. 自定义认证
  5. 密码加密(对称、非对称)
  6. Remember Me
  7. 会话管理(适用于前后端分离、微服务)
  8. CSRF
  9. 跨域CROS
  10. 全局异常处理
  11. 授权&授权模型
  12. OAuth2 & JWT
  13. 前后端分离实战

一、权限管理

基本上设计到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现了对用户访问系统的控制,按照安全规则或策略,控制用户允许访问被授权的资源。
权限管理包括用户身份认证和授权两个部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限后方可访问。

认证(是否合法用户)

身份认证,就是判断一个用户是否为合法用户的过程。最常用的身份认证方式就是系统通过比对用户名密码是否一致。

授权(能否访问资源权限)

即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

解决方案

  • Shiro:轻量、简单、易集成
  • Spring Security:对OAuth2良好支持、对SpringCloud、SpringBoot良好支持
    简介:SpringSecurity是一个面向Java的功能强大、高度定制的身份认证和访问控制的安全框架。
  • 开发者自定义:

二、SpringSecurity整体架构

在SpringSecurity中,认证授权是相互独立的,无论是什么样的认证方式,都不会影响授权。这么做的优势是方便整合外部系统。

1. 认证

1.1 AuthenticationManager

在spring security中认证是由AuthenticationManager接口来提供的,接口定义为

  • 返回Authentication对象,表示认证成功
  • 抛出AuthenticationException异常,表示认证失败

AuthenticationManager主要实现类为ProviderManager,在ProviderManager中管理了众多AuthenticationProvider实例。在一次完整的认证流程中,SpringSecutity允许存在多个AuthenticationProvider,用来实现多种认证方式,这些AuthenticcationProvider都是由ProviderManager来统一管理。

1.2 Authentication

认证及认证成功的信息,主要由Authentication的实现类来进行保存,接口定义:

package org.springframework.security.core;public interface Authentication extends Principal, Serializable {// 获取用户权限信息Collection<" />extends GrantedAuthority> getAuthorities();// 获取用户凭证信息,密码Object getCredentials();// 获取用户详情信息Object getDetails();// 获取用户身份信息,用户名,用户对象等Object getPrincipal();// 用户是否认证成功boolean isAuthenticated();void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;}

1.3 SecurityContextHolder

SecurityContextHolder用来获取登录之后的用户信息。SpringSecurity会将登录用户数据保存在Session中。为了方便SpringSecurity在此基础上做了改进,其中最主要的变化就是线程绑定。当用户登录成功后,SpringSecurity会将登录成功的用户信息保存到SecurityContextHolder中。
SecurityContextHolder中的数据保存默认通过ThreadLocal来实现,使用ThreadLocal创建的变量,只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定。当请求处理完成后,SpringSecurity会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContextHolder中的数据清空。
后续请求时,都会从Session中取出用户登录数据,保存到SecurityContextHolder中,方便在该请求的后续处理过程中使用,同时在请求结束的时候,将SecurityContextHolder中的数据取出保存到Session中,然后将SecurityContextHolder中的数据清空。
该策略方便用户在Controller、Service层以及任何代码中获取当前登录用户数据。

2. 授权 Authorization

2.1 AccessDecisionManager 访问决策管理器

AccessDecisionManager 用来决定此次访问是否被允许。

2.2 AccessDecisionVoter 访问决定投票器

AccessDecisionVoter,投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票。

  • 赞成:表示用户拥有该资源
  • 反对:会将结果告诉AccessDecisionManager,此时会拒绝请求


AccessDecisionVoter和AccessDecisionManager都有很多实现类,在AccessDecisionManager中会挨个遍历AccessDecisionVoter,进而决定是否允许用户访问。其二者的关系类似于Authentication和ProviderManager的关系。

2.3 ConfigAttribute

ConfigAttribute,用来保存授权时候的角色信息。

在SpringSecurity中,用户请求一个资源需要的角色会被封装成一个ConfigAttribute对象,在ConfigAttribute中只有一个getAttribute方法,该方法返回一个String字符串,即角色名称。一般的,角色名称都带有一个ROLE_前缀,投票器AccessDecisionVoter所做的事情,其实就是比较用户所具备的角色和请求某个资源所需的ConfigAttribute之间的关系。

三、环境搭建

  1. 使用idea的Spring Initialzr来初始化项目SpringBoot项目。
  2. 创建HiController.java,访问http://localhost:8888/hi
@RestControllerpublic class HiController {@RequestMapping("/hi")public String hi(){return "

HI Spring Security

"
;}}
  1. 引入SpringSecurity依赖,只要引入该依赖,SpringSecurity就会进行默认的权限控制。默认所有请求都必须验证后才可访问。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
  1. 重新启动应用,可以看到控制台打印日志
// 默认生成的用户名User 密码如下:Using generated security password: 536e7e1e-0b7c-4911-aa7c-5129496cb805This generated password is for development use only. Your security configuration must be updated before running your application in production.

问题:

  • 为什么引入SpringSecurity后没有任何配置所有请求就要认证?
  • 在项目中没有登录界面,那么登录页面是存放在哪里?
    为什么使用user和控制台的密码就可以登录,登陆时验证数据源在哪里?

四、实现原理

https://docs.spring.io/spring-security/reference/servlet/architecture.html
默认过滤器并不是直接放在WEB项目的原生过滤器链中,而是通过FilterChainProxy来统一管理。SpringSecurity中的过滤器通过FilterChainProxy嵌入到Web项目的原生过滤器链中。FilterChainProxy作为一个顶层的管理者,将统一管理SecurityFilter。FilterChainProxy本身通过Spring提供的DelegatingFilterProxy整合到原生的过滤器链中。

五、内置FIlter以及默认加载Filter

在SpringSecurity中给我们默认提供了以下过滤器:
加载顺序由上自下依次加载。
默认的,springBoot在对SpringSecurity进行自动配置的时候,会创建一个名为SpringSecurityFilterChain的过滤器,并注入到Spring容器中,这个过滤器负责所有的安全管理。包括用户认证、授权、重定向到登录页等。具体参考WebSecurityConfiguration类。

六、自动配置分析

官方文档:https://docs.spring.io/spring-security/reference/servlet/getting-started.html#servlet-hello-auto-configuration
SpringBootWebSecurityConfiguration类是SpringBoot自动配置类,通过这个源码得知,默认情况下对所有请求进行权限控制。
这也就是为什么在引入SpringSecurity后,没有任何配置,就会拦截所有请求。

在SpringBootWebSecurityConfiguration类中跟踪注解@ConditionalOnDefaultWebSecurity,继续跟踪DefaultWebSecurityCondition.class。

class DefaultWebSecurityCondition extends AllNestedConditions {DefaultWebSecurityCondition() {super(ConfigurationPhase.REGISTER_BEAN);}@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })static class Classes {}@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class,SecurityFilterChain.class })@SuppressWarnings("deprecation")static class Beans {}}

通过对自动配置的分析,可以看到默认的生效条件为以下两点。在默认情况下,条件都是满足的。

  1. classpath中存在SecurityFilterChain.class,HttpSecurity.class
  2. 没有自定义WebSecurityConfigAdapter.class,SecurityFilterChain.class

WebSecurityConfigurationAdapter类极为重要,SpringSecurity核心配置都在这个类中。如果要对SpringSecurity进行自定义配置,就要自定义这个类的实例,通过覆盖类中的方法达到修改默认配置的目的。
不过我在看的时候发现这个类已经过时了,官方推荐使用SecurityFilterChain或者WebSecurityCustomizer来配置。

/* @deprecated Use a {@link org.springframework.security.web.SecurityFilterChain} Bean to * configure {@link HttpSecurity} or a {@link WebSecurityCustomizer} Bean to configure */

七、生成默认登陆页面

1. 流程分析

  1. 请求/hello接口,在引入SpringSecurity后会经过一系列过滤器
  2. 在请求到达FilterSecurityInterceptor时,发现请求未认证,则拦截,并抛出AccessDeniedException
  3. 抛出的异常会被ExceptionTranslationFilter捕获,这个Filter中,会调用LoginUrlAuthenticationEntryPoint#commence方法,给客户端响应302,将页面重定向到/login
  4. 客户端发送/login请求
  5. /login请求再次被拦截器中的DefaultLoginPageGeneratingFilter拦截,并在拦截器中返回生成的登录页面。

SpringSecurity就是通过这种方式,在默认过滤器中生成登录页面并返回的。

跟踪源码:org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter#doFilter
跟踪源码:String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);

八、默认用户生成

这里在正向的查看我们不容易看到是如何生成的默认user。我们可以通过认证来入手,反向推导。

  1. 查看SpringBootWebSecurityConfiguration#defaultSecurityFilterChain 表单登录方法
  2. 处理登录为FormLoginConfigurer类中调用UsernamePasswordAuthenticationFilter这个类实例
  3. 查看类UsernamePasswordAuthenticationFilter#attempAuthentication方法可以得知实际调用的是AuthenticationManager中的authenticate方法
  4. 调用ProviderManager中的authenticate方法
  5. 调用ProviderManager实现类AbstractUserDetailsAuthenticationProvider类的authenticate方法
  6. 最终调用实现类DaoAuthenticationProvider类中的方法比较
  7. 到这里就可以知道默认是基于InMemoryUserDetailsManager这个类实现的,即基于内存。

UserDetailsService


通过源代码分析得知UserDetailsService是顶层父类接口,接口中的loadUserByUsername方法是用来在认证的时候进行用户名认证方法,默认实现是基于内存的实现,如果想要修改为数据库实现,我们需要自定义UserDetailsService实现类,最终返回UserDetails对象即可。

package org.springframework.security.core.userdetails;public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}

UserDetailsServiceAutoConfiguration

关键部分源代码:

package org.springframework.boot.autoconfigure.security.servlet;@AutoConfiguration// 生效机制:@ConditionalOnClass(AuthenticationManager.class)// classpath存在AuthenticationManager类,默认情况在引入SpringSecurity源码中就存在该类// 当存在ObjectPostProcessor类时@ConditionalOnBean(ObjectPostProcessor.class) // 并且没有定义过如下实例@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,AuthenticationManagerResolver.class },type = { "org.springframework.security.oauth2.jwt.JwtDecoder","org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector","org.springframework.security.oauth2.client.registration.ClientRegistrationRepository","org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })public class UserDetailsServiceAutoConfiguration {@Bean@Lazypublic InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,ObjectProvider<PasswordEncoder> passwordEncoder) {SecurityProperties.User user = properties.getUser();List<String> roles = user.getRoles();return new InMemoryUserDetailsManager(User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build());}}

结论:

  1. 从自动配置源码中得知classpath下存在AuthenticationManager类
  2. 当前项目中,系统没有提供AuthenticationManager、 AuthenticationProvider、 UserDetailsService、AuthenticationManagerResolver实例
  3. 默认情况下都是满足的,这时候SpringSecurity会提供一个InMemoryUserDetailsManager实例


通过查看SecurityProperties源码,可以看见User内部类,并可以通过prefix给其配置账号密码。

spring:security:user:name: adminpassword: adminroles: admin,users

九、环境搭建总结

  • AuthenticationManager、ProviderManager、AuthenticationProvider关系
    – WebSecurityConfigurerAdapter扩展SpringSecurity所有默认配置
  • UserDetailsService用来修改默认认证的数据源信息