最近写一个小应用,有涉及到权限控制的,不想引入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进行优化。