授权原理
在Spring Security权限框架里,若要对后端http接口实现权限受权控制,有两种实现方式。
1、重写 #configure(HttpSecurity http) 方法,主要配置 URL 的权限控制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 配置请求地址的权限
.authorizeRequests()
.antMatchers("/test/echo").permitAll() // 所有用户可访问
.antMatchers("/test/admin").hasRole("ADMIN") // 需要 ADMIN 角色
.antMatchers("/test/normal").access("hasRole('ROLE_NORMAL')") // 需要 NORMAL 角色。
// 任何请求,访问的用户都需要经过认证
.anyRequest().authenticated()
.and()
// 设置 Form 表单登录
//自定义登录页面,可以通过 #loginPage(String loginPage) 设置
.formLogin()
// .loginPage("/login") // 登录 URL 地址
.permitAll() // 所有用户可访问
.and()
// 配置退出相关
.logout()
// .logoutUrl("/logout") // 退出 URL 地址
.permitAll(); // 所有用户可访问
}
2、修改 WebSecurityConfig配置类,增加 @EnableGlobalMethodSecurity 注解,开启对 Spring Security 注解的方法,进行权限验证。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter { }
@RestController
@RequestMapping("/demo")
public class DemoController {
@PermitAll
@GetMapping("/echo")
public String demo() {
return "示例返回";
}
@GetMapping("/home")
public String home() {
return "我是首页";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/admin")
public String admin() {
return "我是管理员";
}
@PreAuthorize("hasRole('ROLE_NORMAL')")
@GetMapping("/normal")
public String normal() {
return "我是普通用户";
}
}
授权流程
在认证过程打印的过滤器链
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- JwtAuthorizationTokenFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
在完成认证过程后,最后需要实现授权,通过FilterSecurityInterceptor完成。
FilterSecurityInterceptor
过滤器链将请求传递转发FilterSecurityInterceptor时,会执行FilterSecurityInterceptor的doFilter方法:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.invoke(new FilterInvocation(request, response, chain));
}
在这段代码当中,FilterInvocation类是一个有意思的存在,其实它的功能很简单,就是将上一个过滤器传递过滤的request,response,chain复制保存到FilterInvocation里,专门供FilterSecurityInterceptor过滤器使用。它的有意思之处在于,是将多个参数统一概括到一个类当中,起到统一管理做用,你想,如果N多个参数,传进来都分散到类的各个地方,参数多了,代码多了,方法过于分散时,可能就很容易形成阅读过程当中,弄糊涂这些个参数都是哪里来了。但若统一概括到一个类里,就能很快定位其来源,方便代码阅读。网上有人提到该FilterInvocation类还起到解耦做用,即避免与其余过滤器使用一样的引用变量。
FilterInvocation
public class FilterInvocation {
private FilterChain chain;
private HttpServletRequest request;
private HttpServletResponse response;
public FilterInvocation(ServletRequest request, ServletResponse response,
FilterChain chain) {
if ((request == null) || (response == null) || (chain == null)) {
throw new IllegalArgumentException("Cannot pass null values to constructor");
}
this.request = (HttpServletRequest) request;
this.response = (HttpServletResponse) response;
this.chain = chain;
}
......
}
FilterSecurityInterceptor的doFilter方法里调用invoke(fi)方法
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
//筛选器已应用于此请求,每一个请求处理一次,因此不需从新进行安全检查
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} else {
// 第一次调用此请求时,需执行安全检查
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
//1.受权具体实现入口
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
//2.受权经过后执行的业务
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.finallyInvocation(token);
}
//3.后续处理
super.afterInvocation(token, (Object)null);
}
}
super.beforeInvocation(filterInvocation)
受权机制实现的入口是super.beforeInvocation(fi),其具体实如今父类AbstractSecurityInterceptor中实现,beforeInvocation(Object object)的实现主要包括如下步骤:
1、获取需访问的接口权限;
2、获取认证经过以后保存在 SecurityContextHolder的用户信息,其中,authorities是一个保存用户所拥有所有权限的集合;
3、尝试受权,用户信息authenticated、请求携带对象信息object、所访问接口的权限信息attributes,传入到decide方法;
主要逻辑块:
1、Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object);
2、Authentication authenticated = this.authenticateIfRequired();
3、this.attemptAuthorization(object, attributes, authenticated);
用户信息获取
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Did not re-authenticate %s before authorizing", authentication));
}
return authentication;
} else {
authentication = this.authenticationManager.authenticate(authentication);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Re-authenticated %s before authorizing", authentication));
}
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
}
在认证过程经过后,执行SecurityContextHolder.getContext().setAuthentication(authentication)将用户信息保存在Security框架当中,以后可经过SecurityContextHolder.getContext().getAuthentication()获取到保存的用户信息;
SecurityContextHolder
认证成功,请求会重新回到UsernamePasswordAuthenticationFilter,然后会通过其父类AbstractAuthenticationProcessingFilter.successfulAuthentication方法将认证对象封装成SecurityContext设置到SecurityContextHolder中
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//认证成功,吧Authentication 设置到SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authResult);
//处理记住我业务逻辑
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//重定向登录成功地址
successHandler.onAuthenticationSuccess(request, response, authResult);
}
尝试受权
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes, Authentication authenticated) {
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException var5) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object, attributes, this.accessDecisionManager));
} else if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
}
this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var5));
throw var5;
}
}
decide()是决策管理器AccessDecisionManager定义的一个方法。
public interface AccessDecisionManager {
void decide(Authentication var1, Object var2, Collection<ConfigAttribute> var3) throws AccessDeniedException, InsufficientAuthenticationException;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
}
AccessDecisionManager是一个interface接口,这是受权体系的核心。FilterSecurityInterceptor 在鉴权时,就是经过调用AccessDecisionManager的decide()方法来进行受权决策,若能经过,则可访问对应的接口。
AccessDecisionManager类的方法具体实现都在子类当中,包含AffirmativeBased、ConsensusBased、UnanimousBased三个子类;
-
AffirmativeBased表示一票经过,这是AccessDecisionManager默认类;
(1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。 Spring security默认使用的是AffirmativeBased。
-
ConsensusBased表示少数服从多数;
(1)如果赞成票多于反对票则表示通过。
(2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。
(3)如果赞成票与反对票相同且不等于0,并且属性 allowIfEqualGrantedDeniedDecisions的值为true,则表 示通过,否则将抛出异常 AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。
(4)如果所 有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值为 true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认 为false
-
UnanimousBased表示一票反对;
(1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出 AccessDeniedException。
(2)如果没有反对票,但是有赞成票,则表示通过。
(3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出 AccessDeniedException。
点进去AffirmativeBased类里,能够看到里面有一行代码int result = voter.vote(authentication, object, configAttributes):
这里的AccessDecisionVoter是一个投票器,用到委托设计模式,即AffirmativeBased类会委托投票器进行选举,而后将选举结果返回赋值给result,而后判断result结果值,若为1,等于ACCESS_GRANTED值时,则表示可一票经过,也就是,容许访问该接口的权限。
这里,ACCESS_GRANTED表示赞成、ACCESS_DENIED表示拒绝、ACCESS_ABSTAIN表示弃权:
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;//表示赞成
int ACCESS_ABSTAIN = 0;//表示弃权
int ACCESS_DENIED = -1;//表示拒绝
......
}