【原创】安全框架shiro

Shiro 是当下常见的安全框架,主要用于用户验证和授权操作。

入门

1、shiro.ini
在src目录下新建 shiro.ini,这里面定义了和安全相关的数据:用户,角色和权限

# 定义用户
[users]
# 用户名 = 密码, 角色
zhang3 = 12345, admin
li4 = abcde, productManager
# 定义角色
[roles]
# 角色名 = *
admin = *
productManager = addProduct, deleteProduct,editProduct,updateProduct,listProduct
orderManager = addOrder,deleteOrder,editOrder,updateOrder,listOrder

2、User
准备用户类,用于存放账号密码

public class User {
    private String name;
    private String password;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

3、TestShiro
准备3个用户,前两个能在 shiro.ini 中找到,第3个不存在。然后测试登录,接着测试是否包含角色,最后测试是否拥有权限
注:Subject 在 Shiro 这个安全框架下, Subject 就是当前用户


import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

import javax.management.relation.Role;
import java.util.ArrayList;
import java.util.List;

public class TestShiro {

    public static void main(String[] args) {
        // 注:Subject 在 Shiro 这个安全框架下, Subject 就是当前用户

        // 用户们
        User zhang3 = new User();
        zhang3.setName("zhang3");
        zhang3.setPassword("12345");

        User li4 = new User();
        li4.setName("li4");
        li4.setPassword("abcde");

        User wang5 = new User();
        wang5.setName("wang5");
        wang5.setPassword("nopwd");

        List<User> users = new ArrayList<>();
        users.add(zhang3);
        users.add(li4);
        users.add(wang5);

        // 角色们
        String roleAdmin = "admin";
        String roleProductManager = "productManager";

        List<String> roles = new ArrayList<>();
        roles.add(roleAdmin);
        roles.add(roleProductManager);

        // 权限们
        String permitAddProduct = "addProduct";
        String permitAddOrder = "addOrder";

        List<String> permits = new ArrayList<>();
        permits.add(permitAddProduct);
        permits.add(permitAddOrder);

        // 登录每个用户
        for (User user : users) {
            if (login(user))
                System.out.printf("%s \t成功登陆,用的密码是 %s\t %n",user.getName(),user.getPassword());
            else
                System.out.printf("%s \t成功失败,用的密码是 %s\t %n",user.getName(),user.getPassword());
        }

        System.out.println("-------我是可爱的分割线------");

        // 判断能够登录的用户是否拥有某个角色
        for (User user : users) {
            for (String role : roles) {
                if (login(user)) {
                    if (hasRole(user, role))
                        System.out.printf("%s\t 拥有角色:%s\t%n", user.getName(),role);
                    else
                        System.out.printf("%s\t 不拥有角色:%s\t%n", user.getName(),role);
                }
            }
        }

        System.out.println("-------我是可爱的分割线------");

        // 判断能够登录的用户是否拥有某个权限
        for (User user : users) {
            for (String permit : permits) {
                if (login(user)) {
                    if (isPermitted(user, permit))
                        System.out.printf("%s\t 拥有权限:%s\t%n", user.getName(), permit);
                    else
                        System.out.printf("%s\t 不拥有权限:%s\t%n", user.getName(), permit);
                }
            }
        }

    }

    private static boolean login(User user) {
        Subject subject = getSubject(user);
        // 如果已经登录过来,退出
        if (subject.isAuthenticated())
            subject.logout();

        // 封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword());

        try {
            // 将用户的数据token最终传递到Realm中进行对比
            subject.login(token);
        } catch (AuthenticationException e) {
            return false;
        }

        // 验证错误
        return subject.isAuthenticated();
    }

    private static Subject getSubject(User user) {
        // 加载配置文件,并获取工厂
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        // 获取安全管理者实例
        SecurityManager sm = factory.getInstance();
        // 将安全管理者放入全局对象
        SecurityUtils.setSecurityManager(sm);
        // 全局对象通过安全管理者生产Subject对象
        Subject subject = SecurityUtils.getSubject();
        return subject;
    }

    private static boolean hasRole(User user, String role) {
        Subject subject = getSubject(user);
        return subject.hasRole(role);
    }

    private static boolean isPermitted(User user, String permit) {
        Subject subject = getSubject(user);
        return subject.isPermitted(permit);
    }
}

数据库支持

在 Shiro 入门 中使用ini 配置文件进行了相关权限数据的配置。 但是实际工作中,我们都会把权限相关的内容放在数据库里。

-RBAC 概念

RBAC 是当下权限系统的设计基础,同时有两种解释:
一: Role-Based Access Control,基于角色的访问控制
即,你要能够删除产品,那么当前用户就必须拥有产品经理这个角色
二:Resource-Based Access Control,基于资源的访问控制
即,你要能够删除产品,那么当前用户就必须拥有删除产品这样的权限

1、表结构
基于 RBAC 概念, 就会存在3 张基础表: 用户,角色,权限, 以及 2 张中间表来建立 用户与角色的多对多关系,角色与权限的多对多关系。 用户与权限之间也是多对多关系,但是是通过 角色间接建立的。

注: 补充多对多概念: 用户和角色是多对多,即表示:
一个用户可以有多种角色,一个角色也可以赋予多个用户。
一个角色可以包含多种权限,一种权限也可以赋予多个角色。

这里给出了表结构,导入数据库即可。

DROP DATABASE IF EXISTS shiro;
CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro;

drop table if exists user;
drop table if exists role;
drop table if exists permission;
drop table if exists user_role;
drop table if exists role_permission;

create table user (
  id bigint auto_increment,
  name varchar(100),
  password varchar(100),
  constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB;

create table role (
  id bigint auto_increment,
  name varchar(100),
  constraint pk_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;

create table permission (
  id bigint auto_increment,
  name varchar(100),
  constraint pk_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;

create table user_role (
  uid bigint,
  rid bigint,
  constraint pk_users_roles primary key(uid, rid)
) charset=utf8 ENGINE=InnoDB;

create table role_permission (
  rid bigint,
  pid bigint,
  constraint pk_roles_permissions primary key(rid, pid)
) charset=utf8 ENGINE=InnoDB;

2、表数据
基于 Shiro入门中的shiro.ini 文件,插入一样的用户,角色和权限数据。

INSERT INTO `permission` VALUES (1,'addProduct');
INSERT INTO `permission` VALUES (2,'deleteProduct');
INSERT INTO `permission` VALUES (3,'editProduct');
INSERT INTO `permission` VALUES (4,'updateProduct');
INSERT INTO `permission` VALUES (5,'listProduct');
INSERT INTO `permission` VALUES (6,'addOrder');
INSERT INTO `permission` VALUES (7,'deleteOrder');
INSERT INTO `permission` VALUES (8,'editeOrder');
INSERT INTO `permission` VALUES (9,'updateOrder');
INSERT INTO `permission` VALUES (10,'listOrder');
INSERT INTO `role` VALUES (1,'admin');
INSERT INTO `role` VALUES (2,'productManager');
INSERT INTO `role` VALUES (3,'orderManager');
INSERT INTO `role_permission` VALUES (1,1);
INSERT INTO `role_permission` VALUES (1,2);
INSERT INTO `role_permission` VALUES (1,3);
INSERT INTO `role_permission` VALUES (1,4);
INSERT INTO `role_permission` VALUES (1,5);
INSERT INTO `role_permission` VALUES (1,6);
INSERT INTO `role_permission` VALUES (1,7);
INSERT INTO `role_permission` VALUES (1,8);
INSERT INTO `role_permission` VALUES (1,9);
INSERT INTO `role_permission` VALUES (1,10);
INSERT INTO `role_permission` VALUES (2,1);
INSERT INTO `role_permission` VALUES (2,2);
INSERT INTO `role_permission` VALUES (2,3);
INSERT INTO `role_permission` VALUES (2,4);
INSERT INTO `role_permission` VALUES (2,5);
INSERT INTO `role_permission` VALUES (3,6);
INSERT INTO `role_permission` VALUES (3,7);
INSERT INTO `role_permission` VALUES (3,8);
INSERT INTO `role_permission` VALUES (3,9);
INSERT INTO `role_permission` VALUES (3,10);
INSERT INTO `user` VALUES (1,'zhang3','12345');
INSERT INTO `user` VALUES (2,'li4','abcde');
INSERT INTO `user_role` VALUES (1,1);
INSERT INTO `user_role` VALUES (2,2);

3、User

public class User {
    private int id;
    private String name;
    private String password;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
}

4、DAO
这个DAO提供了和权限相关查询。 但是,并没有提供权限数据本身的维护。 比如没有做用户的增删改,角色和权限表也没有。 因为那些在提供了 表数据 的基础上,就不是必须的了。
为了专注于 Shiro 和 DAO 的结合,只提供必要的数据库操作支持。

import java.sql.*;
import java.util.HashSet;
import java.util.Set;

public class DAO {
    public DAO() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8", "root",
                "admin");
    }

    // 根据用户名查询密码,这样既能判断用户是否存在,也能判断密码是否正确
    public String getPassword(String userName) {
        String sql = "select password from user where name = ?";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {

            ps.setString(1, userName);

            ResultSet rs = ps.executeQuery();

            if (rs.next())
                return rs.getString("password");

        } catch (SQLException e) {

            e.printStackTrace();
        }
        return null;
    }

    // 根据用户名查询此用户有哪些角色,这是3张表的关联
    public Set<String> listRoles(String userName) {

        Set<String> roles = new HashSet<>();
        String sql = "select r.name from user u "
                + "left join user_role ur on u.id = ur.uid "
                + "left join Role r on r.id = ur.rid "
                + "where u.name = ?";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
            ps.setString(1, userName);
            ResultSet rs = ps.executeQuery();

            while (rs.next()) {
                roles.add(rs.getString(1));
            }

        } catch (SQLException e) {

            e.printStackTrace();
        }
        return roles;
    }

    // 根据用户名查询此用户有哪些权限,这是5张表的关联
    public Set<String> listPermissions(String userName) {
        Set<String> permissions = new HashSet<>();
        String sql =
                "select p.name from user u "+
                        "left join user_role ru on u.id = ru.uid "+
                        "left join role r on r.id = ru.rid "+
                        "left join role_permission rp on r.id = rp.rid "+
                        "left join permission p on p.id = rp.pid "+
                        "where u.name =?";

        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {

            ps.setString(1, userName);

            ResultSet rs = ps.executeQuery();

            while (rs.next()) {
                permissions.add(rs.getString(1));
            }

        } catch (SQLException e) {

            e.printStackTrace();
        }
        return permissions;
    }

    public static void main(String[] args) {
        DAO dao = new DAO();
        System.out.println(dao.listRoles("zhang3"));
        System.out.println(dao.listRoles("li4"));
        System.out.println(dao.listPermissions("zhang3"));
        System.out.println(dao.listPermissions("li4"));
    }

}

-Realm 概念

当应用程序向 Shiro 提供了 账号和密码之后, Shiro 就会问 Realm 这个账号密码是否对, 如果对的话,其所对应的用户拥有哪些角色,哪些权限。
所以Realm 是什么? 其实就是个中介。 Realm 得到了 Shiro 给的用户和密码后,有可能去找 ini 文件,就像Shiro 入门中的 shiro.ini,也可以去找数据库,就如同本知识点中的 DAO 查询信息。
Realm 就是干这个用的,它才是真正进行用户认证和授权的关键地方。

5、DatabaseRealm
DatabaseRealm 就是用来通过数据库 验证用户,和相关授权的类。
两个方法分别做验证和授权:doGetAuthenticationInfo(), doGetAuthorizationInfo()

注: DatabaseRealm 这个类,用户提供,但是不由用户自己调用,而是由 Shiro 去调用。 就像Servlet的doPost方法,是被Tomcat调用一样。

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.Set;

public class DatabaseRealm extends AuthorizingRealm {
    DAO dao = new DAO();

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 能进入这里表示账号已经通过验证了
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 通过DAO获取角色和权限
        Set<String> permissions = dao.listPermissions(userName);
        Set<String> roles = dao.listRoles(userName);

        // 授权对象
        SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
        // 把通过DAO获取到的角色和权限放进去
        s.setStringPermissions(permissions);
        s.setRoles(roles);

        return s;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取账号密码
        UsernamePasswordToken t = (UsernamePasswordToken) authenticationToken;
        String username = authenticationToken.getPrincipal().toString();
        String password = new String(t.getPassword());

        // 获取数据库中的密码
        String passwordInDB = dao.getPassword(username);

        // 如果为空表示账号不存在,不相同表示密码错误,但是都抛出AuthenticationException而不是抛出具体原因,免得给破解者提供帮助信息
        if (null == passwordInDB || !passwordInDB.equals(password))
            throw new AuthenticationException();

        // 认证信息里存放账号密码,getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
        SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(username, password, getName());

        return a;
    }
}

6、shiro.ini

[main]
databaseRealm = DatabaseRealm
securityManager.realms = $databaseRealm

7、TestShiro不变

8、关于JdbcRealm
Shiro 提供了一个 JdbcRealm,它会默认去寻找 users, roles, permissions 三张表做类似于 DAO 中的查询。
但是本例没有使用它,因为实际工作通常都会有更复杂的权限需要,以上3张表不够用。 JdbcRealm 又封装得太严实了,连它执行了 SQL 语句这件事,我都是很久才明白过来,这样不利于初学者消化, 最后 使用 DAO 更符合开发者的习惯,也觉得一切都在自己掌握中,用起来心里更踏实一些。

md5加密

-盐salt

每次 123 md5 之后都是202CB962AC59075B964B07152D234B70,但是 我加上盐,即 123+随机数,那么md5值不就不一样了吗? 这个随机数,就是盐

1、数据库加字段salt
alter table user add (salt varchar(100) )

2、DAO

    // createUser 用于注册,并且在注册的时候,将用户提交的密码加密
    public String createUser(String name, String password) {
        String sql = "insert into user values(null,?,?,?)";
        String salt = new SecureRandomNumberGenerator().nextBytes().toString();
        String encodedPassword = new SimpleHash("md5", password, salt, 2).toString();

        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {

            ps.setString(1, name);
            ps.setString(2, encodedPassword);
            ps.setString(3, salt);
            ps.execute();
        } catch (SQLException e) {

            e.printStackTrace();
        }
        return null;
    }

    // getUser 用于取出用户信息,其中不仅仅包括加密后的密码,还包括盐
    public User getUser(String userName) {
        User user = null;
        String sql = "select * from user where name = ?";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {

            ps.setString(1, userName);

            ResultSet rs = ps.executeQuery();

            if (rs.next()) {
                user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
                user.setSalt(rs.getString("salt"));
            }

        } catch (SQLException e) {

            e.printStackTrace();
        }
        return user;
    }

3、DatabaseRealm
修改 DatabaseRealm,把用户通过 UsernamePasswordToken 传进来的密码,以及数据库里取出来的 salt 进行加密,加密之后再与数据库里的密文进行比较,判断用户是否能够通过验证。

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.Set;

public class DatabaseRealm extends AuthorizingRealm {
    DAO dao = new DAO();

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 能进入这里表示账号已经通过验证了
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 通过DAO获取角色和权限
        Set<String> permissions = dao.listPermissions(userName);
        Set<String> roles = dao.listRoles(userName);

        // 授权对象
        SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
        // 把通过DAO获取到的角色和权限放进去
        s.setStringPermissions(permissions);
        s.setRoles(roles);

        return s;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取账号密码
        UsernamePasswordToken t = (UsernamePasswordToken) authenticationToken;
        String username = authenticationToken.getPrincipal().toString();
        String password = new String(t.getPassword());

        // 获取数据库中的密码
        User user = dao.getUser(username);
        String passwordInDB = user.getPassword();
        String salt = user.getSalt();
        String passwordEncoded = new SimpleHash("md5", password, salt, 2).toString();

        // 如果为空表示账号不存在,不相同表示密码错误,但是都抛出AuthenticationException而不是抛出具体原因,免得给破解者提供帮助信息
        if (null == user || !passwordEncoded.equals(passwordInDB))
            throw new AuthenticationException();

        // 认证信息里存放账号密码,getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
        SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(username, password, getName());

        return a;
    }
}

猜你喜欢

转载自www.cnblogs.com/acuii/p/9904849.html