Apache Shiro 教程--官网

Apache Shiro 教程

第一个Apache Shiro应用

如果你是第一次接触Apache Shiro,这个简短的教程将会通过一个非常简单的项目教你如何使用Apache Shiro,并将会讨论Shiro的核心概念,帮助你熟悉Shiro的设计和API

如果你不想自己编写源文件,可以通过Git获取相关源代码:https://github.com/apache/shiro/tree/master/samples/quickstart

Setup

在这个简单的例子中,将会创建一个非常简单的命令行应用程序,熟悉一下Shiro的API

Apache Shiro从一开始就被设计为支持任何应用程序——从最小的命令行应用程序到集群web应用程序。
本教程是一个简单的应用程序,但是无论应用程序是如何创建的或部署在哪里,使用模式都是相同的。

本教程要求Java的版本不低于1.6 ,使用Maven作为构建工具,但是Maven对于Shiro来说并不是必须的,可以使用Shiro的jar文件,或者Ant,Ivy集成到你的应用程序中

本教程要求Maven版本至少为2.2.1,请在命令行使用mvn --version确保maven的版本

获取maven版本

hazlewood:~/shiro-tutorial$ mvn --version
Apache Maven 2.2.1 (r801777; 2009-08-06 12:16:01-0700)
Java version: 1.6.0_24
Java home: /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
Default locale: en_US, platform encoding: MacRoman
OS name: "mac os x" version: "10.6.7" arch: "x86_64" Family: "mac"

现在,创建一个新目录,这里以Tutorial为例,将下面的pom.xml文件保存到这个新目录中

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>org.apache.shiro.tutorials</groupId>
    <artifactId>shiro-tutorial</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>First Apache Shiro Application</name>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>

        <!-- 这个插件仅仅用于测试,并不是必须的 -->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <classpathScope>test</classpathScope>
                    <mainClass>Tutorial</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.1</version>
        </dependency>
        <!-- Shiro使用SLF4j来进行日志记录,这里使用slf4j-simple,详情请见http://www.slf4j.org for more info. -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.21</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.21</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

现在的目录结构如下所示:

image-20210702002326446.png

Tutorial 类

本教程将要演示一个简单的命令行程序,所以首先需要创建一个具有main方法的类

在和上述pom.xml文件的同级目录下,创建一个src/main/java子目录,在src/main/java目录中创建一个Tutorial.java文件,该Java文件内容如下:

src/main/java/Tutorial.java

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Tutorial {

    private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);

    public static void main(String[] args) {
        log.info("My First Apache Shiro Application");
        System.exit(0);
    }
}

先别关注上面的import语句,后面介绍他们。现在已经创建了一个典型的命令行程序,它只会输出文本“My First Apache Shiro Application”然后退出

现在的目录结构如下所示:

image-20210702002451827.png

测试运行

现在,在命令行中把工作目录切换到项目根目录,即pom.xml所在的目录,然后输入如下命令:

mvn compile exec:java

你将会看到你的程序运行并退出,类似下面的结果

lhazlewood:~/projects/shiro-tutorial$ mvn compile exec:java

... 一大堆Maven的输出 ...

1 [Tutorial.main()] INFO Tutorial - My First Apache Shiro Application
lhazlewood:~/projects/shiro-tutorial$

上述程序运行成功,接下来在这个项目中使用Apache Shiro,在代码更改之后,可以继续使用mvn compile exec:java来看更改的结果

启动 Shiro

需要注意的是,Shiro中的一切都和一个核心组件相关,那就是SecurityManager,但这和Java中的java.lang.SecurityManager是不一样的

后面会详细介绍Shiro的架构设计,在每个应用中,至少得存在一个Security Manager的实例,因此,接下来在Toturial项目中设置一个SecurityManager实例

配置介绍

Shiro的SecurityManager相关实现有着足够多的配置选项和内部组件,这使得通过基于文本的配置格式来配置SecurityManager是非常简单的

为此,Shiro通过基于文本的INI配置提供了一个默认的通用解决方案。INI易于阅读和使用,且不需要太多的依赖。通过对象图导航,可以有效地使用INI配置简单的对象图,如SecurityManager

Shiro的SecurityManager实现和所有的Shiro组件都是JavaBean形式的
这允许Shiro可以通过XML,YAML,JSON等等格式进行配置
INI配置是Shiro的通用配置方案,以防止其他选择不可用的时候
shiro.ini

接下来在项目中,通过INI文件来配置Shiro的SecurityManager,首先,在和pom.xml同级目录下创建一个src/main/resources目录,然后在src/main/resources子目录下创建shiro.ini文件,文件内容如下:

src/main/resources/shiro.ini

# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
# =============================================================================

# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

如上所示,在INI配置文件中,设置了一个少量的用户集合,之后会介绍更复杂的用户数据源,如关系型数据库,LDAP,等等

通过 ini 配置 SecurityManager

现在将创建一个SecurityManager实例,相关代码如下所示:

public static void main(String[] args) {

    log.info("My First Apache Shiro Application");

    //1.
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");

    //2.
    SecurityManager securityManager = factory.getInstance();

    //3.
    SecurityUtils.setSecurityManager(securityManager);

    System.exit(0);
}

在上面标注的三行代码之后,Shiro就可以在程序中使用了,通过运行mvn compile exec:java,就能看到所有的东西仍然成功运行,由于Shiro默认的日志级别是debug或者更低,因此会看到很多Shiro的日志信息,如果运行没有错误,那就是运行成功了

上面标注的3行代码作用如下:

  1. 通过我们之前创建的shiro.ini文件,来构造一个IniSecurityManagerFactory实例,classpath:前缀是一个资源标识符,它来告诉Shiro要去哪里寻找ini文件,其他的前缀如url:file:
  2. factory.getInstance()方法将会解析ini文件,通过反射的方式,构造并返回一个SecurityManger实例
  3. 在这个例子中,以静态单例的方式在JVM中设置了一个SecurityManager,但是请注意,如果在单个JVM中有多个启用Shiro的应用程序,那么这就不可取了。在更复杂的应用中,会把SecurityManager放在应用特有的内存中,如Web的ServletContent,Spring容器等等

使用 Shiro

在程序中,最常用的安全相关问题就是“当前用户是谁”“当前用户是否允许执行某操作”,这和写代码或设计用户接口是一样的:应用是基于用户故事构建的,我们希望对不同的用户展现不同的功能。因此,当前用户是谁,在进行安全保护时是一个核心问题。Shiro中,把当前用户称之为Subject(主体)

在几乎所有的环境中,可以通过下面的代码来获取当前用户:

Subject currentUser = SecurityUtils.getSubject();

通过SecurityUtils.getSubject(),我们可以获取当前执行的主体,主体是一个安全术语,它意味着“当前执行用户的特定于安全的视图”。之所以不把他称之为用户,是因为用户总会和一个人相关,而主体可以是人,可以是第三方进程,可以是一个定时任务等等,主体表示“当前和软件进行交互的东西”。在大多数应用中,可以将主体和用户的概念等同起来

在一个独立的应用程序中,getSubject()调用可能会根据应用程序特定位置的用户数据返回一个Subject,而在一个服务器环境中(例如web应用程序),它会根据与当前线程或传入请求相关的用户数据获取Subject

如果你想要某些东西在用户的会话过程中生效,可以通过如下代码获取会话:

Session session = currentUser.getSession();
session.setAttribute( "someKey", "aValue" );

这个API的方式和HttpSession很像,但是它不需要HTTP的环境。在Web应用程序中,Shiro的Session默认就是HttpSession,但是在非Web环境中,Shiro将会使用企业会话管理。这意味着不管在哪一层,哪个环境中,都可以使用同样的API。任何需要会话的应用程序现在都不需要强制使用HttpSession或EJB Stateful Session Bean,任何客户端技术现在都可以共享会话数据。

目前,用户是匿名的,因为我们还没有进行登录,现在来演示登录

if ( !currentUser.isAuthenticated() ) {
    //collect user principals and credentials in a gui specific manner
    //such as username/password html form, X509 certificate, OpenID, etc.
    //We'll use the username/password example here since it is the most common.
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");

    //this is all you have to do to support 'remember me' (no config - built in!):
    token.setRememberMe(true);

    currentUser.login(token);
}

如果登录失败,login操作将会抛出异常,可以捕获这些特定的异常来了解具体发生了什么,来进行处理:

try {
    currentUser.login( token );
    //if no exception, that's it, we're done!
} catch ( UnknownAccountException uae ) {
    //username wasn't in the system, show them an error message?
} catch ( IncorrectCredentialsException ice ) {
    //password didn't match, try again?
} catch ( LockedAccountException lae ) {
    //account for that username is locked - can't login.  Show them a message?
}
    ... more types exceptions to check if you want ...
} catch ( AuthenticationException ae ) {
    //unexpected condition - error?
}

可以检查许多不同类型的异常,或者针对Shiro可能没有考虑到的自定义条件抛出自己的异常。参考Shiro的AuthenticationException

保障安全的一个手段是向用户提供通用的登录失败消息,因为没有人会想帮助试图侵入系统的攻击者

可以通过添加如下代码,获知是谁登录了系统

//print their identifying principal (in this case, a username): 
log.info( "User [" + currentUser.getPrincipal() + "] logged in successfully." );

下面的代码演示了如何检测主体是否拥有某个特定的角色

if ( currentUser.hasRole( "schwartz" ) ) {
    log.info("May the Schwartz be with you!" );
} else {
    log.info( "Hello, mere mortal." );
}

也可以查看主体是否拥有对某个实体类型执行某个操作的权限

if ( currentUser.isPermitted( "lightsaber:wield" ) ) {
    log.info("You may use a lightsaber ring.  Use it wisely.");
} else {
    log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

还可以进行极度强大的实例级别的权限检测,即检测主体都某个具体的实体是否有某个操作权限:

if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {
    log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'.  " +
                "Here are the keys - have fun!");
} else {
    log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}

最后,用户登出:

currentUser.logout(); //removes all identifying information and invalidates their session too.

最终的Tutorial类

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Tutorial {

    private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);

    public static void main(String[] args) {
        log.info("My First Apache Shiro Application");

        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        // get the currently executing user:
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true);
            try {
                currentUser.login(token);
            } catch (UnknownAccountException uae) {
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //test a typed permission (not instance-level)
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //a (very powerful) Instance Level permission:
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //all done - log out!
        currentUser.logout();

        System.exit(0);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355