文章目录
- 一、权限管理
- 认证(是否合法用户)
- 授权(能否访问资源权限)
- 解决方案
- 二、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/
章节
- 权限管理
- 简介
- 认证&原理
- 自定义认证
- 密码加密(对称、非对称)
- Remember Me
- 会话管理(适用于前后端分离、微服务)
- CSRF
- 跨域CROS
- 全局异常处理
- 授权&授权模型
- OAuth2 & JWT
- 前后端分离实战
一、权限管理
基本上设计到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现了对用户访问系统的控制,按照安全规则或策略,控制用户允许访问被授权的资源。
权限管理包括用户身份认证和授权两个部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限后方可访问。
认证(是否合法用户)
身份认证,就是判断一个用户是否为合法用户的过程。最常用的身份认证方式就是系统通过比对用户名密码是否一致。
授权(能否访问资源权限)
即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
解决方案
- 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之间的关系。
三、环境搭建
- 使用idea的Spring Initialzr来初始化项目SpringBoot项目。
- 创建HiController.java,访问http://localhost:8888/hi
@RestControllerpublic class HiController {@RequestMapping("/hi")public String hi(){return "HI Spring Security
";}}
- 引入SpringSecurity依赖,只要引入该依赖,SpringSecurity就会进行默认的权限控制。默认所有请求都必须验证后才可访问。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
- 重新启动应用,可以看到控制台打印日志
// 默认生成的用户名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 {}}
通过对自动配置的分析,可以看到默认的生效条件为以下两点。在默认情况下,条件都是满足的。
- classpath中存在SecurityFilterChain.class,HttpSecurity.class
- 没有自定义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. 流程分析
- 请求/hello接口,在引入SpringSecurity后会经过一系列过滤器
- 在请求到达FilterSecurityInterceptor时,发现请求未认证,则拦截,并抛出AccessDeniedException
- 抛出的异常会被ExceptionTranslationFilter捕获,这个Filter中,会调用LoginUrlAuthenticationEntryPoint#commence方法,给客户端响应302,将页面重定向到/login
- 客户端发送/login请求
- /login请求再次被拦截器中的DefaultLoginPageGeneratingFilter拦截,并在拦截器中返回生成的登录页面。
SpringSecurity就是通过这种方式,在默认过滤器中生成登录页面并返回的。
跟踪源码:org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter#doFilter
跟踪源码:String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
八、默认用户生成
这里在正向的查看我们不容易看到是如何生成的默认user。我们可以通过认证来入手,反向推导。
- 查看SpringBootWebSecurityConfiguration#defaultSecurityFilterChain 表单登录方法
- 处理登录为FormLoginConfigurer类中调用UsernamePasswordAuthenticationFilter这个类实例
- 查看类UsernamePasswordAuthenticationFilter#attempAuthentication方法可以得知实际调用的是AuthenticationManager中的authenticate方法
- 调用ProviderManager中的authenticate方法
- 调用ProviderManager实现类AbstractUserDetailsAuthenticationProvider类的authenticate方法
- 最终调用实现类DaoAuthenticationProvider类中的方法比较
- 到这里就可以知道默认是基于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());}}
结论:
- 从自动配置源码中得知classpath下存在AuthenticationManager类
- 当前项目中,系统没有提供AuthenticationManager、 AuthenticationProvider、 UserDetailsService、AuthenticationManagerResolver实例
- 默认情况下都是满足的,这时候SpringSecurity会提供一个InMemoryUserDetailsManager实例
通过查看SecurityProperties源码,可以看见User内部类,并可以通过prefix给其配置账号密码。
spring:security:user:name: adminpassword: adminroles: admin,users
九、环境搭建总结
- AuthenticationManager、ProviderManager、AuthenticationProvider关系
– WebSecurityConfigurerAdapter扩展SpringSecurity所有默认配置
- UserDetailsService用来修改默认认证的数据源信息