权限校验
在通用的权限管理中,会出现用户、角色、权限三个表,并且设置大多保存菜单列表给前端,如果将粒度改为按钮级别,表中的数据是很多的,每次查询都要关联到很多数据
虽然三者的关系看似多对多(表设计),其实在真正应用时会发现,一个用户进入系统只会选择一个角色,而一个角色对应的也只有一套权限,从这个大方向上看,三者的关系是单一的
那么要做到细粒度的权限控制,只需要规定好每个接口都需要一个权限值,而每个角色能拥有某些权限值,一个用户绑定一个角色即可
实现的方案就是,可以将其表设计简化,大体用户、角色、权限关系模型依然没有变,但是可以将每个接口规定一个权限值(数字),并直接放在角色表中即可,大大简化了复杂度,session中不在缓存那么多数据(session数据量大也会耗内存)
实现方式
定义一个权限列表注解类
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Auth {
int keyType();
/**
* 登录后的权限
*/
int AUTH_COMMON_LOGIN = 100;
/**
* 测试接口权限
*/
int AUTH_COMMON_TEST = 101;
}
定义拦截器拦截权限
**
* 请求拦截
*/
public class ReqInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.通过session或toten拿到用户角色(ID或名称)
// 2.通过角色在缓存或数据库中拿到角色对应的权限值(字符)
// 3.拿到接口所需要的权限值
HandlerMethod handlerMethod = (HandlerMethod) handler;
Auth auth = handlerMethod.getMethodAnnotation(Auth.class);
// 判断权限值是否包含了接口所需要的值
if (auths.contains(String.valueOf(auth.keyType()))) {
// 有权限放行
return true;
}
// 没有权限不放行并设置一个状态码给前端做判断
response.setStatus(500);
return false;
}
}
使用方式,在接口上加权限注解
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("say")
@Auth(keyType = Auth.AUTH_COMMON_TEST)
public R<String> test(@RequestBody JSONObject params) {
System.out.println(params);
return R.ok("测试成功");
}
}
复杂业务
有这样一个场景,例如公告管理接口,公告存在于不同的版块中,这对于接口端来说就是一个字段的区别,但是在前端,不同的模块放在了不同的地方,并且对于不同的角色看到的模块不一样,如果基于接口来做的权限,也就是这个接口是否有权访问,如果这个接口有权访问,那么所有模块都有权访问,这不符合需求,如果再写一个接口(就是同样的代码),这就是低级程序员干的事,所以现在需要实现的是接口与权限的一对多关系
使用上面的方式做的权限校验可以轻松的实现
调整注解类
int[] keyType();
使用的时候指定多个权限
@Auth(keyType = {Auth.AUTH_COMMON_NOTICE_LIST,Auth.AUTH_COMMON_LOGIN})
调整拦截器的auth.keyType()
,因为此时是个数组
以下内容是二次升级后记录
说明
可实现一下功能:
- 按钮级别权限控制(即对每个接口进行控制)
- 一个接口(如列表)支持有一个权限即可查看
实现方式
定义一个权限列表注解类
import java.lang.annotation.*;
/**
* 自定义注解实现鉴权
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Auth {
String[] keyType();
/**
* 登录后的权限
*/
String COMMON_LOGIN = "COMMON_LOGIN";
/**
* 测试接口权限
*/
String COMMON_TEST = "COMMON_TEST";
}
定义拦截器拦截做权限判断
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gxzn.commonVo.Result;
import com.gxzn.vo.Auth;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* 请求拦截
*/
public class ReqInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 获取请求头的token
String token = request.getHeader("token");
// 这里根据token查询到用户信息和权限列表
List<String> auths = new ArrayList<>();
auths.add("COMMON_LOGIN");
HandlerMethod handlerMethod = (HandlerMethod) handler;
Auth auth = handlerMethod.getMethodAnnotation(Auth.class);
// 判断权限值是否包含了接口所需要的值,有一个即可
if (Objects.nonNull(auth) && Arrays.stream(auth.keyType()).anyMatch(auths::contains)) {
// 有权限放行
return true;
}
// 没有权限不放行并设置一个状态码给前端做判断
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
Result<String> result = new Result<>();
result.setCode(Result.CODE_UNAUTHORIZED);
result.setMsg("未授权");
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
return false;
}
}
注册拦截器,指定哪些接口需要拦截
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器注册
*/
@Configuration
public class Interceptor implements WebMvcConfigurer {
@Bean
ReqInterceptor reqInterceptor() {
return new ReqInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(reqInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
使用方式
/**
* 测试
*/
@RestController
@RequestMapping("/test")
public class TestController {
/**
* 示例
*/
// @Auth(keyType = Auth.COMMON_LOGIN) // 单个权限
@Auth(keyType = {Auth.COMMON_LOGIN, Auth.COMMON_TEST}) // 多个权限有一个即可
@RequestMapping("/demo")
public Result<String> demo(@RequestBody SystemAdminUser systemAdminUser) {
System.out.println(systemAdminUser);
return Result.successMsg("成功");
}
}
信息传递
在拦截器中将用户相关的信息放入Attribute
中,然后使用工具类去获取即可,例如在拦截器中加入数据:
// 将信息加入到请求头,便于controller使用
request.setAttribute("adminType", userCacheVo.getType());
request.setAttribute("adminId", userCacheVo.getId());
使用工具类进行获取:
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* 用户信息工具
*/
public class UserInfoUtil {
/**
* 获取管理员ID
*/
public static String adminUserId() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (Objects.nonNull(requestAttributes)) {
HttpServletRequest request = requestAttributes.getRequest();
System.out.println(request.getAttribute("adminId"));
return (String) request.getAttribute("adminId");
}
return "";
}
/**
* 获取管理员类型
*/
public static Integer adminUserType() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (Objects.nonNull(requestAttributes)) {
HttpServletRequest request = requestAttributes.getRequest();
System.out.println(request.getAttribute("adminId"));
return (Integer) request.getAttribute("adminType");
}
return 0;
}
}