SpringSecurity的简介与框架结构运行原理总览
什么是SpringSecurity
Spring全家桶的一员,Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
核心思想
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的。
整体结构总览与重要组件介绍
FilterChainProxy
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中的SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security的核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理。
SpringSecurity中主要的过滤器
1.SecurityContextPersistenceFilter
这个Filter是整个拦截过程的入口和出口,会在请求开始时从配置好的SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后 SecurityContextHolder 持有的SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext
2.UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,默认情况下处理来自 /login 的请求,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler
3.FilterSecurityInterceptor
FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问过滤安全拦截器。用于授权逻辑。
4.ExceptionTranslationFilter
ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
5.BasicAuthenticationFilter
如果在请求中找到一个 Basic Auth HTTP 头,如果找到,则尝试用该头中的用户名和密码验证用户。
6.DefaultLoginPageGeneratingFilter
默认登录页面生成过滤器。用于生成一个登录页面,如果你没有明确地禁用这个功能,那么就会生成一个登录页面。这就是为什么在启用Spring Security时,会得到一个默认登录页面的原因。
7.DefaultLogoutPageGeneratingFilter
如果没有禁用该功能,则会生成一个注销页面。
认证流程
认证流程图
认证核心组件关系图
AuthenticationManager接口
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而SpringSecurity支持多种认证方式,因此ProviderManager维护着一个List< AuthenticationProvider>列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider(DaoAuthenticationProvider)完成的,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充到Authentication。
AuthenticationProvider接口
public interface AuthenticationProvider {
/**
* authenticate()方法定义了认证的实现过程,它的参数Authentication,里面包含了登录用户所提交的用户名,密码等。而返回值也是一个Authentication,这个Authentication则是在认证成功后,将用户的权限及其他信息重新组装后生成。
* 由于SpringSecurity支持多种认证方式(List< AuthenticationProvider>),不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProvider1,短信登录时使用AuthenticationProvider2等等这样的例子很多。
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
/**
* 每个AuthenticationProvider需要实现supports()方法来表明自己支持的认证方式,如我们使用表单方式认证,在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken,它是由DaoAuthenticationProvider支持的
*/
boolean supports(Class<?> var1);
}
Authentication接口
public interface Authentication extends Principal, Serializable {
/**
* 权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
*/
Object getCredentials();
/**
* 细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。
*/
Object getDetails();
/**
* 身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。
*/
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
UserDetailsService
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
DaoAuthenticationProvider处理了web表单登录的认证逻辑,认证成功后得到一个Authentication对象(UsernamePasswordAuthenticationToken),里面包含了身份信息(Principal)。这个身份信息就是一个Object,大多数情况下它可以被强转为UserDetails对象。
DaoAuthenticationProvider中包含了一个UserDetailsService实例,它负责根据用户名提取用户信息UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据。
UserDetails
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider认证之后被填充的。
通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
Spring Security提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。
PasswordEncoder
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
1.什么是PasswordEncoder
DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求Authentication中的密码做对比呢?
在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现。
2.常见的PasswordEncoder
- NoOpPasswordEncode
- BCryptPasswordEncoder——常用的
- Pbkdf2PasswordEncoder
- SCryptPasswordEncoder
会话
SpringSecurity的会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
代码示例
private String getUsername(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(!authentication.isAuthenticated()){
return null;
}
Object principal = authentication.getPrincipal();
String username = null;
if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
username = ((org.springframework.security.core.userdetails.UserDetails)principal).getUsername();
} else {
username = principal.toString();
}
return username;
}
会话控制
1.会话控制机制说明
机制 | 描述 |
---|---|
always | 如果没有session存在就创建一个 |
ifRequired | 如果需要就创建一个Session(默认)登录时 |
never | SpringSecurity 将不会创建Session,但是如果应用中其他地方创建了Session,那么Spring Security将会使用它。 |
stateless | SpringSecurity将绝对不会创建Session,也不使用Session |
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
}
默认情况下,Spring Security会为每个登录成功的用户会新建一个Session,就是ifRequired 。
若选用never,则指示Spring Security对登录成功的用户不创建Session了,但若你的应用程序在某地方新建了session,那么Spring Security会用它的。
若使用stateless,则说明Spring Security对登录成功的用户不会创建Session了,你的应用程序也不会允许新建session。并且它会暗示不使用cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API及其无状态认证机制。
2.会话超时
可以再sevlet容器中设置Session的超时时间,如下设置Session有效期为3600s;
spring boot 配置文件:
//设置Session有效期
server.servlet.session.timeout=3600s
//httpOnly:如果为true,那么浏览器脚本将无法访问cookie
//secure:如果为true,则cookie将仅通过HTTPS连接发送
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
session超时之后,可以通过Spring Security 设置跳转的路径。
//expired指session过期,invalidSession指传入的sessionid无效。
http.sessionManagement()
.expiredUrl("/login-view?error=EXPIRED_SESSION")
.invalidSessionUrl("/login-view?error=INVALID_SESSION");
授权
授权概述
授权的方式包括 web授权和方法授权,web授权是通过 url拦截进行授权,方法授权是通过 方法拦截进行授权。他们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为FilterSecurityInterceptor;若为方法授权则拦截器为MethodSecurityInterceptor。如果同时通过web授权和方法授权则先执行web授权,再执行方法授权,最后决策通过,则允许访问资源,否则将禁止访问。
授权流程图
- 拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的FilterSecurityInterceptor的子类拦截。
- 获取资源访问策略,FilterSecurityInterceptor会从SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource获取要访问当前资源所需要的权限Collection< ConfigAttribute>。
- SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略
- 最后,FilterSecurityInterceptor会调用AccessDecisionManager进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
授权核心组件关系图
AccessDecisionManager(访问决策管理器)
public interface AccessDecisionManager {
/**
* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
* @param authentication:要访问资源的访问者的身份
* @param object:要访问的受保护资源,web请求对应FilterInvocation
* @param configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
* @param decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
*/
void decide(Authentication authentication , Object object, Collection<ConfigAttribute> configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException;
//略..
}
AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。
AccessDecisionVoter接口
AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。
public interface AccessDecisionVoter<S> {
//同意
int ACCESS_GRANTED = 1;
//弃权
int ACCESS_ABSTAIN = 0;
//拒绝
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}
vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。如果一个AccessDecisionVoter不能判定当前Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。
AccessDecisionManager实现类
- AffirmativeBased
- ConsensusBased
- UnanimousBased
AffirmativeBased
- 只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问
- 如果全部弃权也表示通过
- 如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException
- Spring security默认使用的是AffirmativeBased
ConsensusBased
- 如果赞成票多于反对票则表示通过
- 反过来,如果反对票多于赞成票则将抛出AccessDeniedException
- 如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。
- 如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。
UnanimousBased
- 必须全票通过才可以,有一个反对就会抛出AccessDeniedException
- 如果全部弃权则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException。
web授权
1.代码
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')")
.antMatchers("/r/**").authenticated()
.anyRequest().permitAll();
}
2.注意
授权的顺序是很重要的,把精确放前面模糊的放后面
3.保护URL常用的方法
- authenticated() 保护URL,需要用户登录
- permitAll() 指定URL无需保护,一般应用与静态资源文件
- hasRole(String role) 限制单个角色访问,角色将被增加 “ROLE_” .所以”ADMIN” 将和 “ROLE_ADMIN”进行比较.
- hasAuthority(String authority) 限制单个权限访问
- hasAnyRole(String… roles)允许多个角色访问.
- hasAnyAuthority(String… authorities) 允许多个权限访问.
- access(String attribute) 该方法使用 SpEL表达式, 所以可以创建复杂的限制.
- hasIpAddress(String ipaddressExpression) 限制IP地址或子网
方法授权
1.方法授权可以使用的注解
- @PreAuthorize
- @PostAuthorize
- @Secured
2.开启方法授权
//在SpringSecurity的配置类上标注此注解
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
3.@Secured注解的使用
public interface BankService {
//该方法可以匿名访问
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
//该方法可以匿名访问
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
//只有TELLER角色才能访问这个方法
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
4.@prePostEnabled注解的使用
public interface BankService {
//该方法可以匿名访问
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
//该方法可以匿名访问
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
//同时拥有p_transfer和p_read_account权限才能访问
@PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')")
public Account post(Account account, double amount);
}