权限校验相关原理在原文已经分析的非常透彻,本文仅作转载,方便查阅。
原文链接:
一、源码解析
1、权限校验涉及的相关类图
2、权限校验时序图:
3.在权限校验过程中,几个比较关键的类:
- UsernamePasswordAuthenticationFilter
- AnonymousAuthenticationFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
调用流程如下: - AbstractAuthenticationProcessingFilter.doFilter()
- UsernamePasswordAuthenticationFilter.attemptAuthentication()
- AnonymousAuthenticationFilter.doFilter():AnonymousAuthenticationFilter过滤器是在UsernamePasswordAuthenticationFilter等过滤器之后,如果它前面的过滤器都没有认证成功,Spring Security则为当前的SecurityContextHolder中添加一个Authenticaiton 的匿名实现类AnonymousAuthenticationToken。
- FilterSecurityInterceptor.doFilter():此过滤器为认证授权过滤器链中最后一个过滤器,该过滤器之后就是请求真正的请求服务。
- ExceptionTranslationFilter.doFilter():ExceptionTranslationFilter异常处理过滤器,该过滤器用来处理在系统认证授权过程中抛出的异常(也就是下一个过滤FilterSecurityInterceptor),主要是处理 AuthenticationException 和 AccessDeniedException。
其中,重点过滤器就是 FilterSecurityInterceptor ,也是接下来我们在权限校验过程中接触最多的类。
二、FilterSecurityInterceptor 核心类
- FilterSecurityInterceptor.class
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
if (fi.getRequest() != null && this.observeOncePerRequest) {
fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
#1. before invocation重要
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
#2. 可以理解开始请求真正的 /persons 服务
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.finallyInvocation(token);
}
#3. after Invocation
super.afterInvocation(token, (Object)null);
}
}
1.before invocation重要
2.请求真正的 /persons 服务
3.after Invocation
三个部分中,最重要的是 1,该过程中会调用 AccessDecisionManager 来验证当前已认证成功的用户是否有权限访问该资源
- AbstractSecurityInterceptor#beforeInvocation()
protected InterceptorStatusToken beforeInvocation(Object object) {
.....
#1.重点(获取当前请求路径应具备的权限)
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
.....
}
Authentication authenticated = this.authenticateIfRequired();
try {
#2.重点(将当前认证对象进行权限验证)
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException var7) {
.....
}
......
if (this.publishAuthorizationSuccess) {
this.publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
......
}
1.重点(获取当前请求路径应具备的权限)
2.重点(将当前认证对象进行权限验证)
这两部分都需要自定义实现,参见下方:
1.获取当前请求路径应具备的权限
要想获取当前请求路径应具备的权限,那么就涉及到一个权限资源类 SecurityMetadataSource:
要实现动态的权限验证,当然要先有对应的访问权限资源了。Spring Security是通过SecurityMetadataSource来加载访问时所需要的具体权限,所以第一步需要实现SecurityMetadataSource。
SecurityMetadataSource是一个接口,同时还有一个接口FilterInvocationSecurityMetadataSource继承于它,但FilterInvocationSecurityMetadataSource只是一个标识接口,对应于FilterInvocation,本身并无任何内容:
public interface FilterInvocationSecurityMetadataSource extends SecurityMetadataSource {
}
因为我们做的一般都是web项目,所以实际需要实现的接口是FilterInvocationSecurityMetadataSource,这是因为Spring Security中很多web才使用的类参数类型都是FilterInvocationSecurityMetadataSource。案例,返回当前请求在数据库中对应的权限资源集合:
/**
* Created by sang on 2017/12/28.
* 返回权限资源
*/
@Component
public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* getAttributes方法返回本次访问需要的权限,可以有多个权限。在上面的实现中如果没有匹配的url直接返回
* ROLE_LOGIN(登录),也就是没有配置权限的url默认都需要登录。
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) {
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> allMenu = menuService.getAllMenu();
for (Menu menu : allMenu) {
if (antPathMatcher.match(menu.getUrl(), requestUrl)
&&menu.getRoles().size()>0) {
List<Role> roles = menu.getRoles();
int size = roles.size();
String[] values = new String[size];
for (int i = 0; i < size; i++) {
values[i] = roles.get(i).getName();
}
return SecurityConfig.createList(values);
}
}
//没有匹配上的资源,都是登录访问
return SecurityConfig.createList("ROLE_LOGIN");
}
/**
* getAllConfigAttributes方法如果返回了所有定义的权限资源,Spring Security会在启动时校验每个ConfigAttribute
* 是否配置正确,不需要校验直接返回null。
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
*/
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
2.将当前认证对象进行权限验证
有了权限资源,知道了当前访问的url需要的具体权限,接下来就是决策当前的访问是否能通过权限验证了。
需要通过实现自定义的AccessDecisionManager来实现。Spring Security内置的几个AccessDecisionManager就不讲了,在web项目中基本用不到,在项目中一般是自定义实现AccessDecisionManager,案例如下:
/**
* 权限校验
*/
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
/**
*
* @param auth authentication包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时UserDetailsService中
* 设置的authorities。
* @param o object就是FilterInvocation对象,可以得到request等web资源。
* @param cas 是本次访问需要的权限(也就是我们自定义 CustomMetadataSource#getAttributes() 中返回的权限集合)
*/
@Override
public void decide(Authentication auth, Object o, Collection<ConfigAttribute> cas){
Iterator<ConfigAttribute> iterator = cas.iterator();
while (iterator.hasNext()) {
ConfigAttribute ca = iterator.next();
//当前请求需要的权限
String needRole = ca.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (auth instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("未登录");
} else {
return;
}
}
//当前用户所具有的权限
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足!");
}
/**
* supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
*/
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
/**
* supports方法返回类对象是否支持校验,web项目一般使用FilterInvocation来判断,或者直接返回true。
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
上面的实现中,当需要多个权限时只要有一个符合则校验通过,即或的关系,想要并的关系只需要修改这里的逻辑即可。
3.配置使用上方自定义实现类
上面权限的资源和验证我们已经都实现了,接下来就是指定让Spring Security使用我们自定义的实现类了。
在Spring Boot中提供了ObjectPostProcessor以让用户实现更多想要的高级配置。具体看下面代码,注意withObjectPostProcessor部分:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(metadataSource);
o.setAccessDecisionManager(urlAccessDecisionManager);
return o;
}
})
.and()
.formLogin().loginPage("/login_p").loginProcessingUrl("/login")
.usernameParameter("username").passwordParameter("password")
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req,
HttpServletResponse resp,
AuthenticationException e) throws IOException {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = null;
if (e instanceof BadCredentialsException ||
e instanceof UsernameNotFoundException) {
respBean = RespBean.error("账户名或者密码输入错误!");
} else if (e instanceof LockedException) {
respBean = RespBean.error("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
respBean = RespBean.error("密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
respBean = RespBean.error("账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
respBean = RespBean.error("账户被禁用,请联系管理员!");
} else {
respBean = RespBean.error("登录失败!");
}
resp.setStatus(401);
ObjectMapper om = new ObjectMapper();
PrintWriter out = resp.getWriter();
out.write(om.writeValueAsString(respBean));
out.flush();
out.close();
}
})
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req,
HttpServletResponse resp,
Authentication auth) throws IOException {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.ok("登录成功!", HrUtils.getCurrentHr());
ObjectMapper om = new ObjectMapper();
PrintWriter out = resp.getWriter();
out.write(om.writeValueAsString(respBean));
out.flush();
out.close();
}
})
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.ok("注销成功!");
ObjectMapper om = new ObjectMapper();
PrintWriter out = resp.getWriter();
out.write(om.writeValueAsString(respBean));
out.flush();
out.close();
}
})
.permitAll()
.and().csrf().disable()
.exceptionHandling().accessDeniedHandler(deniedHandler);
}
主要是在创建默认的FilterSecurityInterceptor的时候把我们的accessDecisionManager和securityMetadataSource设置进去。