目录
1、授权步骤
(1)URL 权限控制:调用 HttpSecurity#authorizeRequests() 方法,开始配置 URL 的权限控制。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin();//表单提交
// 授权 -> 认证拦截
http.authorizeRequests()
.antMatchers("/login.html", "/error.html", "/main.html").permitAll()
.antMatchers("/admin/test").hasAnyAuthority("admin")
.anyRequest().authenticated();// 所有请求都必须认证
http.csrf().disable(); //关闭csrf防护
}
(2)方法权限控制:修改 WebSecurityConfig配置类,增加 @EnableGlobalMethodSecurity 注解,开启对 Spring Security 注解的方法,进行权限验证。
@Configuration // 标记为注解类
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
}
2、基于Filter的授权流程
FilterSecurityInterceptor 进行资源拦截,当拦截资源为方法时,还会进入到拦截器MethodSecurityInterceptor,它们的继承关系如下图
授权流程如下
1.拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的FilterSecurityInterceptor 的子类拦截。
2. 获取资源访问策略,FilterSecurityInterceptor 会从 SecurityMetadataSource 的子类DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限Collection。SecurityMetadataSource其实就是读取访问策略的抽象,读取的内容就是我们配置的访问规则。
3. 最后,FilterSecurityInterceptor 会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
3、授权源码分析
进入 FilterSecurityInterceptor,进入 FilterSecurityInterceptor#doFilter 方法
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
// 从 invoke 开始
invoke(fi);
}
然后进入 invoke 方法
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
// 避免重新进行安全检查
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
// 第一次调用此请求时,执行安全检查
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 通过这个方法进行权限验证
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
进入 AbstractSecurityInterceptor#beforeInvocation,在该方法中,首先会去获取当前访问资源的权限,然后再进行权限验证的投票决策
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
// 1-获取访问资源的权限->资源是authenticated,还是permitAll
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
if (debug) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
// 2-获取登陆用户的权限
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
// 3-核心投票方法:尝试进行权限验证
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
进入权限验证决策方法,AccessDecisionManager#decide,AccessDecisionManager 有三个实现,默认使用 AffirmativeBased,其中两种决策方式,文章后边也会介绍。此处,我们先看第一个
进入 AffirmativeBased#decide 方法,我们去看它的决策实现
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
// 对权限进行循环遍历
for (AccessDecisionVoter voter : getDecisionVoters()) {
// 获取投票结果
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED://同意返回 -> 跳出循环
return;
case AccessDecisionVoter.ACCESS_DENIED: //拒绝
deny++; // 统计拒绝次数
break; // 继续下一次循环
default:
break; // 弃权 -> 继续下一次循环
}
}
// 如果统计拒绝次数 > 0
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// 如果每个投票者都选择放弃,默认也会抛出 AccessDeniedException
// private boolean allowIfAllAbstainDecisions = false; 是否允许放弃决策,默认flase
checkAllowIfAllAbstainDecisions();
}
至此,投票决策逻辑分析完成。
4、AccessDecisionManager 的三种实现
AccessDecisionManager 采用投票的方式来确定是否能够访问受保护资源。 AccessDecisionManager 中包含的一系列 AccessDecisionVoter 将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。
// 做出最终的访问控制(授权)决策。
public interface AccessDecisionManager {
// ~ Methods
// 为传递的参数解析访问控制决策。
// authentication 调用方法的调用者(非空)
// object 对象被调用的受保护对象
// configAttributes 与被调用的受保护对象关联的配置属性
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;
// 指示此AccessDecisionManager是否能够处理传入的ConfigAttribute。
boolean supports(ConfigAttribute attribute);
// 指示AccessDecisionManager实现是否能够为指定的受保护对象类型提供访问控制决策。
boolean supports(Class<?> clazz);
}
AffirmativeBased
如果有 AccessDecisionVoter 的投票为 ACCESS_GRANTED 则同意用户进行访问,但如果统计到反对票,则将抛出AccessDeniedException。
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
// 获取投票结果
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED: // 匹配到同意票,直接返回
return;
case AccessDecisionVoter.ACCESS_DENIED: // 决绝
deny++;
break;
default:
break;
}
}
// 统计到拒绝票,抛出异常
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// 全体弃票,抛出异常
checkAllowIfAllAbstainDecisions();
}
Spring security默认使用的是AffirmativeBased。
ConsensusBased(少数服从多数)
如果赞成票多于反对票则表示通过,如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions 的值为true,则表示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int grant = 0;
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
// 1-获取投票结果
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
grant++; // 统计同意次数
break;
case AccessDecisionVoter.ACCESS_DENIED:
deny++; // 统计决绝次数
break;
default:
break;
}
}
if (grant > deny) { // 2-比较同意次数和拒绝次数
return;
}
if (deny > grant) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
if ((grant == deny) && (grant != 0)) { // 3-判断是否允许两个次数相等的情况
if (this.allowIfEqualGrantedDeniedDecisions) {
return;
}
else {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
}
// 4-对弃权票的处理
checkAllowIfAllAbstainDecisions();
}
UnanimousBased(一票否决)
如果没有反对票,但是有赞成票,则表示通过。
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) throws AccessDeniedException {
int grant = 0;
List<ConfigAttribute> singleAttributeList = new ArrayList<>(1);
singleAttributeList.add(null);
for (ConfigAttribute attribute : attributes) {
singleAttributeList.set(0, attribute);
for (AccessDecisionVoter voter : getDecisionVoters()) {
// 获取投票结果
int result = voter.vote(authentication, object, singleAttributeList);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
grant++;
break;
case AccessDecisionVoter.ACCESS_DENIED: // 有拒绝,直接抛异常
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied",
"Access is denied"));
default:
break;
}
}
}
// To get this far, there were no deny votes
if (grant > 0) { // 没有拒绝票
return;
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
至此,AccessDecisionManager 的三种实现分析完毕。