Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之登录、权限、菜单和登出

  1. 尝试新的开发组合:Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS
  2. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之配置IdentityServer
  3. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之数据迁移
  4. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之添加实体
  5. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之显示登录视图
  6. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之验证码
  7. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之登录、权限、菜单和登出

登录

登录成功后,服务器会返回访问令牌(accessToken)、加密访问令牌(encryptedAccessToken)和令牌过期时间(tokenExpireDate)等三个数据,在客户端端要做的就是把访问令牌和加密访问令牌保存到Cookie中,具体代码如下:

onLoginButton: function() {
        ...
                success: function(form, action) {
                    var rememberMe = form.getValues().rememberClient || false,
                        obj = Ext.decode(action.response.responseText, true),
                        tokenExpireDate,
                        msg = '未知错误';
                    if (obj.success) {
                        tokenExpireDate = rememberMe ? (new Date(new Date().getTime() + 1000 * obj.result.expireInSeconds)) : null
                        HEADERS.setCookies(HEADERS.authTokenCookieName, obj.result.accessToken, tokenExpireDate);
                        HEADERS.setCookies(HEADERS.encrptedAuthTokenName, obj.result.encryptedAccessToken, tokenExpireDate, LOCALPATH);
                        window.location.reload();
                    } else {
                        if (result.error && result.error.message)
                            msg = result.error.message + (result.error.details ? result.error.details : '');
                        TOAST.toast(
                            msg,
                            form.owner.el,
                            'bl'
                        );
                    }
                },
        ...
    },

如果返回结果成功(success为true),则先判断rememberMe是否为true,如果为true,则以返回的过期时间为基准,设置cookie的过期时间。计算好过期时间后,就调用setCookies方法设置cookie。

由于之前的setCookies方法在调用Ext.util.Cookiesset方法时使用了全部6个参数,而现在只设置了4个参数,会造成错误,不能设置cookie,因而需要修改setCookies方法,将参数修改为4个。

在设置cookie后,就要调用reload方法重新刷新页面从新走流程了。这时候,在onMainViewRender方法内,由于已经有cookie了,就不转到登录页,而是需要加载当前用户信息了。在ABP框架,在获取初始化数据的时候,将用户数据分在了两个地方,一个是在项目启动时获取Session信息,在里面包含了用户名、电子邮件和用户编号等信息,一个是获取当前用户配置信息,包括了租户(MultiTenancy)、Session、本地化(Localization)、功能(Features)、认证(Auth,包括全部权限和用户授权的权限)、导航(Nav)、设置(Setting)、时钟(Clokc)、时间配置(Timing)和安全(Security)等信息,具体可通过查看Abp源代码中的Abp.Web.Common\Web\Configuration\AbpUserConfigurationBuilder.cs文件来了解具体情况。

本来只打算访问AbpUserConfiguration/GetAll来获取用户配置信息就算了,但发现居然不包含用户名等信息,如果自己加的话要修改AbpUserConfigurationBuilder类,比较麻烦,所以还是根据angular的加载流程来加载算了。在angular的流程中,Session是通过Promise对象来异步加载的,还好,在Ext JS 6.2中,包含了对Promise对象封装,我们也可以使用该类来实现Session的加载。先在app\util文件夹下创建一个名为Session.js的文件,然后添加以下代码:

Ext.define('SimpleCMS.util.Session',{
    alternateClassName: 'SESSION',
    singleton: true,

    requires:[
        'SimpleCMS.util.Failed'
    ],

    init: function(){
        return new Ext.Promise(function (resolve, reject) {
            Ext.Ajax.request({
                url: URI.get('Session', 'GetCurrentLoginInformations'),   
                success: function (response) {
                    resolve(response.responseText);
                },   
                failure: function (response) {
                    reject(response.status);
                }
            });
        });
    },

    processData: function(content){
        var obj = Ext.decode(content, true);
        if(obj.success){
            Ext.apply(CFG, obj.result);
        }
    }
});

代码中,在init方法内创建了一个Ext.Promise对象,用来访问Session/GetCurrentLoginInformations来获取Session信息,返回成功后,将调用processData方法来处理返回的数据,在这里只是简单的将返回的结果复制到CFG对象中。

app.js中添加了对SimpleCMS.util.Session的访问后,就可Application.jsinit方法的最后加入以下代码来获取登录信息了:

SESSION.init().then(SESSION.processData);

好了,现在可以获取到登录后的用户名和邮件地址了,在主视图的视图控制器的onMainViewRender方法内,可以添加后续代码来获取用户配置信息了,具体代码如下:

onMainViewRender: function() {
        var me = this,
            token = HEADERS.getAuthToken();
        if (Ext.isEmpty(token)) {
            me.setCurrentView("login");
 return;
        }
        Ext.Msg.wait(I18N.GetUserInfo);
        Ext.Ajax.request({
            url: URI.get('AbpUserConfiguration', 'GetAll', true),
            success: function(response, opts) {
                var me = this,
                    refs = me.getReferences(),
                    navigationList = refs.navigationTreeList,
                    store = navigationList.getStore(),
                    root = store.getRoot(),
                    viewModel = me.getViewModel(),
                    obj = Ext.decode(response.responseText, true),
                    hash, node, parentNode, roles, reuslt;
                Ext.Msg.hide();
                if (obj.success) {
                    result = obj.result;
                    if (!result.session.userId) {
                        me.setCurrentView("login");
 return;
                    }
                    Ext.apply(CFG, result);
                    viewModel.set('UserName', CFG.user.userName);
                    me.processMenu(root, result.nav.menus.MainMenu.items);
                    me.isLogin = true;
                    hash = window.location.hash.substr(1);
                    me.setCurrentView(Ext.isEmpty(hash) || hash === 'login' ? "articleView" : hash);
                }
            },
            failure: FAILED.ajax,
            scope: me
        });
    },

由于AbpUserConfiguration/GetAll不是api的访问地址,因而需要在调用get方法时加上第三个参数且值为true。

在成功获取到用户配置信息后,先判断session中是否存在uersId,如果没有,说明访问令牌已经过期,不能访问资源,需要重新登录。如果存在,说明已经能访问资源,就将返回的信息复制到CFG对象,并设置视图模型中的UserName,以便在页面显示用户吗。

由于ABP框架的菜单定义与我们所需的菜单定义的格式不同,因而需要调用processMenu方法做一下转换,具体代码如下:

processMenu: function(root, menus) {
        var ln = menus.length,
            i = 0,
            result = [],
            routeId = '';
        for (i; i < ln; i++) {
            menu = menus[i];
            routeId = menu.url;
            result.push({
                text: menu.displayName,
                iconCls: menu.icon,
                rowCls: 'nav-tree-badge',
                viewType: routeId,
                routeId: routeId,
                leaf: true
            });
        }
        if (result.length > 0) root.appendChild(result);
    },

转换过程主要这将返回的菜单显示名称(displayName)作为导航节点的文本(text)值,将图标(icon)作为iconCls的值,将url作为routeId和ViewType的值。这里的要注意的是,处理过程没有考虑子菜单的情况,如果有子菜单,需要做递归处理。在菜单转换完成后,就可将菜单添加到导航树中。

菜单处理完成后,要调用setCurrentView方法来设置初始视图。

至此,登录过程就完成了。

菜单

在ABP中添加导航菜单,需要从NavigationProvider类中派生出自己的导航提供者类。在
Web.Core项目中,添加一个名为Navigation的文件夹,并在文件夹下创建一个名为SimpleCmsWithAbpAppNavigationProvider的类,然后添加以下代码:

public class SimpleCmsWithAbpAppNavigationProvider : NavigationProvider
    {
        public override void SetNavigation(INavigationProviderContext context)
        {
            context.Manager.MainMenu
                .AddItem(
                    new MenuItemDefinition(
                        "ArticleManagement",
                        L("ArticleManagement"),
                        url: "articleView",
                        icon: "fa fa-file-text-o",
                        requiresAuthentication:true,
                        requiredPermissionName: PermissionNames.Pages_Articles
                    )
                )
                .AddItem(
                    new MenuItemDefinition(
                        "MediaManagement",
                        L("MediaManagement"),
                        url: "mediaView",
                        icon: "fa fa-file-image-o",
                        requiresAuthentication: true,
                        requiredPermissionName: PermissionNames.Pages_Articles
                    )
                )
                .AddItem(
                    new MenuItemDefinition(
                        "UserManagement",
                        L("UserManagement"),
                        url: "userView",
                        icon: "fa fa-user",
                        requiresAuthentication: true,
                        requiredPermissionName: PermissionNames.Pages_Users
                    )
                );
        }

        private static ILocalizableString L(string name)
        {
            return new LocalizableString(name, SimpleCmsWithAbpConsts.LocalizationSourceName);
        }

在代码中添加了3个菜单,每个菜单包含了菜单名称(name)、显示名称(displayName)、访问地址(url)、图标(icon)、要求验证(requiresAuthentication)和所需权限(requiredPermissionName)等项。将requiresAuthentication设置为true后,只有用户具有访问权限的菜单才会返回到客户端,没有访问权限的菜单将不会返回客户端。而判断权限的依据就是requiredPermissionName的定义。

菜单定义之后,打开SimpleCmsWithAbpWebCoreModule.cs文件,在PreInitialize方法的ConfigureTokenAuth之上添加以下代码配置菜单:

Configuration.Navigation.Providers.Add<SimpleCmsWithAbpAppNavigationProvider>();

对于菜单的本地化工作,可以直接修改Core项目中Localization\SourceFiles文件夹下的文件,也可以添加到数据库中。

为了便于将本地化信息添加到数据库,我们可以在EntityFrameworkCore项目中的EntityFrameworkCore\Seed\Host中添加一个名为DefaultApplicationLanguageTextCreator的类,具体代码如下:

public class DefaultApplicationLanguageTextCreator
    {
        public static List<ApplicationLanguageText> InitialLanguageTexts => GetInitialLanguageTexts();

        private readonly SimpleCmsWithAbpDbContext _context;
        private const string DefaultLanguageName = "zh-CN";

        private static List<ApplicationLanguageText> GetInitialLanguageTexts()
        {
            return new List<ApplicationLanguageText>
            {
                new ApplicationLanguageText()
                {
                    CreationTime = Clock.Now,
                    Source = SimpleCmsWithAbpConsts.LocalizationSourceName,
                    LanguageName = DefaultLanguageName,
                    Key = "verifyCodeInvalid",
                    Value = "验证码错误"
                },
                new ApplicationLanguageText()
                {
                    CreationTime = Clock.Now,
                    Source = SimpleCmsWithAbpConsts.LocalizationSourceName,
                    LanguageName = DefaultLanguageName,
                    Key = "UserManagement",
                    Value = "用户管理"
                },
                new ApplicationLanguageText()
                {
                    CreationTime = Clock.Now,
                    Source = SimpleCmsWithAbpConsts.LocalizationSourceName,
                    LanguageName = DefaultLanguageName,
                    Key = "ArticleManagement",
                    Value = "文章管理"
                },
                new ApplicationLanguageText()
                {
                    CreationTime = Clock.Now,
                    Source = SimpleCmsWithAbpConsts.LocalizationSourceName,
                    LanguageName = DefaultLanguageName,
                    Key = "MediaManagement",
                    Value = "媒体管理"
                },

            };
        }

        public DefaultApplicationLanguageTextCreator(SimpleCmsWithAbpDbContext context)
        {
            _context = context;
        }

        public void Create()
        {
            CreateLanguages();
        }

        private void CreateLanguages()
        {
            foreach (var languageText in InitialLanguageTexts)
            {
                AddLanguageIfNotExists(languageText);
            }
        }

        private void AddLanguageIfNotExists(ApplicationLanguageText languageText)
        {
            if (_context.LanguageTexts.IgnoreQueryFilters().Any(m=>m.LanguageName == languageText.LanguageName && m.Key == languageText.Key ))
            {
                return;
            }

            _context.LanguageTexts.Add(languageText);

            _context.SaveChanges();
        }

    }

完成DefaultApplicationLanguageTextCreator类后,打开InitialHostDbBuilder.cs文件,将以下代码添加到Create方法中:

new DefaultApplicationLanguageTextCreator(_context).Create();

这样,就可通过运行Migrations项目将本地化信息添加到数据库中了。

总体来说,将本地化信息添加到数据库中挺麻烦的,不如直接修改xml文件来得方便。如果不喜欢xml格式的定义,也可以使用JSON格式的定义。在SimpleCmsWithAbpLocalizationConfigurer类中,将XmlEmbeddedFileLocalizationDictionaryProvider替换为JsonEmbeddedFileLocalizationDictionaryProvider就行了,具体的使用可参考文档6.3 ABP表现层 - 本地化

在这里需要考虑一个问题,当本地化资源很多的时候,这样返回本地化资源到客户端是否合适?笔者觉得,在使用Ext JS的时候,可以考虑自定义用户信息返回结果,甚至把Session这部合并在一起,以避免两次获取信息。

权限

在定义菜单的时候,添加了一个权限PermissionNames.Pages_Articles,现在来定义这个权限。打开Core项目的Authorization文件夹下的PermissionNames.cs文件,先添加权限名称,代码如下:

public const string Pages_Articles = "Pages.Articles";

然后打开SimpleCmsWithAbpAuthorizationProvider.cs文件,添加权限,代码如下:

context.CreatePermission(PermissionNames.Pages_Articles, L("Articles"));

好了,权限现在已经添加好了。如果觉得一个权限来处理类别、文章和媒体范围太大了,可以自行定义多个权限。

登出

由于是通过令牌来授权访问的,因而,把令牌清理就相当于退出了,不需要发送请求到服务器进行注销。将主视图的视图控制器内的onLogout方法修改为以下代码就行了:

onLogout: function() {
        HEADERS.setCookies(HEADERS.authTokenCookieName, null, null, null);
        HEADERS.setCookies(HEADERS.encrptedAuthTokenName, null, null, LOCALPATH);
        window.location.reload();
    }

至此,整个登录流程就完成了。

相关文章
相关标签/搜索