Spring Security是解决安全访问控制的问题,说白了就是认证和授权两个问题。而至于像之前示例中页面控件的查看权限,是属于资源具体行为。SpringSecurity虽然也提供了类似的一些支持,但是这些不是Spring Security控制的重点。Spring Security功能的重点是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。而Spring Security对Web资源的保护是通过Filter来实现的,所以要从Filter入手,逐步深入Spring Security原理。
当初始化Spring Security时,org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration中会往Spring容器中注入一个名为SpringSecurityFilterChain的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy。它实现了javax.servlet.Filter,因此外部的请求都会经过这个类。
而FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时,这些Filter都已经注入到Spring容器中,他们是Spring Security的核心,各有各的职责。但是他们并不直接处理用户的认证和授权,而是把他们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理。下面是FilterChainProxy相关类的UML图示:
Spring Security的功能实现主要就是由一系列过滤器链相互配合完成的。在启动过程中可以看到有info日志。
下面介绍过滤器链中主要的几个过滤器及其作用:
SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的SecurityContextRepository 中获取 SecurityContext,然后把它设置给SecurityContextHolder。在请求完成后将SecurityContextHolder 持有的SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除securityContextHolder 所持有的 SecurityContext;
UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和AccessDeniedException,其它的异常它会继续抛出。
具体流程:
(1)、用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter 过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
(2)、 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
(3)、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication 实例。
(4)、SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List 列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终
AuthenticationProvider将UserDetails填充至Authentication。
public interface AuthenticationProvider {
//认证的方法
Authentication authenticate(Authentication authentication) throws
AuthenticationException;
//支持哪种认证
boolean supports(Class<?> var1);
}
这里对于AbstractUserDetailsAuthenticationProvider,他的support方法就表明他可以处理用户名密码这样的认证
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
继承自Principal类,代表一个抽象主体身份。继承了一个getName()方法来表示主体的名称
public interface Authentication extends Principal, Serializable {
//获取权限信息列表
Collection<? extends GrantedAuthority> getAuthorities();
//获取凭证信息。用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
Object getCredentials();
//细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地 址和sessionId的值。
Object getDetails();
//身份信息,大部分情况下返回的是UserDetails接口的实现类
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
获取用户信息的基础接口,只有一个根据用户名获取用户信息的方法。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws
UsernameNotFoundException;
}
在DaoAuthenticationProvider的retrieveUser方法中,会获取spring容器中的UserDetailsService。如果我们没有自己注入UserDetailsService对象,那么在UserDetailsServiceAutoConfiguration类中,会在启动时默认注入一个带user用户
的UserDetailsService。我们可以通过注入自己的UserDetailsService来实现加载自己的数据。
代表了一个用户实体,包括用户、密码、权限列表,还有一些状态信息,包括账号过期、认证过期、是否启用
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
DaoAuthenticationProvider在additionalAuthenticationChecks方法中会获取Spring容器中的PasswordEncoder来对用户输入的密码进行比较。
这是SpringSecurity中最常用的密码解析器。他使用BCrypt算法。他的特点是加密可以加盐sault,但是解密不需要盐。因为盐就在密文当中。这样可以通过每次添加不同的盐,而给同样的字符串加密出不同的密文。
密文形如:
$2a 10 10 10vTUDYhjnVb52iM3qQgi2Du31sq6PRea6xZbIsKIsmOVDnEuGb/.7K
其中:$是分割符,无意义;2a是bcrypt加密版本号;10是cost的值;而后的前22位是salt值;再然后的字符串就是密码的密文了
授权是在用户认证通过后,对访问资源的权限进行检查的过程。Spring Security可以通过http.authorizeRequests()对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问
授权的流程:
1、拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中(实现类为DefaultSecurityFilterChain)的 FilterSecurityInterceptor 的子类拦截。
2、获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限Collection 。
SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则,读取访问策略如:
http.csrf().disable()//关闭csrg跨域检查
//这里注意matchere是有顺序的。
.authorizeRequests()
.antMatchers("/mobile/**").hasAuthority("mobile")
.antMatchers("/salary/**").hasAuthority("salary")
.antMatchers("/common/**").permitAll() //common下的请求直接通过
.anyRequest().authenticated() //其他请求需要登录
.and() //并行条件
.formLogin().defaultSuccessUrl("/main.html").failureUrl("/common/loginFailed");
3、最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
关于AccessDecisionManager接口,最核心的就是其中的decide方法。这个方法就是用来鉴定当前用户是否有访问对应受保护资源的权限。
public interface AccessDecisionManager {
//通过传递的参数来决定用户是否有访问对应受保护资源的权限
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws
AccessDeniedException,
InsufficientAuthenticationException;
}
这里着重说明一下decide的参数:authentication:要访问资源的访问者的身份,
object:要访问的受保护资源,web请求对应FilterInvocation。
configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
在AccessDecisionManager的实现类ConsensusBased中,是使用投票的方式来确定是否能够访问受保护的资源。
AccessDecisionManager中包含了一系列的AccessDecisionVoter讲会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终角色**(为什么要投票? 因为权限可以从多个方面来进行配置,有角色但是没有
资源怎么办?这就需要有不同的处理策略)**
AccessDecisionVoter是一个接口,定义了三个方法:
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
vote()就是进行投票的方法。投票可以表示赞成、拒绝、弃权。
Spring Security内置了三个基于投票的实现类,分别是AffirmativeBased,ConsensusBasesd和UnanimaousBased
AffirmativeBased是Spring Security默认使用的投票方式,他的逻辑是只要有一
个投票通过,就表示通过。
1、只要有一个投票通过了,就表示通过。
2、如果全部弃权也表示通过。
3、如果没有人投赞成票,但是有人投反对票,则抛出AccessDeniedException.
ConsensusBased的逻辑是:多数赞成就通过
1、如果赞成票多于反对票则表示通过
2、如果反对票多于赞成票则抛出AccessDeniedException
3、如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表示通过,否则抛出
AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认是true。
4、如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异
常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。
UnanimousBased相当于一票否决。
1、如果受保护对象配置的某一个ConfifigAttribute被任意的
AccessDecisionVoter反对了,则将抛出AccessDeniedException。
2、如果没有反对票,但是有赞成票,则表示通过。
3、如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则
通过,false则抛出AccessDeniedException。
Spring Security默认是使用的AffirmativeBased投票器,我们同样可以通过往
Spring容器里注入的方式来选择投票决定器
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter(),
new MinuteBasedVoter());
return new UnanimousBased(decisionVoters);
}
然后在configure中配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.anyRequest()
.authenticated()
.accessDecisionManager(accessDecisionManager());
}
//配置安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/r/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()//允许表单登录
.loginPage("/login‐view")//自定义登录页面
.loginProcessingUrl("/login")//自定义登录处理地址
.defaultSuccessUrl("/main.html")//指定登录成功后的跳转地址-页面重定向
// .successForwardUrl("/login‐success")//指定登录成功后的跳转URL - 后端跳转
.permitAll();
}
修改UserDetails,从数据库加载用户信息。修改HttpSecurity,从数据库加载授权配置
(1)、代码方式配置
Spring Security可以通过HttpSecurity配置URL授权信息,保护URL常用的方法有:
authenticated() 保护URL,需要用户登录
permitAll() 指定URL无需保护,一般应用与静态资源文件
hasRole(String role) 限制单个角色访问。角色其实相当于一个"ROLE_"+role的资源。
hasAuthority(String authority) 限制单个权限访问
hasAnyRole(String... roles)允许多个角色访问.
hasAnyAuthority(String... authorities) 允许多个权限访问.
access(String attribute) 该方法使用 SpEL表达式, 所以可以创建复杂的限制.
hasIpAddress(String ipaddressExpression) 限制IP地址或子网
(2)、注解方式配置
Spring Security除了可以通过HttpSecurity配置授权信息外,还提供了注解方式对方法进行授权。注解方式需要先在启动加载的类中打开
@EnableGlobalMethodSecurity(securedEnabled=true) 注解,然后在需要权限管理的方法上使用@Secured(Resource)的方式配合权限
@EnableGlobalMethodSecurity(securedEnabled=true) 开启@Secured 注解过滤权限
打开后@Secured({"ROLE_manager","ROLE_admin"}) 表示方法需要有manager和admin
两个角色才能访问
另外@Secured注解有些关键字,比如IS_AUTHENTICATED_ANONYMOUSLY 表示可以匿名登
录。
@EnableGlobalMethodSecurity(jsr250Enabled=true) 开启@RolesAllowed 注解过滤权限
@EnableGlobalMethodSecurity(prePostEnabled=true) 使用表达式时间方法级别的安全性,
打开后可以使用一下几个注解。
@PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问。例如
@PreAuthorize("hasRole('normal') AND hasRole('admin')")
@PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常。此注释支持使用returnObject来表示返回的对象。例如 @PostAuthorize("returnObject!=null &&returnObject.username == authentication.name")
@PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
@PreFilter 允许方法调用,但必须在进入方法之前过滤输入值
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管
理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。可以通过为SecurityContextHolder.getContext().getAuthentication()获取当前登录用户信息。
@RestController
@RequestMapping("/common")
public class LoginController {
@GetMapping("/getLoginUserByPrincipal")
public String getLoginUserByPrincipal(Principal principal){
return principal.getName();
}
@GetMapping(value = "/getLoginUserByAuthentication")
public String currentUserName(Authentication authentication) {
return authentication.getName();
}
@GetMapping(value = "/username")
public String currentUserNameSimple(HttpServletRequest request) {
Principal principal = request.getUserPrincipal();
return principal.getName();
}
@GetMapping("/getLoginUser")
public String getLoginUser(){
Principal principal =
(Principal)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return principal.getName();
}
}
可以通过配置sessonCreationPolicy参数来了控制如何管理Session。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) }
这个属性有几个选项:
机制 描述
always 如果没有Session就创建一个
ifRequired如果需要就在登录时创建一个,默认策略
never SpringSecurity将不会创建Session。但是如果应用中其他地方创建了
Session,那么Spring Security就会使用。
stateless SpringSecurity将绝对不创建Session,也不使用。适合于一些REST API
的无状态场景。
会话超时时间可以通过spring boot的配置直接审定。
server.servlet.session.timeout=3600s
session超时后,可以通过SpringSecurity的http配置跳转地址
http.sessionManagement()
.expiredUrl("/login‐view?error=EXPIRED_SESSION")
.invalidSessionUrl("/login‐view?error=INVALID_SESSION");
expired是指session过期,invalidSession指传入的sessionId失效。
我们可以使用httpOnly和secure标签来保护我们的会话cookie:
httpOnly: 如果为true,那么浏览器脚本将无法访问cookie
secure: 如果为true,则cookie将仅通过HTTPS连接发送
spring boot 配置文件:
server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true
Spring Security默认实现了logout退出,直接访问/logout就会跳转到登出页面,而ajax访问/logout就可以直接退出。
在WebSecurityConfifig的config(HttpSecurity http)中,也是可以配置退出的一些属性,例如自定义退出页面、定义推出后的跳转地址。
http
.and()
.logout() //提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用
.logoutUrl("/logout") //默认退出地址
.logoutSuccessUrl("/login‐view?logout") //退出后的跳转地址
.addLogoutHandler(logoutHandler) //添加一个LogoutHandler,用于实现用户退出时
的清理工作.默认 SecurityContextLogoutHandler 会被添加为最后一个 LogoutHandler 。
.invalidateHttpSession(true);
//指定是否在退出时让HttpSession失效,默认是true
在退出操作时,会做以下几件事情:
1、使HTTP Session失效。
2、清除SecurityContextHolder
3、跳转到定义的地址。
logoutHandler
一般来说, LogoutHandler 的实现类被用来执行必要的清理,因而他们不应该抛出
异常。
下面是Spring Security提供的一些实现:
PersistentTokenBasedRememberMeServices 基于持久化token的RememberMe功能的相关清理
TokenBasedRememberMeService 基于token的RememberMe功能的相关清
理
CookieClearingLogoutHandler 退出时Cookie的相关清理
CsrfLogoutHandler 负责在退出时移除csrfTokenSecurityContextLogoutHandler 退出时SecurityContext的相关清理
链式API提供了调用相应的 LogoutHandler 实现的快捷方式,比如
deleteCookies()