不得不承认,这个标题异常绕口……


前言

现在用jersey+spring+hibernate已经做了好几套“XXXX管理系统”了,但其实和单机应用无异,因为从来都没有遇到过权限设计的问题。但是上周帮学长做毕业设计时候不得已碰到了这一块。折腾了近5个小时。最终算有个屌丝版的解决方案了。

要求

个人觉得一个可接收的权限设计的最基本要求大概如下:

  • 与业务(接口)分离
  • 设计简单,安全性有保障
  • 浏览器端与应用端方法统一

分析

与业务分离很容易理解,解耦合,就算以后有权限改动也不用改动代码。设计简单没什么好说的。浏览器端与应用端统一,貌似应用端使用session不是太方便,而且我一直都是非常喜欢Restful的无状态设计。(我不会说只是我不会用session罢了)。所以也不选用session

实现方案

Token_1.png

差不多就是这样了。

代码实现

数据库设计

首先是后端部分,表设计如下: Token_2.png

由于权限是精确到各个API接口部分的,所以有三张表,一张接口,一张角色,一张权限控制。 API又分POST,DELETE,GET,PUT等,所以API表需要做个细分。初步设想是这样的,在系统运行时,一次性把权限表载入内存,省去了每次读写数据库的开支。然后有改动之后同步即可。

生成Token

登陆模块生成Token较为简单:

  • 获取用户帐号密码
  • 与数据库对比
  • 用户+密码+盐(或者时间,或者随机码)MD5之类的生成密钥(Token)
  • 把<密钥,用户>放入缓存,并设置过期时间
  • 返回密钥给前台

拦截验证

后台Jersey的拦截器代码大致如下:

public void filter(ContainerRequestContext requestContext) throws IOException {
    /*请求Path模板*/
    List<UriTemplate> matchedTemplates = uriInfo.getMatchedTemplates();
    StringBuilder path = new StringBuilder();
    for (int i = matchedTemplates.size() - 1; i >= 0; i--) {
        path.append(matchedTemplates.get(i).getTemplate());
    }
    /*请求类型*/
    String type = requestContext.getMethod();
    /*获取token*/
    String token = requestContext.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
    if (TokenUser == null) {
        /*创建<Token,User>缓存*/
        TokenUser = new CrunchifyInMemoryCache<>(3600, 300, 3000);
    }
    /*如果已经登陆*/
    if (TokenUser.get(token) != null) {
        /*获取用户以及其权限*/
        User loginUser = TokenUser.get(token);
        String role = loginUser.getPrivilege();
        Permission x = new Permission();
        if (PermissionCache == null) {
            /*创建权限缓存,并从表导入*/
            PermissionCache = new CrunchifyInMemoryCache<>(0, 0, 3000);
            permissionService.loadIntoCache();
        }
        /*在缓存内寻找用户的权限*/
        Boolean flag = PermissionCache.get(role + "-" + path + "-" + type);
        /*如果未找到(未设置权限)或者无权限*/
        if (flag == null || flag == false) {
            /*打断,返回401*/
            requestContext.abortWith(Response
                    .status(Response.Status.UNAUTHORIZED)
                    .entity("User cannot access the resource.")
                    .build());
        }else{
            /*正常通行*/
        }
    /*如果未登陆*/
    } else {
        /*登陆api属于特殊放行的特例*/
        if (path.toString().trim().equals("/user/login")) {
            /*正常通行*/
        } else {
            /*打断,返回401*/
            requestContext.abortWith(Response
                    .status(Response.Status.UNAUTHORIZED)
                    .entity("User cannot access the resource.")
                    .build());
        }
    }
}

前端控制

由于对前端不熟,所以只有一个最简单的思路:

  • 发送帐号密码,获取Token(必要时也获取用户信息)

  • 放入Cookie,设置过期时间

  • ajaxSetup设置每次的请求头,和统一处理返回码401的情况(跳转到登录页)

  • 注销时删除所有相关Cookie