简单的登录与权限(Java + Angularjs)

数据库模型

简单的用户权限系统中, 涉及到三种模型: 用户、角色、权限。 用户具有角色, 角色拥有权限, 于是用户通过角色便可以拥有权限。基于这样的联系, 用户通过角色便获得了权限。如图。著名的开源系统Redmine就使用了这种简单的权限设计。

User -> RoleList -> PermissionList

服务器端

登录机制

三个接口: 用户登录, 用户登出, 用户是否登录。此处过于基础常见, 不再细说。

权限机制

通过注解管理接口权限, 定义权限注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Permission {
     String value();  //接口权限
}

需权限限制方法处添加注记:

@RestController
Class A {
    @Permission(value='create')
    String create() {
        … // create logic
    }
}

通过拦截器实现权限限制逻辑

public class PermissionInterceptor extends HandlerInterceptorAdapter {
        
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

…
List<String> userPermissionsList = …;  //从request中获取用户权限列表

if(handler.getClass().isAssignableFrom(HandlerMethod.class)){
    Permission permissionAnno = ((HandlerMethod) handler).getMethodAnnotation(Permission.class);    
// 从注解中获取接口允许的权限列表
    String permission = permissionAnno.value();
// 如果用户权限列表于接口权限列表有交集, 放行; 否则返回客户端无权限 
if (StringUtils.isBlank(permission) || userPermissionsList.contains(permission)) {
       return true;
} else {
    Return false;
}
}
return true;
}

客户端(AngularJs)

登录机制

主要可以分为以下几块。

本地登录状态存储: 按照生命周期的不同, 可以保存为全局变量(页面级), SessionStorage(会话级), localStorage(本地持久化)。

服务器登录状态验证: 每次发送http请求, 或者切换页面时, 检查服务器端的登录状态。

登录状态更新事件的传播:每次获取到登录状态, 向顶层作用域广播登录状态的更新时间

登录状态改变事件的捕获:判断最新的登录状态, 如果没有登录, 跳转至登录页面。

本地登录信息存储

在AngularJs中 , Service都是单例的。所以我们可以把状态的保存放在Service中。 而根据实际需要, 登录状态以及登录用户信息可以在Service中用成员变量保存, 或者从sessionStorage中读取, 或者从localStorage中读取。

通过这个本地的登录状态, 系统可以判断当前的登录状态, 也可以读取登录用户的信息。

下面的这个例子中, 本地用户登录信息是放在成员变量中, 所以每次打开页面, 这个变量都是空, 系统都会跳转到登录页面。

app.factory('BaseAuthService', function ($rootScope,$q, $http) {
    var baseLoginUser = null;
    var userName = "";
    var userId = "";

    return {
        getAuth: function () {
            return {
                baseLoginUser : baseLoginUser,
                userName : userName,
                userId : userId
            }
        },
        getUserId: function () {
            return userId;
        },
        getUser: function () {
            return baseLoginUser;
        },
isLogin: function () {
            if (null == baseLoginUser) {
                return false;
            } else {
                return true;
            }
        },
    };

});

登录状态验证

在特定的操作下, 特定的时间点, 前端页面需要到服务器获取用户登录状态。 具体选择什么样的操作, 什么样的时机, 还是需要根据自己系统的特点选择。

如果为了实现简单, 系统也没有大的并发压力, 可以在每一次http请求, 每一次页面流转时, 均进行登录验证。也可以在获取到登录信息的同时, 获取登录信息的过期时间, 前端页面只有在登录信息过期时才试图更新登录信息。

现在只讨论如何在每一次http请求, 每一次页面流转时, 加入登录状态的验证。

  • http拦截器

每一次http请求后均检查用户是否登录, 如果检测到未登录, 设置系统状态为未登录。


app.config(['$httpProvider', function ($httpProvider){
    $httpProvider.interceptors.push('normalHttpInterceptor');
}]);


app.factory('normalHttpInterceptor', ['$rootScope', '$injector', function ($rootScope,$injector){
    return {
 
        response: function(res){
            $rootScope.gHideLoading();
            var BaseAuthService = $injector.get('BaseAuthService');
            if (!!res.data && res.data.code == "business021") {
                // 未登录, 跳转登录页面
                BaseAuthService.setAuth(null, null);
            }
            return res;
        },
    };
}]);

  • 页面流转处登录验证
  • 捕获页面流转时产生的$stateChangeStart事件,如果成功获取用户的登录状态(已登录或未登录), 设置本地系统状态为相应状态。

    	app.run(["$rootScope",'$state', 'BaseAuthService', function($rootScope, $state, BaseAuthService){
        $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
    
            if (toState.name == "login") {
                return;
            }
            var fetch = BaseAuthService.fetchAuth();
            fetch.then(
                function (res) {
    
                    if (!!res) {
                        if (res.success) {
                            BaseAuthService.setAuth(res.data, res.data.permissions);
                        } else {
                            BaseAuthService.setAuth(null, null);
                        }
                    } else {
    
                    }
                },
                function (err) {
                    
                }
            );
        });
    
    }]);
    
    
    

    登录状态更新事件的传播

    更新本地的登录状态时, 同时广播登录状态更改事件: baseAuthChanged

    app.factory('BaseAuthService', function ($rootScope,$q, $http) {
        var basePermissionList = null;
        var baseLoginUser = null;
        var userName = "";
        var userId = "";
    
    .....
        return {
            setAuth: function(baseLoginUse, basePermissionLis) {
                baseLoginUser = baseLoginUse;
                basePermissionList = basePermissionLis;
    
                userName = null;
                userId = null;
                if (!!baseLoginUser) {
                    userName = baseLoginUser.userName;
                    userId = baseLoginUser.userId;
                }
    
                $rootScope.$broadcast('baseAuthChanged');
                $rootScope.$broadcast('permissionsChanged');
            },
    
        };
    
    });
    

    登录状态改变事件的捕获

    在最外层的Controller中监听事件“baseAuthChanged”
    事件触发时, 如果本地登录状态为未登录, 跳转到登录页面。

    $rootScope.$on("baseAuthChanged", function () {
        var auth = BaseAuthService.getAuth();
    
        if (!BaseAuthService.isLogin()) {
            //$location.path("/login");
            $state.go('login');
            return;
        } 
    });
    
    

    AngularJs中两种事件传播机制

    $broadcast的作用是将事件从父级作用域传播至子级作用域,包括自己。格式如下:$broadcast(eventName,args)

    $emit的作用是将事件从子级作用域传播至父级作用域,包括自己,直至根作用域。格式如下:$emit(eventName,args)

    权限机制

    前端权限控制的目的, 仅仅是让元素按照不同的权限进行显示与隐藏。具体实现起来十分简单。

    在上述的登录机制中, 后台接口提供的登录用户信息, 不仅仅有id, 用户名等基本信息,还需提供权限列表信息。我们设置本地登录信息的时候, 登录用户的权限列表也就保存在本地页面了。

    app.factory('BaseAuthService', function ($rootScope,$q, $http) {
        var basePermissionList = null;
        var baseLoginUser = null;
        var userName = "";
        var userId = "";
    
    
        var fetchAuth = function () {
            var deferred = $q.defer();
            $http({
                method: 'GET',
                url: 'rms/1.0/auth/getLoginUser'
            }).success(function(response) {
                deferred.resolve(response);
            }).error(function(response) {
                deferred.reject(response);
            });
            return deferred.promise;
        }
    
    
        return {
            setAuth: function(baseLoginUse, basePermissionLis) {
                baseLoginUser = baseLoginUse;
                basePermissionList = basePermissionLis;
    
                userName = null;
                userId = null;
                if (!!baseLoginUser) {
                    userName = baseLoginUser.userName;
                    userId = baseLoginUser.userId;
                }
    
                $rootScope.$broadcast('baseAuthChanged');
                $rootScope.$broadcast('permissionsChanged');
            },
            getAuth: function () {
                return {
                    baseLoginUser : baseLoginUser,
                    basePermissionList : basePermissionList,
                    userName : userName,
                    userId : userId
                }
            },
            getUserId: function () {
                return userId;
            },
            getUser: function () {
                return baseLoginUser;
            },
            getPermissions: function () {
                return basePermissionList;
            },
            hasPermission: function (permission) {
                if (null == permission || typeof (permission) == "undefined" || permission == "") {
                    return true;
                }
    
                for (var idx in basePermissionList) {
                    var userPermission = basePermissionList[idx];
                    if (userPermission.name  == permission) {
                        return true;
                    }
                }
    
                return false;
            },
            isLogin: function () {
                if (null == baseLoginUser) {
                    return false;
                } else {
                    return true;
                }
            },
            fetchAuth : fetchAuth
        };
    
    });
    
    

    在本地拥有了权限列表的前提下, 我们可以通过AngularJs的指令机制, 轻易地将权限应用到页面上的元素中。

    <button has-permission=”create”></button>

    app.directive('hasPermission', function(BaseAuthService) {
        return {
            restrict: 'A',
            link: function(scope, element, attrs) {
    
                var value = attrs.hasPermission.trim();
                var notPermissionFlag = value[0] === '!';
                if(notPermissionFlag) {
                    value = value.slice(1).trim();
                }
    
                function toggleVisibilityBasedOnPermission() {
                    var hasPermission = BaseAuthService.hasPermission(value);
    
                    if(hasPermission && !notPermissionFlag || !hasPermission && notPermissionFlag)
                        element.show();
                    else
                        element.hide();
                }
                toggleVisibilityBasedOnPermission();
                scope.$on('permissionsChanged', toggleVisibilityBasedOnPermission);
            }
        };
    });