之前的SpringSecurity登录验证步骤和源码浅析已经完成了登录部门,接下来结合自定义权限校验。在本文的最后说了一个前后端分离,每次请求服务端securityContext失效的问题。
在springSecurity中
自定义权限校验意思就是,用保存在数据库中的权限来设置url的请求权限。
先看看configure文件
@SpringBootConfiguration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailHandler myAuthenticationFailHandler;
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
@Bean
UserDetailsService customUserService() {
return new MyCustomUserService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义UserDetailsService
auth.userDetailsService(customUserService()).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginProcessingUrl("/user/login")
// 自定义的登录验证成功或失败后的去向
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailHandler)
// 每个子匹配器将会按照声明的顺序起作用(不会拦截/user/login登录请求)
.and().authorizeRequests()
// PreflightRequest请求不做拦截(OPTIONS)
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
// 禁用csrf防御机制(跨域请求伪造),这么做在测试和开发会比较方便。
.and().csrf().disable();
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
}
看上去可能有些复杂,其内容涉及到自定义登录校验,参考博客SpringSecurity前后端分离下对登录认证的管理
先大致说说,原来的springSercurity是怎么对权限进行校验的
主要就是这几个类
FilterSecurityInterceptor(在进行下一步之间进行权限校验) --> AbstractSecurityInterceptor (获取请求该url所需要的权限和用户所拥有的权限)--> AffirmativeBased(判断用户是否拥有请求该url权限,一种判别机制) --> WebExpressionVoter(这里才正式判断权限)
大家可以根据这个打上断点debug一遍逻辑就更清晰了。
FilterSecurityInterceptor就不说了主要就是做个跳转。
下面是AbstractSecurityInterceptor核心源码
this.obtainSecurityMetadataSource().getAttributes(object);该方法是由DefaultFilterInvocationSecurityMetadataSource类提供的,获取的是configure里面配置的访问该url所需要的权限,
Authentication authenticated = this.authenticateIfRequired();就是从SecurityContextHolder中获取SecurityContext(SecurityContext保存的就是用户拥有的所有权限,这里有个坑,在本文的最后会在说的)。
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
boolean debug = this.logger.isDebugEnabled();
if (!this.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: " + this.getSecureObjectClass());
} else {
// 表示的是获取访问当前url所需要的权限,如果当前url并没设置权限,并且在前面的configure配置了.anyRequest().authenticated(),则获取的就是authenticated,即只需要登录验证即可。否则就为null了。
Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object);
if (attributes != null && !attributes.isEmpty()) {
if (debug) {
this.logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);
}
// 获取当前用户所拥有的权限(如果未登录或者没有给该用户绑定角色,这里获取的就是anomyousUser)
Authentication authenticated = this.authenticateIfRequired();
try {
// 权限检验
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException var7) {
this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7));
throw var7;
}
if (debug) {
this.logger.debug("Authorization successful");
}
if (this.publishAuthorizationSuccess) {
this.publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
if (runAs == null) {
if (debug) {
this.logger.debug("RunAsManager did not change Authentication object");
}
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
} else {
if (debug) {
this.logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
} else if (this.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'");
} else {
if (debug) {
this.logger.debug("Public object - authentication not attempted");
}
this.publishEvent(new PublicInvocationEvent(object));
return null;
}
}
}
接着就到AffirmativeBased判定机制,springsecurity提供的判定机制有三个,默认是AffirmativeBased。
AffirmativeBased :至少一个投票者必须决定授予访问权限
ConsensusBased :多数投票者必须授予访问权限
UnanimousBased :所有投票者都必须投票或放弃投票授予访问权限(无投票表决拒绝访问)
AffirmativeBased和接下来的WebExpressionVoter就不说明了,在springsecurity3.x权限判断默认使用的是RoleVoter,4.x之后就是用了WebExpressionVoter,校验原理有点复杂(实在看不懂)。就不说明了。
下面到我们自己动手实现自己的权限校验了。这么做主要就是可以在数据库中修改权限并能及时应用了,而不需要在configure重新写代码然后在重启项目。
先看下configure配置
@SpringBootConfiguration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailHandler myAuthenticationFailHandler;
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
@Bean
UserDetailsService customUserService() {
return new MyCustomUserService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义UserDetailsService
auth.userDetailsService(customUserService()).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginProcessingUrl("/user/login")
// 自定义的登录验证成功或失败后的去向
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailHandler)
// 每个子匹配器将会按照声明的顺序起作用(不会拦截/user/login登录请求)
.and().authorizeRequests()
// PreflightRequest请求不做拦截(OPTIONS)
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
// 其他请求,只需要用户被验证(就是已经登录即可)
//.anyRequest().authenticated()
// 禁用csrf防御机制(跨域请求伪造),这么做在测试和开发会比较方便。
.and().csrf().disable();
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
}
根据这个流程
FilterSecurityInterceptor(在进行下一步之间进行权限校验) –> AbstractSecurityInterceptor (获取请求该url所需要的权限和用户所拥有的权限)–> AffirmativeBased(判断用户是否拥有请求该url权限,一种判别机制) –> WebExpressionVoter(这里才正式判断权限)
要想实现自定义,我们就需要重写自己的类替代AffirmativeBased和WebExpressionVoter还有DefaultFilterInvocationSecurityMetadataSource。
怎么做呢?
先配置一个自定义MyFilterSecurityInterceptor,同时在configure加上前面所示的代码。配置这个类有什么用呢,这个类本身功能并不大只是做个跳转。所以自定实现的代码也很简单,但是如果没有这个自定义配置类后面的自定义权限判断等类就不会起作用。这里有个细节在configure配置中我们是这么添加MyFilterSecurityInterceptor的
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
这么做并不让FilterSecurityInterceptor失去作用,这样做意思是让MyFilterSecurityInterceptor先执行(包括接下来的自定义类),
再到FilterSecurityInterceptor有个前提条件,就是处理自定义MyFilterSecurityInterceptor类的时候出现异常,如权限不匹配等,就不会回到FilterSecurityInterceptor。否则还是会回到FilterSecurityInterceptor然后接下来的默认类(这个时候就有个问题了,如果是这样的话,会不会破坏我们自己定的权限校验功能),其实并不会,接下来的FilterSecurityInterceptor会对权限都允许为permitAll(前提在configure并没有添加任何的权限控制).
/**
* 资源管理拦截器AbstractSecurityInterceptor
*/
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
/**
* 配置文件注入
*/
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
/**
* 登陆后,每次访问资源都通过这个拦截器拦截
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation filterInvocation = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(filterInvocation);
}
/**
* @param filterInvocation 里面有一个被拦截的url
*/
private void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
// 里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取filterInvocation对应的所有权限
// 再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken statusToken = super.beforeInvocation(filterInvocation);
try {
// 执行下一个拦截器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.afterInvocation(statusToken, null);
SecurityContextHolder.clearContext();
}
}
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public Class getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
这个配置完成之后就是自定义MyInvocationSecurityMetadataSource类了。这个类只需要加上注解@Component就行,不需要在configure配置。这样springSecurity默认使用的DefaultFilterInvocationSecurityMetadataSource类就失去作用了。
/**
* 读取访问改url资源所需要的权限。
*/
@Component
public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private SysAclMapper sysAclMapper;
private HashMap> map = null;
/**
* 加载权限表中所有权限
*/
private void loadResourceDefine() {
map = Maps.newHashMap();
List sysAclList = sysAclMapper.selectAllAcl();
for(SysAcl sysAcl : sysAclList) {
List configAttributeList = new ArrayList<>();
ConfigAttribute configAttribute = new SecurityConfig(sysAcl.getName());
configAttributeList.add(configAttribute);
map.put(sysAcl.getUrl(), configAttributeList);
}
}
/**
* 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,
* 则返回给 MyAccessDecisionManager的decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
* @param o 中包含用户请求的request 信息
*/
@Override
public Collection getAttributes(Object o) throws IllegalArgumentException {
if(map == null) {
loadResourceDefine();
}
HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
for(Map.Entry> entry : map.entrySet()) {
String resUrl = entry.getKey();
AntPathRequestMatcher matcher = new AntPathRequestMatcher(resUrl);
if(matcher.matches(request)) {
return map.get(resUrl);
}
}
return null;
}
@Override
public Collection getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class aClass) {
return true;
}
}
再着就是MyAccessDecisionManager该自定类直接把AffirmativeBased和WebExpressionVoter两个类所拥有的功能直接替代了。
/**
* 执行顺序
* MyFilterSecurityInterceptor -> MyInvocationSecurityMetadataSource -> MyAccessDecisionManager
*
* 控制访问权限
*/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
/**
* 方法是判定是否拥有权限的决策方法,
* @param authentication 是释CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
* @param o 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
* @param collection 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
*/
@Override
public void decide(Authentication authentication, Object o, Collection collection) throws AccessDeniedException, InsufficientAuthenticationException {
if(CollectionUtils.isEmpty(collection)) {
return;
}
// 权限判断
for (ConfigAttribute configuration : collection) {
String needAcl = configuration.getAttribute();
for (GrantedAuthority authority : authentication.getAuthorities()) {
if (needAcl.equals(authority.getAuthority())) {
return;
}
}
}
// 木有权限就抛出异常
throw new CustomException("没有访问权限, 如需访问请联系管理员");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class aClass) {
return true;
}
}
最后再说下一个细节,如果是前后端分离的项目,因为每一次的ajax都会让服务端的session或者cookie失效。因为服务端的SecurityContext使用的是session保存,并且在每次请求都把SecurityContext放入到SecurityContextHolder中。所以每次请求都会导致其失效,我们该怎么做呢。自定义securityContext并把它放入SecurityContextHolder
只需要在把自定义MyFilterSecurityInterceptor改成如下
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
/**
* 配置文件注入
*/
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
/**
* 登陆后,每次访问资源都通过这个拦截器拦截
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation filterInvocation = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(filterInvocation);
}
/**
* @param filterInvocation 里面有一个被拦截的url
*/
private void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
// 这里重新自定义SecurityContext并把它放入到SecurityContextHolder
List authorityList = new ArrayList<>();
// 这里数据应该是从数据库中读取,我就直接模拟假数据了
authorityList.add(new SimpleGrantedAuthority("角色列表"));
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("18389667224", "admin", authorityList);
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(token);
SecurityContextHolder.setContext(securityContext);
InterceptorStatusToken statusToken = super.beforeInvocation(filterInvocation);
try {
// 执行下一个拦截器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.afterInvocation(statusToken, null);
// 清理Holder,(其实后面security也会帮我们清理)
SecurityContextHolder.clearContext();
}
}
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public Class getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
以上就是自定义权限判断的分析了,(能力有限,感觉很多地方都说不太明白,如果有发现错误,请指正),如果觉得本文对你有用,请麻烦随手给个赞~