分享我的权限认证模型,只依赖JFinal,简单灵活完全可定制

鉴权是每个系统的标配,以前也试过shiro,但总是感觉过于繁琐,不符合自己的操作习惯。

好在JFinal的AOP支持够好,通过Interceptor可以很方便的定义自己的鉴权模型。

我实现的方案包括以下几个部分:

1、AuthRequire:鉴权注解

2、EInterceptorAuth:鉴权拦截器,可定义为全局,或控制器自由引用

3、扩展User Model,增加鉴权相关的具体实现方法

4、控制器层面的鉴权

5、视图模板层面的鉴权

关键部分的源码如下:

/**
 * 授权验证注解
 * 
 * @author netwild
 *
 */
public @interface AuthRequire {

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE, ElementType.METHOD })
    public static @interface Guest{}

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE, ElementType.METHOD })
    public static @interface Logined{}

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE, ElementType.METHOD })
    public static @interface Perm{ String value(); }

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE, ElementType.METHOD })
    public static @interface Perms{ String[] value(); }

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE, ElementType.METHOD })
    public static @interface Role{ String value(); }

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE, ElementType.METHOD })
    public static @interface Roles{ String[] value(); }

}
/**
 * 鉴权拦截器
 * 
 * @author netwild
 *
 */
public class EInterceptorAuth implements Interceptor {
    
    public void intercept(Invocation inv) {
        EController controller = (EController)inv.getController();
        HttpServletRequest request = controller.getRequest();
        HttpServletResponse response = controller.getResponse();
        
        //返回Cookie中的登录ID,视图层可根据currId是否为null判断当前的登录状态
        String currentId = getCurrent(request, response);
        //根据登录ID,返回用户(User)实例
        UserInterface user = getCurrentUser(currentId);
        
        boolean authAccept = false; //鉴权结果,默认为失败
        Method method = inv.getMethod(); //当前访问的Action
        
        /**
         * 解析鉴权模式:
         * 1)先判断Action是否要求鉴权;
         * 2)如果没有,则继续判断Controller是否要求鉴权;
         * 3)如果仍然没有,则默认允许匿名访问
         */
        AuthMode mode = new AuthMode(); //鉴权模式对象封装
        getMethodAuthMode(mode, method, controller.getClass());
        
        if(mode.authCode == ErrorCode.REQ_GUEST){ //允许匿名
            authAccept = true; //鉴权通过
        }else if(user != null){ //不允许匿名,且当前已登录
            switch(mode.authCode){
            case REQ_ROLE: //根据单个角色进行鉴权
                if(user.checkRole(mode.authId)) authAccept = true;
                break;
            case REQ_ROLES: //根据多个角色进行鉴权
                if(user.checkRoles(mode.authIds)) authAccept = true;
                break;
            case REQ_PERM: //根据单个权限进行鉴权
                if(user.checkPerm(mode.authId)) authAccept = true;
                break;
            case REQ_PERMS: //根据多个权限进行鉴权
                if(user.checkPerms(mode.authIds)) authAccept = true;
                break;
            case REQ_LOGIN: //根据用户的登录状态进行鉴权
                authAccept = true;
                break;
            default:
                break;
            }
        }
        
        //将登录标识ID及用户实例存入会话
        //视图中即可使用“user”的系列鉴权方法实现按钮级鉴权
        controller.setAttr(Const.ATTR_KEY_CURRENT, currentId);
        controller.setAttr(Const.ATTR_KEY_USER, user);
        
        if(!authAccept){ //身份认证失败
            controller.renderErr(mode.authCode, "身份认证失败");
        }else{ //认证通过,执行Controller
            inv.invoke();
        }
    }
    
    /**
     * 优先进行Action层面的鉴权
     * @param method
     * @param ctrl
     */
    private void getMethodAuthMode(AuthMode mode, Method method, Class<?> ctrl){
        if(method.isAnnotationPresent(AuthRequire.Role.class)){
            mode.authCode = ErrorCode.REQ_ROLE;
            mode.authId = method.getAnnotation(AuthRequire.Role.class).value();
        }else if(method.isAnnotationPresent(AuthRequire.Roles.class)){
            mode.authCode = ErrorCode.REQ_ROLES;
            mode.authIds = method.getAnnotation(AuthRequire.Roles.class).value();
        }else if(method.isAnnotationPresent(AuthRequire.Perm.class)){
            mode.authCode = ErrorCode.REQ_PERM;
            mode.authId = method.getAnnotation(AuthRequire.Perm.class).value();
        }else if(method.isAnnotationPresent(AuthRequire.Perms.class)){
            mode.authCode = ErrorCode.REQ_PERMS;
            mode.authIds = method.getAnnotation(AuthRequire.Perms.class).value();
        }else if(method.isAnnotationPresent(AuthRequire.Logined.class)){
            mode.authCode = ErrorCode.REQ_LOGIN;
        }else if(method.isAnnotationPresent(AuthRequire.Guest.class)){
            mode.authCode = ErrorCode.REQ_GUEST;
        }else{
            getControllerAuthMode(mode, ctrl);
        }
    }
    
    /**
     * 进行Controller层面的鉴权,只有当Action未设置时有效
     * @param ctrl
     */
    private void getControllerAuthMode(AuthMode mode, Class<?> ctrl){
        if(ctrl.isAnnotationPresent(AuthRequire.Role.class)){
            mode.authCode = ErrorCode.REQ_ROLE;
            mode.authId = ctrl.getAnnotation(AuthRequire.Role.class).value();
        }else if(ctrl.isAnnotationPresent(AuthRequire.Roles.class)){
            mode.authCode = ErrorCode.REQ_ROLES;
            mode.authIds = ctrl.getAnnotation(AuthRequire.Roles.class).value();
        }else if(ctrl.isAnnotationPresent(AuthRequire.Perm.class)){
            mode.authCode = ErrorCode.REQ_PERM;
            mode.authId = ctrl.getAnnotation(AuthRequire.Perm.class).value();
        }else if(ctrl.isAnnotationPresent(AuthRequire.Perms.class)){
            mode.authCode = ErrorCode.REQ_PERMS;
            mode.authIds = ctrl.getAnnotation(AuthRequire.Perms.class).value();
        }else if(ctrl.isAnnotationPresent(AuthRequire.Logined.class)){
            mode.authCode = ErrorCode.REQ_LOGIN;
        }else{
            mode.authCode = ErrorCode.REQ_GUEST;
        }
    }
    
    /**
     * 从缓存或数据库中根据当前登录ID获取用户对象
     * @param currentId
     * @return
     */
    private UserInterface getCurrentUser(String currentId){
        if(EStr.notEmpty(currentId)){
            return EZPlatLaunch.getUserService().findById(currentId);
        }else{
            return null;
        }
    }
    
    /**
     * 鉴权模式封装
     */
    class AuthMode{
        private ErrorCode authCode = null; //各种鉴权模式枚举
        private String authId = null; //单条鉴权标识(单角色、单权限)
        private String[] authIds = null; //多条鉴权标识(多角色、多权限)
    }

}
/**
 * 控制器鉴权
 * 如果方法中定义了鉴权规则,则被忽略
 * 如果方法和控制器都未定义鉴权规则,默认允许匿名
 */

@AuthRequire.Role("SuperAdmin") //要求单条角色鉴权
//@AuthRequire.Roles({"UserAdmin", "FileAdmin"}) //要求多条角色授权
//@AuthRequire.Perm("addUser") //要求单条权限鉴权
//@AuthRequire.Perms({"addUser", "delUser"}) //要求多条权限鉴权
//@AuthRequire.Logined //要求登录鉴权
//@AuthRequire.Guest //允许匿名访问
public class OaController extends Controller {

    /**
     * 优先执行方法鉴权:
     * 如果未定义,则执行控制器鉴权
     */
    
    @AuthRequire.Role("SuperAdmin") //要求单条角色鉴权
    //@AuthRequire.Roles({"UserAdmin", "FileAdmin"}) //要求多条角色授权
    //@AuthRequire.Perm("addUser") //要求单条权限鉴权
    //@AuthRequire.Perms({"addUser", "delUser"}) //要求多条权限鉴权
    //@AuthRequire.Logined //要求登录鉴权
    //@AuthRequire.Guest //允许匿名访问
    public void addUser(){
        //...
    }
    
}
<!-- 
视图模板中的鉴权 
其中的"addUser"为权限别名
-->

#if(user.checkPerm("addUser")??)
<a href="...">添加用户</a>
#end


整个鉴权模型非常简单,可定制性也很高,目前为止用的很顺手

希望能给需要的人带来一点启发,也希望大大们提出宝贵建议!

评论区

爪爪

2017-06-17 09:34

感谢

爪爪

2017-06-17 10:12

请教一下,你这个Perms的权限标记是怎么给用户的,不是通过角色给的吧,是单独给的,就是Perms这个权限标记与角色没有关系,是操作人根据需求意愿给用户的????

netwild

2017-06-17 11:06

@爪爪 我习惯使用三层权限:User -> Role -> Perm
User与Role是1 : *,Role与Perm也是1 : *
不建议直接将Perm与User建立关系,后期权限多了很麻烦
具体鉴权时,Role或者Roles都很简单,Perm和Perms都需要通过Role来连接

JFinal

2017-06-17 11:40

@netwild 这个设计非常简洁,已点赞 + 收藏

有几个小建议:
1:#if(user.checkPerm("addUser")??false) 可以改为:#if(user.checkPerm("addUser")??),因为 ?? 表达式右侧没有任何表达式时与 expr ?? null 是完全等价的,而 null 值的 boolean 求值为 false

2:建议用 jfinal template engine 的指令级扩展实现界面的权限控制,类似于这样的用法:
#roles("role1", "role2")
### 显示某某菜单的 html 内容
#end

#permission(...)
### 显示某某操作按钮的 html 内容
#end

3:EInterceptorAuth 这个拦截器有线程安全问题,因为 jfinal 拦截器是全局共享的,所以 authCode、authId、authIds 这三个属性在多线程并发时会出问题

改进的办法是将继承 Interceptor 改为继承 PrototypeInteceptor。最好的办法是让那三个属性变为方法内的临时变量,这样性能最好

netwild

2017-06-17 12:09

@JFinal 感谢波总的点评
1、安全输出的用法我还不太熟练,继续强化吧
2、指令扩展也可以,但我发现指令无法嵌套使用,就是一个#(...)里面没办法再调用其他指令。例如这种需求:
#if(user.checkPerm("addUser")?? || mode == 'Super')
...
#end
如果使用扩展指令的话,就没办法额外增加后面的判断了
3、这个太重要了,感谢波总提醒,已经改为局部变量

JFinal

2017-06-17 12:37

@netwild 关于第二点,指令参数必须是表达式,则不能是指令,所以你可以使用表达式作为自定义指令的参数

而指令间的嵌套是指这样的形式:
#dirAaa(...)
#dirBbb(...)
#end
#end

这样的嵌套 jfinal 必然是支持的,用自定义指令扩展是绝对可以实现你的需求的,有不少人已经是这样做的了,注意看下面的用法:
#hasPermission("menu:add")
。。。。。
#end

项目链接在下面:
http://git.oschina.net/jfinal/LMS/blob/master/lms-web/src/main/webapp/WEB-INF/views/admin/menu/menuManage.html?dir=0&filepath=lms-web%2Fsrc%2Fmain%2Fwebapp%2FWEB-INF%2Fviews%2Fadmin%2Fmenu%2FmenuManage.html&oid=da7ebdfd43636a17cc62c8ebe08d187f63befece&sha=0ad0fa183a3e90af4593f6bf6793377bbcb362cf

netwild

2017-06-17 13:01

@JFinal 是我表达有误,不是指令嵌套。
但就我说的需求,用自定义指令貌似还是无法实现,或者说实现起来很麻烦。
#hasPermission("menu:add") 这种简单情况没问题
那如果 #hasPermission("menu:add") || 其他条件呢?
简单说就是页面上某个按钮不仅跟权限有关,还可能同时需要做其他的逻辑判断
那这时候势必会使用#if(...),但括号里又不能使用指令

jimmyyn

2017-06-19 12:01

建议还是用shiro,便于维护和升级。

JFinal

2017-06-19 12:29

@jimmyyn 今天正好有些 shrio 整合 jfinal 的资源出来:
https://my.oschina.net/giegie/blog/983110
http://git.oschina.net/myaniu/jfinalshiroplugin

爪爪

2017-06-21 10:09

@JFinal 波总这个链接失效了??

liuzy666

2017-07-04 20:00

@netwild 写个插件咋样?为权限苦恼万年了

netwild

2017-07-05 08:16

@liuzy666 我现在就是这么用的,插件的话你可以封装一下然后共享出来

lyh061619

2017-07-07 09:40

这个思路不错呀,实现的效果跟shrio差不多的功能。

RichardHe

2017-08-09 17:48

这注解也太多了。感觉不够简洁。

netwild

2017-08-14 11:39

@RichardHe 我只是实现了一个正常的权限验证模型应该有的,至于注解的多与少,全凭项目需要可以自行调整,何况常用的其实就那几个

Shydow

2017-10-17 20:08

@netwild 方便分享下数据库表的格式嚒?1179501421@qq.com

netwild

2018-01-06 17:55

@Shydow 你是说角色跟权限表吗?这两个表的结构没啥特殊的

ppupup

2018-09-11 12:36

感谢楼主,跟我一个想法,shiro太麻烦。而且经常使用各种框架带来的问题就是人变懒了,对于基础知识以及设计的遗忘,所以时不时写两个轮子也是很不错的。

JFinal

2018-09-11 13:24

@ppupup jfinal club 提供了一个极度简洁的权限方案,权限自动化生成

热门分享

扫码入社