基本上设计到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现了对用户访问系统的控制,按照安全规则或策略,控制用户允许访问被授权的资源。
权限管理包括用户身份认证和授权两个部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限后方可访问。
身份认证,就是判断一个用户是否为合法用户的过程。最常用的身份认证方式就是系统通过比对用户名密码是否一致。
即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
在SpringSecurity中,认证授权是相互独立的,无论是什么样的认证方式,都不会影响授权。这么做的优势是方便整合外部系统。
在spring security中认证是由AuthenticationManager
接口来提供的,接口定义为
AuthenticationManager主要实现类为ProviderManager,在ProviderManager中管理了众多AuthenticationProvider实例。在一次完整的认证流程中,SpringSecutity允许存在多个AuthenticationProvider,用来实现多种认证方式,这些AuthenticcationProvider都是由ProviderManager来统一管理。
认证及认证成功的信息,主要由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;
}
SecurityContextHolder用来获取登录之后的用户信息。SpringSecurity会将登录用户数据保存在Session中。为了方便SpringSecurity在此基础上做了改进,其中最主要的变化就是线程绑定。当用户登录成功后,SpringSecurity会将登录成功的用户信息保存到SecurityContextHolder中。
SecurityContextHolder中的数据保存默认通过ThreadLocal来实现,使用ThreadLocal创建的变量,只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定。当请求处理完成后,SpringSecurity会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContextHolder中的数据清空。
后续请求时,都会从Session中取出用户登录数据,保存到SecurityContextHolder中,方便在该请求的后续处理过程中使用,同时在请求结束的时候,将SecurityContextHolder中的数据取出保存到Session中,然后将SecurityContextHolder中的数据清空。
该策略方便用户在Controller、Service层以及任何代码中获取当前登录用户数据。
AccessDecisionManager 用来决定此次访问是否被允许。
AccessDecisionVoter,投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票。
AccessDecisionVoter和AccessDecisionManager都有很多实现类,在AccessDecisionManager中会挨个遍历AccessDecisionVoter,进而决定是否允许用户访问。其二者的关系类似于Authentication和ProviderManager的关系。
ConfigAttribute,用来保存授权时候的角色信息。
在SpringSecurity中,用户请求一个资源需要的角色会被封装成一个ConfigAttribute对象,在ConfigAttribute中只有一个getAttribute方法,该方法返回一个String字符串,即角色名称。一般的,角色名称都带有一个ROLE_
前缀,投票器AccessDecisionVoter所做的事情,其实就是比较用户所具备的角色和请求某个资源所需的ConfigAttribute之间的关系。
@RestController
public class HiController {
@RequestMapping("/hi")
public String hi(){
return "HI Spring Security
";
}
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
// 默认生成的用户名User 密码如下:
Using generated security password: 536e7e1e-0b7c-4911-aa7c-5129496cb805
This 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整合到原生的过滤器链中。
在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 {
}
}
通过对自动配置的分析,可以看到默认的生效条件为以下两点。在默认情况下,条件都是满足的。
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 */
SpringSecurity就是通过这种方式,在默认过滤器中生成登录页面并返回的。
跟踪源码:org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter#doFilter
跟踪源码:String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
这里在正向的查看我们不容易看到是如何生成的默认user。我们可以通过认证来入手,反向推导。
通过源代码分析得知UserDetailsService是顶层父类接口,接口中的loadUserByUsername方法是用来在认证的时候进行用户名认证方法,默认实现是基于内存的实现,如果想要修改为数据库实现,我们需要自定义UserDetailsService实现类,最终返回UserDetails对象即可。
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
关键部分源代码:
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
@Lazy
public 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());
}
}
结论:
通过查看SecurityProperties源码,可以看见User内部类,并可以通过prefix给其配置账号密码。
spring:
security:
user:
name: admin
password: admin
roles: admin,users