Loading... # 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过期或无效 最后修改:2021 年 01 月 22 日 08 : 55 AM © 允许规范转载 赞赏 如果觉得我的文章对你有用,请随意赞赏 ×Close 赞赏作者 扫一扫支付 支付宝支付 微信支付