1. 程式人生 > >【原創】安全框架shiro

【原創】安全框架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;
    }
}