自定义权限校验

最近写一个小应用,有涉及到权限控制的,不想引入SpringSecurity等权限框架,感觉用框架太重了,于是自己用拦截器简单实现了下。思路就是自定义一个注解,标注在需要权限控制Contoller的方法上,该注解有一个roles属性,表示接口需要的角色,定义一个拦截器,拦截每个请求,根据请求头携带的Token查询用户信息,判断用户角色中是否有注解声明的某个角色,如果有权限就放行,并将用户信息保存在ThreadLcoal中以便后续使用,没有就拦截抛异常。

数据库表结构

登录

因为会拦截所有请求,从请求头中获取TokenID查询用户信息,而这个TokenID就是登录成功时返回给前端,由前端保存,在后续发起请求时,放在请求头中传给后端。在当前这个应用中,是小程序登录,根据前端传的code加上appid、appsecret,请求微信登录接口返回openId,确认是系统用户,就生成一个tokenId存在数据库中,并返回给前端。

自定义注解

使用注解可以很方便用来标识接口是否需要权限,需要哪些权限,类似于SpringSecurity的@PreAuthorize注解。自定义注解代码如下

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface HasAnyRole {

    Role[] value();
}

public enum Role {
    ADMIN, NORMAL
}

注解只能标注在方法上,并且有一个属性用来表示接口需要的权限,Role是一个枚举,表示系统中所有的角色。

保存用户信息到ThreadLocal

登录成功后为了在任何地方都能获取到当前登录用户信息,使用ThreadLocal保存登录用户的信息,并封装一个用户相关的静态工具类

public class UserInfoContextHolder {

    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    public static void set(User user) {
        userThreadLocal.set(user);
    }

    public static User get() {
        return userThreadLocal.get();
    }


    public static void remove() {
        userThreadLocal.remove();
    }
}
public class CurrentUserUtils {

    private CurrentUserUtils () {

    }

    public static String currentUserId() {
        User user = UserInfoContextHolder.get();
        return user != null ? user.getId() : null;
    }
}

拦截器

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private Isolator isolator;

    @Autowired
    private TokenMapper tokenMapper;

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) throws Exception {
        //虽然线程销毁ThreadLocal里面的信息会删除,但是使用线程池可能会导致线程还在,这里请求结束手动删除ThreadLocal中的信息,以防出现问题
        UserInfoContextHolder.remove();
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{

        log.info("this is login interceptor");
        if(handler instanceof HandlerMethod){
            HasAnyRole needAuthority = ((HandlerMethod) handler).getMethodAnnotation(HasAnyRole.class);
            if (needAuthority == null || needAuthority.value().length == 0) {
                return true;
            }
            String authorization = request.getHeader("Authorization");
            if (StringUtils.isEmpty(authorization) || !authorization.startsWith("Token ")) {
                log.info("current user not login");
                response.sendError(401, "请先登录");
                return false;
            }

            String tokenId = authorization.split(" ")[1];
            Token token = isolator.getTokenByTokenId(tokenId);
            if (token == null) {
                log.info("token is null");
                response.sendError(401, "Token为空, 请登录");
                return false;
            }

            if (token.isExpire()) {
                log.info("token is expire");
                tokenMapper.deleteById(tokenId);
                response.sendError(401, "Token过期, 请重新登录");
                return false;
            }

            User user = isolator.findUserByUserId(token.getUserId());
            if (user == null) {
                log.info("user is null");
                response.sendError(401, "用户为空");
                return false;
            }

            List<Role> roles = user.getRoles();
            if(CollectionUtils.isEmpty(roles)) {
                log.info("user has empty roles");
                response.sendError(401, "用户没有角色");
                return false;
            }

            boolean res = Arrays.stream(needAuthority.value()).anyMatch(roles::contains);
            if (res) {
                UserInfoContextHolder.set(user);
                tokenMapper.extend(tokenId);
                return true;
            }
            log.info("current user has no role to visit current interface");
            response.sendError(403, "暂无权限访问");
            return false;

        }
        log.info("handler type is not HandlerMethod");
        return false;
    }
}

实现HandlerInterceptor接口,重写preHandle方法,Isolator属性是根据tokenID来Mock Token的,在跑测试的时候是直接获取所有角色。tokenMapper就是操作token表的。

逻辑就是和文章开头说的一样,从request对象拿tokenId,再获取用户的角色,获取方法标注的自定义注解,拿到里面声明的角色列表,判断用户拥有的角色是否有一个在注解声明的角色列表中,如果有就通过校验,否则返回403。如果接口需要权限但是请求头中没有带tokenId说明没有登录,返回401。

拦截器还需要配置,设置拦截规则。

@Configuration
public class WebConfigurer implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(loginInterceptor)
            .addPathPatterns("/**");
    }
}

使用

在需要角色校验的地方加上自定义注解即可

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService iUserService;

    @PostMapping("/login")
    public UserLoginVO login(@RequestBody UserLoginCommand command) {
        String tokenId = iUserService.login(command.getCode());
        return UserLoginVO.from(tokenId);
    }

    @GetMapping("/test")
    @HasAnyRole({Role.NORMAL, Role.ADMIN})
    public String test() {
        String userId = CurrentUserUtils.currentUserId();
        return "111";
    }
}

一般小应用可以就使用简单的方式,如果稍微复杂点的应用,权限控制可以抽出来,放到网关中进行处理,可以再配合Redis和Jwt进行优化。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AE86Jag

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值