鉴权实现(二)

73 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 10 天,点击查看活动详情

每日英语:

Courage,above all things,is the first quality of a warrior.

勇气高于一切是战士的第一品质。 -卡尔·冯·克劳塞维茨

权限校验

权限初始化

按照上一篇分析流程,我们在程序启动的时候,就需要初始化权限数据,也就是需要程序启动就执行相关代码,SpringBoot可以实现ApplicationRunner 或 CommandLineRunner接口,实现程序初始化启动。

初始化执行操作我们要执行2个操作,分别是角色权限初始化和所有权限初始化,所有权限初始化主要用来校验哪些地址需要被校验,而所有权限地址中不存在的地址那一定是不需要校验的,用户没登录也是能访问的。

1)Dao

我们在mall-permission-servicecom.xz.mall.permission.mapper.PermissionMapper中创建一个方法,用于查询所有角色的权限

@Select("SELECT * FROM role_permission")
List<Map<Integer, Integer>> allRolePermissions();

2)Service

接口:在com.xz.mall.permission.service.PermissionService创建2个方法,分别查询不同匹配方式的权限和所有角色权限,代码如下:

//不同匹配方式的权限
List<Permission> findByMatch(int i);
​
//所有角色的权限映射
List<Map<Integer, Integer>> allRolePermissions();

实现类:修改com.xz.mall.permission.service.impl.PermissionServiceImpl添加实现方法,代码如下:

/***
 * 根据匹配方式查找
 * @param i
 * @return
 */
@Override
public List<Permission> findByMatch(int i) {
    QueryWrapper<Permission> queryWrapper = new QueryWrapper<Permission>();
    queryWrapper.eq("url_match",i);
    return permissionMapper.selectList(queryWrapper);
}
​
/**
 * 所有角色的权限
 * @return
 */
@Override
public List<Map<Integer, Integer>> allRolePermissions() {
    return permissionMapper.allRolePermissions();
}

3)初始化加载

创建com.xz.mall.permission.init.InitPermission实现ApplicationRunner,从而达到SpringBoot工程启动加载的目的,代码如下:

@Component
public class InitPermission implements ApplicationRunner {
​
    @Autowired
    private PermissionService permissionService;
​
    @Autowired
    private RedisTemplate redisTemplate;
​
    /***
     * 初始化
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        //根据匹配方式查找
        List<Permission> permissionsMatch0 = permissionService.findByMatch(0);
        List<Permission> permissionsMatch1 = permissionService.findByMatch(1);
​
        //查询所有角色权限
        List<Map<Integer,Integer>> rolePermissions = permissionService.allRolePermissions();
        //角色权限处理
        Map<String, Set<Permission>> roleMap = rolePermissionFilter(rolePermissions, permissionsMatch0, permissionsMatch1);
​
        //将所有权限和角色权限存入到Redis缓存
        redisTemplate.boundHashOps("RolePermissionAll").put("PermissionListMatch0",permissionsMatch0);
        redisTemplate.boundHashOps("RolePermissionAll").put("PermissionListMatch1",permissionsMatch1);
        redisTemplate.boundHashOps("RolePermissionMap").putAll(roleMap);
    }
​
​
    /***
     * 角色权限过滤
     * @param rolePermissions   : 角色权限映射关系
     * @param permissionsMatch0 :所有完全匹配路径
     * @param permissionsMatch1 :所有通配符匹配路径
     * @return
     */
    public Map<String,Set<Permission>> rolePermissionFilter(List<Map<Integer,Integer>> rolePermissions,
                                                             List<Permission> permissionsMatch0,
                                                             List<Permission> permissionsMatch1){
        //角色权限关系  key=roleid,value=List<Permission>
        Map<String, Set<Permission>> rolePermissionMapping = new HashMap<String,Set<Permission>>();
​
        //关系循环处理
        for (Map<Integer, Integer> rolePermissionMap : rolePermissions) {
            Integer rid = rolePermissionMap.get("rid");  //角色ID
            Integer pid = rolePermissionMap.get("pid");  //权限ID
            String key0 = "Role_0_"+rid.intValue();
            String key1 = "Role_1_"+rid.intValue();
​
            //获取当前角色拥有的权限
            Set<Permission> permissionSet0 = rolePermissionMapping.get(key0);
            Set<Permission> permissionSet1 = rolePermissionMapping.get(key1);
​
            //防止空指针
            permissionSet0=permissionSet0==null? new HashSet<Permission>(): permissionSet0;
            permissionSet1=permissionSet1==null? new HashSet<Permission>(): permissionSet1;
​
            //循环完全匹配路径
            for (Permission permission : permissionsMatch0) {
                if(permission.getId().intValue()==pid.intValue()){
                    permissionSet0.add(permission);
                    break;
                }
            }
            //循环通配符匹配路径
            for (Permission permission : permissionsMatch1) {
                if(permission.getId().intValue()==pid.intValue()){
                    permissionSet1.add(permission);
                    break;
                }
            }
            //将数据添加到rolePermissionMapping中
            if(permissionSet0.size()>0){
                rolePermissionMapping.put(key0,permissionSet0);
            }
            if(permissionSet1.size()>0){
                rolePermissionMapping.put(key1,permissionSet1);
            }
        }
        return rolePermissionMapping;
    }
}

如上代码,加载了所有权限以及不同角色的权限,并且所有权限都区分了匹配方式:

1:所有权限完全匹配 key=PermissionListMatch0
2:所有权限通配符匹配 key=PermissionListMatch1
3:角色权限完全匹配 key=Role_0_*,这里的*就是角色ID
4:角色权限通配符匹配 key=Role_1_*,这里的*就是角色ID

拦截校验

我们接下来实现对用户请求的地址进行校验,先校验当前请求地址是否需要执行权限校验,如果执行拦截才进入校验过程,如果不需要进行拦截是不需要校验的。

1)过滤器顺序调整

用户每次请求,地址栏会先经过处理,比如每次提交都会多携带一个/mall路径,此时对权限校验会造成干扰,需要将该路径处理掉,处理该路径SpringCloud Gateway提供了一个过滤器叫RouteToRequestUrlFilter,该过滤器处理完成后会找到处理当前请求的服务,接下来会调用LoadBalancerClientFilter选择一个处理请求的真实服务,我们要想判断权限,应该在这两个过滤器中间执行权限判断,因为此时知道要调用的服务名字也知道用户请求的真实服务地址,拦截器调用如下图:

1612699933885.png

此时我们把com.xz.mall.api.filter.ApiFilter的执行顺序调整一下:

@Override
public int getOrder() {
    //在RouteToRequestFilter之后执行
    return 10001;
}

调整后,过滤器执行顺序如下图:

1612700163615.png

2)是否校验判断

我们在com.xz.mall.api.permission.AuthorizationInterceptor中编写一个方法用于判断当前请求是否需要进行权限拦截,代码如下:

@Autowired
private RedisTemplate redisTemplate;
​
/****
 * 校验是否需要拦截指定请求
 */
public Boolean isIntercept(ServerWebExchange exchange){
    //request
    ServerHttpRequest request = exchange.getRequest();
    //uri
    String uri = request.getURI().getPath();
    //提交方法
    String method = request.getMethodValue();
    //路由URI信息
    URI routerUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
    //获取所有权限
    List<Permission> permissionMatch0 = (List<Permission>) redisTemplate.boundHashOps("RolePermissionAll").get("PermissionListMatch0");
    //完全匹配
    Permission permission = match0(permissionMatch0, uri, method,routerUri.getHost());
    //完全匹配如果没有,则匹配通配符
    if(permission==null){
        //匹配通配符
        List<Permission> permissionMatch1 = (List<Permission>) redisTemplate.boundHashOps("RolePermissionAll").get("PermissionListMatch1");
    }
    //如果此时permission则表示不需要进行权限校验
    if(permission==null){
        //不需要权限校验
        return false;
    }
    return true;
}
​
/***
 * 完全匹配
 * @param permissionMatch0
 * @param uri
 * @param method
 * @return
 */
public Permission match0(List<Permission> permissionMatch0,String uri,String method,String serviceName){
    //循环匹配
    for (Permission permission : permissionMatch0) {
        String matchUrl = permission.getUrl();
        String matchMethod = permission.getMethod();
        if(matchUrl.equals(uri)){
            //匹配提交方式
            if(!matchMethod.equals("*") && matchMethod.equalsIgnoreCase(method) && serviceName.equals(permission.getServiceName())){
                return permission;
            }
        }
    }
    return null;
}

这里我们会用到RedisTemplate,在mall-service项目中用的都是String进行序列化,我们需要把mall-common中的Redis工具类RedisConfig拷贝到mall-api-gateway中。

运行时一些参数如下:

1612700248755.png

此时可以通过URI获取要调用的服务名字、用户请求的地址等,如下图:

1612700397573.png

我们接下来在com.xz.mall.api.filter.ApiFilter中进行调用:

1612704651735.png

角色权限校验

1)令牌校验

我们先对之前代码进行优化,把令牌校验单独抽出到com.xz.mall.api.permission.AuthorizationInterceptor中,代码如下:

/***
 * 令牌校验
 */
public Map<String, Object> tokenIntercept(ServerWebExchange exchange){
    //request
    ServerHttpRequest request = exchange.getRequest();
    //客户端IP
    String ip = IPUtil.getIp(request);
    //用户令牌
    String token = request.getHeaders().getFirst("authorization");
    //令牌校验
    Map<String, Object> resultMap = AuthorizationInterceptor.jwtVerify(token, ip);
    return resultMap;
}

com.xz.mall.api.filter.ApiFilter中进行调用;

1612709023765.png

2)权限校验

每个用户存在多种角色,角色权限校验应该对种种角色权限进行校验,我们在com.xz.mall.api.permission.AuthorizationInterceptor中编写方法实现校验,代码如下:

/***
 * 角色权限校验
 */
public Boolean rolePermission(ServerWebExchange exchange,Map<String, Object> token){
    //request
    ServerHttpRequest request = exchange.getRequest();
    //uri
    String uri = request.getURI().getPath();
    //提交方法
    String method = request.getMethodValue();
    //路由URI信息
    URI routerUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
​
    //获取角色
    String[] roles = token.get("roles").toString().split(",");
    //当前角色权限
    Permission permission = null;
​
    //循环角色,获取角色权限
    for (String role : roles) {
        //===========完全匹配数据key0==============
        String key0 = "Role_0_"+role;
        //获取角色权限数据
        List<Permission> rolePermissionList0 = (List<Permission>) redisTemplate.boundHashOps("RolePermissionMap").get(key0);
        if(rolePermissionList0!=null){
            //匹配权限
            permission = match0((JSON.parseArray(JSON.toJSONString(rolePermissionList0),Permission.class)), uri, method, routerUri.getHost());
        }
​
        if(permission==null){
            
        }
​
        //如果找不到权限,说明无权访问
        if(permission!=null){
            break;
        }
    }
    return permission!=null;
}

我们接下来在com.xz.mall.api.filter.ApiFilter中进行调用,代码如下:

1612762831229.png

测试的时候,有权限则顺利通过调用,没有权限的角色会显示如下提示:

1612758554098.png

总结

本篇主要介绍了一下权限初始化、拦截校验、角色权限校验的实现demo。