最近在整合完单点登录后,又来了一个新活,好在这个任务已经比较成熟,实现的方式也比较多,也有比较成熟的框架已经实现了它,它就是权限管理。
实现权限管理的方式有很多,我提出我通过查阅资料学习所知道的,如果有更多欢迎分享:
其中,1、2点都是基于在类或方法上加上一个自定义注解实现的,在通知或拦截器中通过获取类或方法上的注解的value值,来比较判断用户的权限,再决定是否放行执行我们的核心业务。
考虑到公司架构和其他原因,我使用到的是使用AOP进行鉴权认证。
实现过程如下:
RBAC是基于角色的访问控制,是用户通过角色与权限进行关联,为什么不直接给用户分配权限呢?简单来说,一个用户拥有多个角色,每个角色拥有若干权限。这样就构成了“用户-角色-权限”的授权模型。在这个模型中,用户与角色、角色与权限之间是多对多的关系。
根据角色授权的思想,我们需要设计五张表
用户表(user)
角色表(role)
权限表(permission)
这三个表之间都是多对多的关系,所以衍生出两个关联表如下
用户角色表(user_role)
角色资源表(role_permission)
1.定义注解
@Retention(value = RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Inherited
@Documented
public @interface PreAuthorize {
String value();
}
注解的value值代表的是方法所需的条件或权限
2.AuthorizeAspect切面定义
@Aspect
@Component
public class AuthorizeAspect {
@Autowired
private ZsjScadaUserService userService;
@Autowired
private ZsjScadaUserRoleService userRoleService;
@Autowired
private ZsjScadaRolePermissionService rolePermissionService;
@Autowired
private ZsjScadaPermissionService permissionService;
//定义切点
@Pointcut("@annotation(com.metastar.vip.scada.service.annotation.PreAuthorize)")
public void logPointCut(){
}
//鉴权通知
//环绕通知选择原因:取消方法执行:在环绕通知中,我们可以选择不执行目标方法(JointPoint),从而取消方法的执行。这在鉴权过程中非常有用,如果用户没有相应的权限,我们可以直接返回错误信息,而不必执行目标方法。
@Around("logPointCut()")
public Object authAround(ProceedingJoinPoint joinPoint) throws Throwable{
// TODO: 2023/8/31 获取目标方法中的HttpRequest -> 获取请求头携带的Cookie
Object[] args = joinPoint.getArgs();
String name = joinPoint.getSignature().getName();
HttpServletRequest request = (HttpServletRequest) args[0];
Object proceed = null;
HttpSession session = request.getSession(false);
if (session == null){
return proceed = Result.fail("请先登录");
}
// TODO: 2023/8/31 根据cookie获取当前会话信息 -> 获取当前登录用户
Assertion assertion = (Assertion) session.getAttribute("_const_cas_assertion_");
Map attributes = assertion.getPrincipal().getAttributes();
String user = attributes.get("USER").toString();
JSONObject userJsonObject = JSON.parseObject(user);
String userId = userJsonObject.getString("userId");
// TODO: 2023/8/31 根据用户ID查询该用户角色
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id",userId).eq("in_use",1);
UserDO userDO = userService.getOne(queryWrapper);
if (user == null){
throw new RuntimeException("该用户不存在");
}
// TODO: 2023/8/31 根据该角色去查询所拥有的权限
QueryWrapper userRoleDOQueryWrapper = new QueryWrapper<>();
userRoleDOQueryWrapper.eq("user_id",userDO.getId());
UserRoleDO one = userRoleService.getOne(userRoleDOQueryWrapper);
Long roleId = one.getRoleId();
QueryWrapper rolePermissionDOQueryWrapper = new QueryWrapper<>();
rolePermissionDOQueryWrapper.eq("role_id",roleId);
List permissionIds = rolePermissionService.list(rolePermissionDOQueryWrapper).stream().map(RolePermissionDO::getPermissionId).collect(Collectors.toList());
// TODO: 2023/8/31 查看该权限所能访问的资源uri
// TODO: 2023/8/31 查看当前目标类+方法的URI是否存在与当前用户权限所对应的资源uri列表中
// TODO: 2023/8/31 存在->继续指定控制器中的方法 不存在->返回错误信息
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Object target = joinPoint.getTarget();
//获取注解标注的类(鉴权以类为单位)
// PreAuthorize annotation1 = target.getClass().getAnnotation(PreAuthorize.class);
// String value = annotation1.value();
//获取注解标注的方法(鉴权以方法为单位)
Method method = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
//通过方法获取注解
PreAuthorize annotation = method.getAnnotation(PreAuthorize.class);
//获取注解中的值
String permissionValue = annotation.value();
Integer count = 0;
for (Long permissionId : permissionIds) {
String permissionName = permissionService.getById(permissionId).getPermissionName();
if (permissionName.equals(permissionValue)){
//该角色含有该权限 访问方法
proceed = joinPoint.proceed(args);
break;
}
count++;
}
if (count == permissionIds.size()){
//没有该权限 抛出异常
return proceed = Result.fail("权限不足,无法访问");
}
return proceed;
}
}
3.测试
@RestController
@RequestMapping("/auth")
public class authTestController {
@PostMapping("/test1")
@PreAuthorize(value = "用户模块")
public Result Test1(HttpServletRequest request,String userName){
return Result.ok(userName);
}
}
Postman访问该url后,拦截后执行环绕通知进行鉴权,通过后访问该控制器中的Test1方法响应结果。
1.同样是定义自定义注解
@Target({ElementType.TYPE, ElementType.METHOD}) //注解的作用域,即此注解应该被用在什么地方
@Retention(RetentionPolicy.RUNTIME) //注解的生命周期,即注解在什么范围内有效
@Inherited //标识注解,允许子类继承
@Documented //标识注解,生成javadoc文档时,会包含此注解
public @interface RoleNum {
// int value(); //default 1;
String value();
}
2.定义拦截器
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 目标方法执行之前(Controller方法调用之前)
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// TODO: 2023/7/21 获取前端Authorization的token值 访问redis
// TODO: 2023/7/21 命中 放行 刷新redis中token的TTL
// TODO: 2023/7/21 未命中(过期) 重新向登录页面 拦截
// TODO: 2023/7/24 在验证到相应登陆的Token后 需要查看当前用户Role值 检验是否有该路径的访问权限
String token = request.getHeader("Authorization");
Long size = stringRedisTemplate.opsForHash().size(RedisConstant.LOGIN_INFO + token);
if (size == 0){
//重定向到登录页面
// response.sendRedirect("/login");
//拦截
response.getWriter().write("{\"code\":-1,\"msg\":\"please login first\",\"data\":null}");
return false;
}else {
String role = (String) stringRedisTemplate.opsForHash().get(RedisConstant.LOGIN_INFO + token, "role");
if (hasPermission(handler,role)) {
//权限足够
//放行 并 刷新 用户信息token TTL
stringRedisTemplate.expire(RedisConstant.LOGIN_INFO + token,RedisConstant.LOGIN_TTL, TimeUnit.MINUTES);
return true;
} else {
//权限不足
response.getWriter().write("{\"code\":-1,\"msg\":\"privilege is not enough\",\"data\":null}");
//放行 并 刷新 用户信息token TTL
stringRedisTemplate.expire(RedisConstant.LOGIN_INFO + token,RedisConstant.LOGIN_TTL, TimeUnit.MINUTES);
return false;
}
}
}
/**
* 验证权限是否足够
* @param handler
* @param role
* @return
*/
private boolean hasPermission(Object handler, String role) {
if (handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取方法上的注解
RoleNum roleNum = handlerMethod.getMethod().getAnnotation(RoleNum.class);
//如果方法上的注解为空 则获取类的注解
if (roleNum == null){
roleNum = handlerMethod.getMethod().getDeclaringClass().getAnnotation(RoleNum.class);
}
//如果标记了注解,则判断权限
if (roleNum != null){
if (roleNum.value().equals("2")){ //该通用身份即可访问
// System.err.println("权限足够,正在访问");
return true;
}
if (roleNum.value().equals("1")) { //Normal 0
if (role.equals("1")){
return true;
}else if (role.equals("0")){
return true;
}
}
if (roleNum.value().equals("0")){
if (role.equals("0")){
return true;
}else {
return false;
}
}
}
}
return false;
}
注意:上述用户的角色权限信息是存储在Redis中的。
现在鉴权模块已经非常成熟,以上仅为个人心得和实践,还有很多改进的地方欢迎评论,后续还会补充通过Shiro框架实现的鉴权这一模块。