AAA - ActFramework的安全框架II - 应用

上一篇博客中我们介绍了 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("/");
    }

}

该控制器主要实现了下列功能:

  1. 登陆
  2. 注册并发送激活邮件
  3. 响应激活链接请求
  4. 处理激化请求(初始化密码)

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 服务包括:

  1. 取当前用户所有的 TODO 项
  2. 创建新的 TODO 项目
  3. 修改已有的 TODO 项目
  4. 删除 TODO 项目

所有的请求均经过授权方予以执行

5.3 AuthenticatedController

public abstract class AuthenticatedController {
    @LoginUser
    protected User me;
}

提供该控制器是一个推荐用法, 所有需要用户登陆的控制器都可以继承该控制器, 并自动获取当前登陆用户的实例: this.me. 这是使用了 act-aaa 提供的 @LoginUser 注解, 并由 ActFramework 进行依赖注入的.

总结

本博客讲述了如何在应用中使用 act-aaa 插件, 包括:

  1. 引入依赖
  2. 创建应用的 User 类和其他 User 关联类
  3. 配置应用的 AAA 层
  4. 处理用户注册登陆以及激活帐号
  5. 在资源控制器方法上进行授权

本博客的项目代码保存在码云上: https://git.oschina.net/greenlaw110/yatl

参考

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java成神之路

Spring_总结_03_装配Bean(一)之自动装配

(2)当必须要显示配置的时候,再使用类型安全并且比XML更强大的JavaConfig

852
来自专栏编程坑太多

java BufferedImage Graphics 绘制验证码

1313
来自专栏JAVA技术站

Spring整合Rabbitmq

没有找到一篇完整的文章介绍Spring如何整合Rabbitmq应用,琢磨一天搞出的一个入门的demo与伙伴们分享.

392
来自专栏JavaQ

深入Spring Boot (六):使用SpringMVC框架创建Web应用

《深入Spring Boot (一):快速入门》中示例代码使用SpringMVC编写了一个简单的Web应用,本篇将继续使用SpringMVC框架编写复杂的Web...

2868
来自专栏Hongten

java画图程序_图片用字母画出来_源码发布

主要是把一些调试的截图发布出来,现在程序调试我认为可以了(当然,你如果还想调试的话,也可以下载源码自己调试)。

833
来自专栏CodeSheep的技术分享

初探Kotlin+SpringBoot联合编程

Kotlin是一门最近比较流行的静态类型编程语言,而且和Groovy、Scala一样同属Java系。Kotlin具有的很多静态语言特性诸如:类型判断、多范式、扩...

70514
来自专栏Java学习123

一个简单的AXIS远程调用Web Service示例

3267
来自专栏颇忒脱的技术博客

Spring、Spring Boot和TestNG测试指南 - 共享测试配置

我们可以将测试配置放在一个@Configuration里,然后在测试@SpringBootTest或ContextConfiguration中引用它。

612
来自专栏JavaEE

springboot整合shiro(含MD5加密)写在前面:开发环境:项目开始:

3.6K10
来自专栏前端知识分享

第68天:原型prototype方法

构造函数有一个prototype属性,指向实例对象的原型对象。通过同一个构造函数实例化的多个对象具有相同的原型对象。经常使用原型对象来实现继承

852

扫码关注云+社区