Spring Security 动态URL鉴权遇到的坑

配置类做校验

在Security配置类中可以做校验,将指定路径进行权限配置,如下:

http.csrf().disable()
               ...
                .antMatchers("/auth/login", "/auth/captcha", "/auth/token", "/auth/password").permitAll()
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/druid/**").anonymous()
             ...

以上是配置了登入认证相关的接口能直接访问.swagger,druid及其静态资源能匿名访问

使用注解做校验

Spring Security做权限校验时,使用起来挺方便,可以通过方法上面加注解达到方法级别鉴权.

首先在Spring Security配置类上使用注解@EnableGlobalMethodSecurity(prePostEnabled=true),随后就可以在方法上标注@PreAuthorize("hasAnyRole(xxx)")来进行角色校验,使用@PreAuthorize("hasAnyAuthority(xxx)")进行角色校验.

使用注解的前提还有已经配置好了Spring Security配置类,用户在访问方法时Security已经有了对应的UserDetails信息

动态URL鉴权

但是如果要根据URL做动态鉴权,那么坑就有点多了...

方案1 添加过滤器

最初通过添加一个URI过滤器,由于Spring Security在执行时会经过很多过滤器,然后通过UsernamePasswordAuthenticationFilter这个过滤器时,会对当前用户进行认证以及获取权限信息(没错,就是UserDetails类).这个UserDetails类中实现了很多需要用到的细节,比如账号是否锁定,是否过期,账号密码是否正确,账号对应的权限信息等.认证就是通过UserDetails实例的密码去与用户输入的密码对比,如果相同则认证通过.

而我们的权限信息中,就拥有我们能访问的路径(我们将URI)作为权限代码,我们只需要在这个拦截器中判断用户的权限信息中是否有相应的权限当前访问URI对应,有则过滤器通过,没有则直接返回失败.

UserDetails类 注意重写toString方法,不然死循环

package cool.xxdk.tblog.security;

import cool.xxdk.tblog.entity.sys.User;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

/**
 * Security
 *
 * @author OyLong
 * @date 2021/01/01 21:57
 **/
@AllArgsConstructor
@NoArgsConstructor
public class MyUserDetails extends User implements UserDetails {
    List<GrantedAuthority> authorities;

    public void setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return super.getPassword();
    }

    @Override
    public String getUsername() {
        return super.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return super.getStatus() == 1;
    }

    @Override
    public String toString() {
        return "MyUserDetails{" +
                "authorities=" + authorities +
                '}';
    }
}

这个方案看上去好像没什么问题了.但是由于我们之前还添加了一个用于Token认证的过滤器,两个过滤器结合起来就会出现一些不完美的地方.

  • 输入一个不存在的路径,不会提示404信息,而提示用户没有权限访问
  • 由于Token过滤器在之前执行,为了配合URI过滤器,对Token过滤器进行了部分修改,导致登入时如果带Token会无法登入

尝试过调整两个过滤器的各种顺序,最终还是不完美.

方案2 直接判断

这种方法就更直接了,也是添加一个过滤器(也可以直接写在Token认证之前),但是不同之处在于,这个过滤器需要写在Token验证之前,并且需要自己写个Bean,这个Bean有一个属性就是通过Spring容器拿到的RequestMappingHandlerMapping对象,而所有的URI信息也封装在了这个对象里面,我们将URI信息保存在一个Set里面去,然后通过uriExist方法就能判断URI是否存在了.

Bean继承自ApplicationContextAware是因为需要在注入时拿到ApplicationContext对象

package cool.xxdk.tblog.service.common;

import cn.hutool.core.collection.ConcurrentHashSet;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.Map;

/**
 * 存储所有被映射的uri
 *
 * @author OyLong
 * @date 2021/01/02 00:07
 **/

@Component
public class MyUriService implements ApplicationContextAware {
    ApplicationContext applicationContext;

    private static ConcurrentHashSet<String> uriSet = new ConcurrentHashSet<>();

    public boolean uriExist(String uri) {
        return uriSet.contains(uri);
    }

    public void reload() {
        uriSet.clear();
        RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
        for (RequestMappingInfo rmi : handlerMethods.keySet()) {
            uriSet.addAll(rmi.getPatternsCondition().getPatterns());
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;

        reload();
    }
}

最后实现过滤器的关键代码如下:

 @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if (!myUriService.uriExist(httpServletRequest.getRequestURI())) {
            // 没有相应的路径,直接返回404消息
            ResultUtil.jsonResponse(httpServletResponse, ResultUtil.fail(ResultCode.NOT_FOUND));
            return;
        }
      ...

最终通过SpringSecurity实现的功能如下:

  • 输入不存在的URL会提示404消息
  • 登入时输入Token能正常处理(Token存在也会继续当前登入并返回)
  • 所有权限或者匿名权限都能直接访问
  • 在有效Token的情况下,没有权限的URL会提示没权限,有权限可正常处理
  • 无效Token会提示Token过期或无效
Last modification:February 4, 2021
If you think my article is useful to you, please feel free to appreciate