在上一篇博客中我们介绍了 AAA 安全框架的概念. 下面我们开始讲述实际项目中 (本篇博客将实现一个多用户的 Todo 列表系统) 如何使用 AAA 安全框架. 在本博客中我们将假设应用使用 MongoDB 来存储数据. 不过关于安全框架应用的部分代码和具体数据库无关.
1 引入项目依赖
在你的 pom.xml
文件中添加 act-aaa 插件依赖:
<dependency>
<groupId>org.actframework</groupId>
<artifactId>act-aaa</artifactId>
<version>${act-aaa.version}</version>
</dependency>
其中 ${act-aaa.version}
是 act-aaa 插件的版本. 截止本文到落笔时的最新版本是 1.0.2
.
**注意: ** 当你的项目加入依赖之后发生的最直接的变化是所有的控制器响应方法都需要身份认证了. 如果你没有进一步做任何工作, 所有的访问都会被返回 401 Unauthorised
响应.
2 处理不需要身份认证的控制器方法
act-aaa 插件默认所有的 HTTP 访问都需要身份认证. 但很明显任何 web 应用都有不需要身份认证的访问结构, 最简单的例子是登陆和注册.
如果某个响应方法不需要身份认证, 可以在该方法上添加 @org.osgl.aaa.NoAuthentication
注解:
@NoAuthentication
public void loginForm() {}
如果某个控制器所有的响应方法都不需要身份认证, 可以在控制器类上添加 @org.osgl.aaa.NoAuthentication
注解:
@NoAuthentication
public class PublicAccessController {
...
}
3 Model 类
3.1 创建一个 UserLinked 接口
UserLinked 接口将被用于检查具体某个被保护的资源是否属于当前正在访问的用户:
package demo.security;
public interface UserLinked {
/**
* Return the user ID
*/
public String userId();
}
3.2 创建 User 类
每个需要身份认证的应用都需要一个 Model 类来对正在和系统交互的用户建模. 下面是一个基本的 User 类代码. 具体应用可以增加自己需要的字段和逻辑
package demo.model;
@Entity("user")
public class User extends MorphiaAdaptiveRecord<User> implements UserLinked {
public String email;
private String password;
@Override
public String userId() {
return this.email;
}
public void setPassword(String password) {
this.password = Act.crypto().passwordHash(password);
}
public boolean verifyPassword(char[] password) {
return Act.crypto().verifyPassword(password, this.password);
}
public static class Dao extends MorphiaDao<User> {
public User findByEmail(String email) {
return findOneBy("email", email);
}
}
}
注意 我们在 User 类中使用了 ActFramework 提供的工具来对密码明文进行哈希运算以及校验操作. 关于这方面更多的知识可以参见此篇博客.
3.3 创建 TODO 类
我们在本篇博客中实现一个多用户的 TODO 列表管理. 下面是一个简单的 TODO Model 类:
package demo.model;
@Entity("todo")
public class Todo extends MorphiaAdaptiveRecord<Todo> implements UserLinked {
/**
* Store the owner's email
*/
public String owner;
@Override
public String userId() {
return this.owner;
}
public static class Dao extends MorphiaDao<Todo> {
public Iterable<Todo> findByOwner(User owner) {
return findBy("owner", owner.email);
}
}
}
4. 配置安全层
4.1 定义权限
AAA 的 API 支持使用两种权限, 第一传入某个实现了 org.osgl.aaa.Permission
的类实例; 第二传入某个字符串. 在此篇博客中我们将使用第二种简单形式. 下面定义在这个 TODO 应用中涉及到的权限:
package demo.security;
public final class TodoPermission {
private TodoPermission() {}
public static final String PERM_CREATE_TODO_ITEM = "create-todo-item";
public static final String PERM_UPDATE_TODO_ITEM = "update-todo-item";
public static final String PERM_VIEW_TODO_ITEM = "view-todo-item";
public static final String PERM_DELETE_TODO_ITEM = "delete-todo-item";
}
上面的 TodoPermission
类主要是为了在应用中方便地使用权限常量. 下面我们将定义一个配置文件来告诉 act-aaa 应用中使用的权限及其特性. 在 src/main/resources/
目录下添加一个名为 acl.yaml
的文件, 内容如下:
create-todo-item:
type: permission
dynamic: false
update-todo-item:
type: permission
dynamic: true
view-todo-item:
type: permission
dynamic: true
delete-todo-item:
type: permission
dynamic: true
注意 在上面的定义中除了 create-todo-item
权限, 其他所有权限的 dynamic
属性均为 true
, 这是因为只有当需要校验 create-todo-item
的时候我们不需要校验数据 (i.e. Todo 实例, 因为还没有创建), 而其他权限都需要检查被访问的数据 (i.e Todo 实例) 是否属于当前用户.
4.2 配置应用安全框架
此步骤将使用一个类来设置 act-aaa 的各种配置:
package demo.security;
/**
* Here we use the generic parameter to tell act-aaa about the user model class
*/
public class TodoSecurity extends ActAAAService.Base<demo.model.User> {
/**
* In this simple Todo app every signed up user get granted
* all of the following permissions
*/
private static final Set<String> DEFAULT_PERMS = C.set(
PERM_CREATE_TODO_ITEM,
PERM_DELETE_TODO_ITEM,
PERM_UPDATE_TODO_ITEM,
PERM_VIEW_TODO_ITEM
);
/**
* We tell act-aaa `email` is the key to extract the user from database
*/
@Override
protected String userKey() {
return "email";
}
/**
* Just return the default permission set
*/
@Override
protected Set<String> permissionsOf(User user) {
return DEFAULT_PERMS;
}
/**
* inject the logic of password verification into act-aaa
*/
@Override
protected boolean verifyPassword(User user, char[] password) {
return user.verifyPassword(password);
}
/**
* This will help to check a protected resource against the current logged in user
* if the permission been authorised is dynamic
*/
public static class DynamicPermissionChecker extends DynamicPermissionCheckHelperBase<UserLinked> {
@Override
public boolean isAssociated(UserLinked userLinked, Principal principal) {
return S.eq(userLinked.userId(), principal.getName());
}
}
}
5. 控制器
5.1 登陆注册控制器
package demo.controller;
public class LoginController {
@Inject
private User.Dao userDao;
@GetAction("/login")
public void loginForm() {
}
@PostAction("/login")
public void login(String email, String password, H.Flash flash, ActionContext context) {
User user = userDao.authenticate(email, password);
if (null == user) {
flash.error("cannot find user by email and password combination");
redirect("/login");
}
context.login(email);
redirect("/");
}
@GetAction("/sign_up")
public void signUpForm() {
}
@PostAction("/sign_up")
public void signUp(User user, ActionContext context, @Provided PostOffice postOffice) {
if (userDao.exists(user.email)) {
context.flash().error("User already exists");
redirect("/sign_up");
}
user.activated = false;
userDao.save(user);
postOffice.sendWelcomeLetter(user);
redirect("/sign_up_ok");
}
@GetAction("/sign_up_ok")
public void signUpConfirm() {
}
@GetAction("/activate")
public void activate(String tk, ActionContext context) {
Token token = Act.crypto().parseToken(tk);
notFoundIfNot(token.isValid());
User user = userDao.findByEmail(token.id());
notFoundIfNull(user);
context.session("tk", tk);
render(user);
}
@PostAction("/activate")
public void completeActivation(String password, ActionContext context) {
String tk = context.session("tk");
notFoundIfNull(tk);
Token token = Act.crypto().parseToken(tk);
notFoundIfNot(token.isValid());
User user = userDao.findByEmail(token.id());
token.consume();
user.setPassword(password);
user.activated = true;
userDao.save(user);
context.login(user.email);
redirect("/");
}
}
该控制器主要实现了下列功能:
- 登陆
- 注册并发送激活邮件
- 响应激活链接请求
- 处理激化请求(初始化密码)
5.2 Todo控制器
@Controller("/todo")
public class TodoController extends AuthenticatedController {
@Inject
private TodoItem.Dao dao;
@GetAction
public Iterable<TodoItem> myItems() {
AAA.requirePermission(me, PERM_VIEW_TODO_ITEM);
return dao.findBy("owner", me.email);
}
@PostAction
public TodoItem add(String subject) {
AAA.requirePermission(me, PERM_CREATE_TODO_ITEM);
TodoItem todoItem = new TodoItem(subject);
todoItem.owner = me.email;
return dao.save(todoItem);
}
@PutAction("{id}")
public TodoItem update(@DbBind("id") TodoItem todo, String subject) {
notFoundIfNull(todo);
AAA.requirePermission(todo, PERM_UPDATE_TODO_ITEM);
todo.subject = subject;
return dao.save(todo);
}
@DeleteAction("{id}")
public void delete(@DbBind("id") TodoItem todo) {
notFoundIfNull(todo);
AAA.requirePermission(todo, PERM_DELETE_TODO_ITEM);
dao.delete(todo);
}
}
该控制器提供操作 TODO 项的 RESTful 服务包括:
- 取当前用户所有的 TODO 项
- 创建新的 TODO 项目
- 修改已有的 TODO 项目
- 删除 TODO 项目
所有的请求均经过授权方予以执行
5.3 AuthenticatedController
public abstract class AuthenticatedController {
@LoginUser
protected User me;
}
提供该控制器是一个推荐用法, 所有需要用户登陆的控制器都可以继承该控制器, 并自动获取当前登陆用户的实例: this.me
. 这是使用了 act-aaa 提供的 @LoginUser
注解, 并由 ActFramework 进行依赖注入的.
总结
本博客讲述了如何在应用中使用 act-aaa 插件, 包括:
- 引入依赖
- 创建应用的 User 类和其他 User 关联类
- 配置应用的 AAA 层
- 处理用户注册登陆以及激活帐号
- 在资源控制器方法上进行授权
本博客的项目代码保存在码云上: https://git.oschina.net/greenlaw110/yatl
参考
- ActFramework 官网
- ActFramework视频
- AAA - ActFramework的安全框架I - 概念及简介
- simple-bbs - AAA 演示项目1
- Yet Another Todo List - AAA 演示项目2