初識Shiro

Shiro是Apache基金會下的一個開源安全框架,提供了身份驗證、受權、密碼學和會話管理等功能,Shiro框架不只直觀易用,並且也能提供健壯的安全性,另一點值得說的是Shiro的前身是一個始於2004的開源項目JSecurity,該項目於2008年加入Apache,並於2010年成爲Apache的頂級項目。
OK,以上是關於Shiro的一點簡單介紹,實際上,我在以前有一篇關於權限控制的博客在Spring Boot中使用Spring Security實現權限控制,Shiro的功能沒有Spring Security強大,可是對於通常的項目而言也是夠用了,並且用起來要更簡單一些。OK,關於這兩個孰優孰劣,咱們不去爭論,咱們這裏主要來看Shiro的使用。java

三個核心

Subject

在Shiro中,任意一個和程序交互的主體都是一個Subject,Subject包括可是不限於用戶,全部的Subject都綁定到SecurityManager上 ,全部交由Subject處理的操做最終都會委託給SecurityManager。mysql

SecurityManager

SecurityManager單從字面上咱們能夠理解爲一個安全管理器,全部的Subject都由SecurityManager來管理,它是Shiro的核心,SecurityManager有點相似於Spring框架中的DispatcherServlet。git

Realm

Shiro在運行的過程當中,從Realm中獲取安全數據,好比用戶的權限、角色等,每當SecurityManager要驗證用戶身份的時候,那麼他就從Realm中獲取相應的數據進行比對,這個有點相似於DAO,由它提供數據源。github

經過上面的簡單介紹,咱們能夠大體梳理一下一個Shiro應用的流程:
代碼經過Subject來進行認證和受權等操做,而Subject又將這個操做委託給SecurityManager,咱們將要驗證的數據源注入到Realm中,SecurityManager在Realm中查詢數據進行驗證。sql

登陸操做

OK,經過以上的介紹,相信小夥伴對Shiro已經有了一個基本的認識,接下來咱們來看看幾個基本的操做。數據庫

建立工程並添加依賴

我這裏使用IntelliJ IDEA做爲開發工具,咱們先來建立一個基本的Maven工程,而後添加以下依賴:apache

<dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>1.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.3.2</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
    </dependency>

單元測試框架、日誌框架和一個shiro-core三個東西就足夠了。後端

經過ini問價建立數據源

咱們這裏先經過ini文件建立一個簡單的數據源,注意文件的位置:
這裏寫圖片描述
OK,shiro.ini文件的內容以下:安全

[users]
zuolengchan=456

注意上面是users,有s,users表示對用戶、密碼、角色等的配置。咱們這裏提供了一個用戶名爲zuolengchan,密碼爲456的用戶。markdown

測試

OK,接下來,咱們經過一個簡單的單元測試來看看怎麼登陸,以下:

@Test
    public void test1() {
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zuolengchan", "456");
        try {
            subject.login(token);
            System.out.println("登陸成功");
        } catch (AuthenticationException e) {
            //登陸失敗
            System.out.println("登陸失敗");
            e.printStackTrace();
        }
        subject.logout();
    }

關於以上測試文件,我說如下幾點:

1.首先咱們須要獲取SecurityManager工廠,獲取的時候經過ini配置文件來初始化。
2.經過SecurityManager工廠獲取到SecurityManager的實例,並將該實例綁定給SecurityUtils,設置給SecurityUtils是一個全局設置,設置一次便可。
3.從SecurityUtils中獲取到一個Subject實例
4.經過UsernamePasswordToken對象建立用戶名密碼身份驗證Token
5.調用Subject中的login方法執行登陸操做,Subject會將這個登陸操做自動委託給SecurityManager去執行。
6.在登陸過程當中,若是沒有拋異常,說明登陸成功,若是拋異常,說明登陸失敗
7.調用logout方法咱們能夠退出登陸,退出登陸的操做也是委託給SecurityManager去執行。

OK,咱們在登陸的過程當中,輸入正確的用戶名和密碼就能成功登陸。

自定義Realm

OK,上個案例中,咱們在ini文件中預設了數據源,固然咱們也能夠自定義Realm,前面咱們也說過Realm至關因而咱們的數據源,咱們能夠在Realm中來進行數據匹配,自定義Realm須要咱們實現Realm接口,該接口中有三個方法須要咱們實現:

1.getName()方法用來返回該Realm的惟一名字。
2.supports(AuthenticationToken token)方法用來驗證傳入的Token是否被支持,由於咱們驗證方式不只僅Username和Password這種方式,還有不少其餘方式。
3.getAuthenticationInfo(AuthenticationToken token)這個方法用來執行驗證操做,而且將驗證結果返回。

OK,有了上面的介紹,接下來咱們來實現一個Realm。

定義Realm

public class MyRealm1 implements Realm {

    public String getName() {
        return "myrealm1";
    }

    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof UsernamePasswordToken;
    }

    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        String password = new String(((char[]) token.getCredentials()));
        if (!"wang".equals(username)) {
            //用戶名錯誤
            throw new UnknownAccountException();
        }
        if (!"123".equals(password)) {
            //密碼錯誤
            throw new IncorrectCredentialsException();
        }
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}

getName()方法返回了當前Realm的名字,supports方法判斷是不是Username+Password的驗證,getAuthenticationInfo方法中進行驗證,首先獲取到傳入的用戶名和密碼,而後進行比對,若是不存在用戶名就拋出UnknownAccountException異常,若是密碼錯誤則拋出IncorrectCredentialsException異常,最後將結果返回。

定義ini文件

這裏的ini文件就比較簡單了,以下:

myRealm1=org.sang.MyRealm1
securityManager.realms=$myRealm1

首先定義myRealm1,值爲咱們自定義Realm的全路徑,而後設置securityManager的realms屬性便可,OK,作完這些咱們就能夠測試啦。

測試

@Test
    public void test2() {
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhang", "456");
        try {
            subject.login(token);
            System.out.println("登陸成功");
        } catch (AuthenticationException e) {
            //登陸失敗
            System.out.println("登陸失敗");
            e.printStackTrace();
        }
        subject.logout();
    }

若是咱們在登陸的時候輸入正確的用戶名和密碼就可以成功登陸,若是有任意一個輸錯,則會拋出相應的異常。

定義多個Realm

OK,以上是咱們自定義一個Realm,事實上咱們能夠自定義多個,以下:

public class MyRealm2 implements Realm {

    public String getName() {
        return "myrealm2";
    }

    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof UsernamePasswordToken;
    }

    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        String password = new String(((char[]) token.getCredentials()));
        if (!"zhang".equals(username)) {
            //用戶名錯誤
            throw new UnknownAccountException();
        }
        if (!"456".equals(password)) {
            //密碼錯誤
            throw new IncorrectCredentialsException();
        }
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}

自定義多個Realm以後須要咱們在ini文件中配置多個,以下:

myRealm1=org.sang.MyRealm1
myRealm2=org.sang.MyRealm2
securityManager.realms=$myRealm1,$myRealm2

這裏若是咱們不寫最後 一行也是能夠的,Shiro會自動將前面定義的myRealm1,myRealm2設置給SecurityManager的realms屬性。
OK,這裏的測試方式和上文一致,再也不贅述。

JdbcRealm

定義數據庫

上面的數據源都是咱們提早定義好,寫死的,固然咱們也能夠將數據定義在數據庫中,好比,我建立以下三張表,並預設一條數據:

drop database if exists shiro1;
create database shiro1 default character set=utf8 collate utf8_bin;
use shiro1;
create table users( id bigint auto_increment, username varchar(100), password varchar(100), password_salt varchar(100), constraint pk_users primary key(id) )charset=utf8;
create unique index idx_users_username on users(username);
create table user_roles( id bigint auto_increment, username varchar(100), role_name varchar(100), constraint pk_user_roles primary key(id) )charset=utf8;
create unique index idx_user_roles on user_roles(username,role_name);
create table roles_permissions( id bigint auto_increment, role_name varchar(100), permission varchar(100), constraint pk_roles_permissions primary key(id) )charset=utf8;
create unique index idx_roles_permissions on roles_permissions(role_name,permission);
insert into users(username,password) values('wang','111');

添加相關的依賴

而後在Maven中添加數據庫驅動和鏈接池:

<dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.40</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.0.29</version>
    </dependency>

添加ini文件

在ini文件中配置JdbcRealm,以下:

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiro1?useUnicode=true&characterEncoding=UTF-8
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
securityManager.realm=$jdbcRealm

jdbcRealm定義了咱們要用的JdbcRealm,在最後將之設置給SecurityManager的realm屬性,jdbcRealm中還有dataSource,就是咱們自定義的數據源,其餘的都是數據庫鏈接的東西,我就再也不贅述。

測試

@Test
    public void test3() {
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-jdbc-realm.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("wang", "111");
        try {
            subject.login(token);
            System.out.println("登陸成功");
        } catch (AuthenticationException e) {
            //登陸失敗
            System.out.println("登陸失敗");
            e.printStackTrace();
        }
        subject.logout();
    }

測試方式仍是和前文一致。再也不囉嗦。

自定義驗證策略

Shiro中有三種不一樣的驗證策略,以下:

1.FirstSuccessfulStrategy:表示只要有一個Realm驗證成功便可,而後返回第一個Realm身份驗證成功的認證信息,其餘的忽略。
2.AtLeastOneSuccessfulStrategy:表示只要有一個Realm驗證成功便可,可是它會將全部的成功驗證成功的信息返回。
3.AllSuccessfulStrategy:表示全部的Realm驗證成功纔算成功,且返回全部Realm身份驗證成功的認證信息。

Shiro默認使用了第二種策略。

OK,假設我如今有三個Realm,分別以下:

public class MyRealm3 implements Realm {

    public String getName() {
        return "myrealm3";
    }

    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof UsernamePasswordToken;
    }

    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        String password = new String(((char[]) token.getCredentials()));
        if (!"zhang".equals(username)) {
            //用戶名錯誤
            throw new UnknownAccountException();
        }
        if (!"123".equals(password)) {
            //密碼錯誤
            throw new IncorrectCredentialsException();
        }
        return new SimpleAuthenticationInfo(username, password, getName());
    }


}
public class MyRealm4 implements Realm {

    public String getName() {
        return "myrealm4";
    }

    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof UsernamePasswordToken;
    }

    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        String password = new String(((char[]) token.getCredentials()));
        if (!"wang".equals(username)) {
            //用戶名錯誤
            throw new UnknownAccountException();
        }
        if (!"123".equals(password)) {
            //密碼錯誤
            throw new IncorrectCredentialsException();
        }
        return new SimpleAuthenticationInfo(username, password, getName());
    }


}
public class MyRealm5 implements Realm {

    public String getName() {
        return "myrealm5";
    }

    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof UsernamePasswordToken;
    }

    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        String password = new String(((char[]) token.getCredentials()));
        if (!"zhang".equals(username)) {
            //用戶名錯誤
            throw new UnknownAccountException();
        }
        if (!"123".equals(password)) {
            //密碼錯誤
            throw new IncorrectCredentialsException();
        }
        return new SimpleAuthenticationInfo("zhang@163.com", password, getName());
    }


}

而後在ini文件中配置驗證策略:

#指定securityManager的authenticator實現
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
#驗證策略
allSuccessfulStrategy= org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy

myRealm3=org.sang.MyRealm3
myRealm4=org.sang.MyRealm4
myRealm5=org.sang.MyRealm5
securityManager.realms=$myRealm3,$myRealm5,$myRealm4

這個表示我一會驗證的時候,驗證的條件只要有一個知足便可驗證成功,而且在驗證成功後系統會返回全部的驗證信息給我。
固然,我也能夠給allSuccessfulStrategy設置另外兩種值,以下:

allSuccessfulStrategy= org.apache.shiro.authc.pam.AllSuccessfulStrategy
allSuccessfulStrategy= org.apache.shiro.authc.pam.FirstSuccessfulStrategy

OK,咱們來看看測試代碼:

@Test
    public void test4() {
        login("classpath:shiro-authenticator-all-success.ini");
        Subject subject = SecurityUtils.getSubject();
        PrincipalCollection principals = subject.getPrincipals();
        Iterator iterator = principals.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

    private void login(String configFile) {
        Factory<SecurityManager> factory = new IniSecurityManagerFactory(configFile);
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("wang", "123");
        try {
            subject.login(token);
            System.out.println("登陸成功");
        } catch (AuthenticationException e) {
            System.out.println("登陸失敗");
            e.printStackTrace();
        }
    }

登陸成功以後將全部的認證信息打印出來。

OK,以上就是Shiro的一個簡單應用。

本文案例地址:
https://github.com/lenve/Shiro

更多JavaEE資料請移步這裏:
https://github.com/lenve/JavaEETest

參考資料:
張開濤大神的《跟我學Shiro》,原文鏈接http://jinnianshilongnian.iteye.com/blog/2018398

關注公衆號【江南一點雨】,專一於 Spring Boot+微服務以及先後端分離等全棧技術,按期視頻教程分享,關注後回覆 Java ,領取鬆哥爲你精心準備的 Java 乾貨!

相關文章
相關標籤/搜索