鉴权主要分为身份认证和权限控制两部分:
身份认证:检查当前用户是否合法(比如已登录)
权限控制:检查当前用户是否有访问该资源的权限
本文主要给出一个示例,说明如何自定义权限控制。
因为一个完整的示例代码,比较多,我在前面的几篇文章都有相关示例:
spring boot security快速使用示例
spring boot security之前后端分离配置
spring boot security自定义认证
spring boot security验证码登录示例
spring boot security使用jwt认证
所以本文主要给出权限检查相关的示例代码。
在前面几篇文章里,都给了完整示例,照搬就能用,这里只粘贴出部分代码,可能让人迷糊,所以给出一些源码说明。
在启用spring security后,我们启动应用应该能看到下面这行日志:
2023-07-14 13:36:41.306 INFO 9584 — [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@6af5bbd0, org.springframework.security.web.context.SecurityContextPersistenceFilter@3204e238, org.springframework.security.web.header.HeaderWriterFilter@20b9d5d5, org.springframework.web.filter.CorsFilter@76464795, org.springframework.security.web.authentication.logout.LogoutFilter@4821aa9f, com.xxd.security.demo.security.JwtRequestFilter@129bd55d, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@35a0e495, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@a5272be, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@53cf9c99, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@b34832b, org.springframework.security.web.session.SessionManagementFilter@291373d3, org.springframework.security.web.access.ExceptionTranslationFilter@4228bf58, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@10f7918f]
注意最后一个过滤器:org.springframework.security.web.access.intercept.FilterSecurityInterceptor
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
...
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
...
}
如上:在doFilter方法调用的invoke方法内调用了super.beforeInvocation()方法。
super.beforeInvocation()方法内调用认证检查和权限检查的方法,可以自己找到这个方法里去看,我们重点看权限检查:
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
Authentication authenticated) {
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException ex) {
...
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
throw ex;
}
}
主要就是:this.accessDecisionManager.decide(authenticated, object, attributes);这个方法。
accessDecisionManager,顾名思义是访问决策管理,构造方法需要传递一些投票器,在决策的时候根据投票器的投票结果来返回最终结果,默认主要有3个实现:
投票器的结果有3种:同意、拒绝、弃权。
不是说授权么,怎么突然引入投票的概念,是不是很懵???这里的实现就是这样。
所以我们自定义授权,就是来定义投票器的实现。
@Slf4j
@Component
public class AuthorityDecisionVoter implements AccessDecisionVoter<FilterInvocation> {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final Set<String> ANON_URL_SET = new HashSet<>();
public AuthorityDecisionVoter() {
ANON_URL_SET.add("/login/**");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {
// 拿到当前请求uri
String requestUrl = object.getRequestUrl();
String method = object.getRequest().getMethod();
for (String anonUrl : ANON_URL_SET) {
if (pathMatcher.match(anonUrl, requestUrl)) {
return ACCESS_ABSTAIN;
}
}
List<String> permList = Arrays.asList(requestUrl, method + ":" + requestUrl);
// 拿到当前用户所具有的权限.不处理了,这里是做个示例,因为每个人的场景或处理不一样,有的可能这里返回的是角色信息
Collection<? extends GrantedAuthority> permissions = ((UserDetails) authentication.getPrincipal()).getAuthorities();
// 示例代码,写死了,/hello/world 请求通过,其它都拒绝
for (String perm : permList) {
if (perm.equalsIgnoreCase("get:/hello/world")) {
return ACCESS_GRANTED;
}
}
return ACCESS_DENIED;
}
}
前面说过了,accessDecisionManager是决策投票器的投票结果的。
@Configuration
public class WebConfiguration {
@Bean
public AccessDecisionManager accessDecisionManager(ApplicationContext applicationContext,
AccessDecisionVoter authorityDecisionVoter) {
DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
// spel 表达式中解析bean使用的,如果用到了必须设置这个
expressionHandler.setApplicationContext(applicationContext);
WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
webExpressionVoter.setExpressionHandler(expressionHandler);
// 默认是只有webExpressionVoter这个投票器的,解析spel表达式,authorityDecisionVoter是我们自定义的,注入进来,解析的时候,两个投票器都会投票的
List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(webExpressionVoter, authorityDecisionVoter);
// 这个决策器表示,上面的这两个投票器如果有一个反对票,就抛出访问拒绝的异常
UnanimousBased unanimousBased = new UnanimousBased(decisionVoters);
return unanimousBased;
}
}
注意下,我上面针对spel注释的说明。如果没有用到spel表达式,WebExpressionVoter (这个是默认的投票器)可以直接new一个出来。
演示一个spel的使用,这个不是必须的啊,这里演示下,用户认证的时候,想要打印一条日志:
@Slf4j
@Component
public class AuthenticationHandler {
public boolean hasAuthenticated(HttpServletRequest request, Authentication authentication) {
// 获取主体
Object principal = authentication.getPrincipal();
if (principal instanceof String) {
log.error("匿名用户{}访问", principal);
return false;
}
// 判断主体是否属于 UserDetails
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
return userDetails.isEnabled() && userDetails.isCredentialsNonExpired();
}
return false;
}
}
@Component
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final AccessDecisionManager accessDecisionManager;
public WebSecurityConfigurer(AccessDecisionManager accessDecisionManager) {
this.accessDecisionManager = accessDecisionManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 在这里自定义配置
http.authorizeRequests()
// 登录相关接口都允许访问
.antMatchers("/login/**").permitAll()
.anyRequest()
// spel的使用
.access("@authenticationHandler.hasAuthenticated(request, authentication)")
// .authenticated()
// 自定义权限检查主要是一行代码
.accessDecisionManager(accessDecisionManager)
.and()
...
}
}
基本使用示例就是这样,可以根据需要调整。