登录成功后,服务器会返回访问令牌(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.Cookies
的set
方法时使用了全部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.js
的init
方法的最后加入以下代码来获取登录信息了:
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();
对于菜单的本地化工作,可以直接修改Core项目中Localization\SourceFiles
文件夹下的文件,也可以添加到数据库中。
为了便于将本地化信息添加到数据库,我们可以在EntityFrameworkCore项目中的EntityFrameworkCore\Seed\Host
中添加一个名为DefaultApplicationLanguageTextCreator
的类,具体代码如下:
public class DefaultApplicationLanguageTextCreator
{
public static List InitialLanguageTexts => GetInitialLanguageTexts();
private readonly SimpleCmsWithAbpDbContext _context;
private const string DefaultLanguageName = "zh-CN";
private static List GetInitialLanguageTexts()
{
return new List
{
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();
}
至此,整个登录流程就完成了。