zoukankan      html  css  js  c++  java
  • Apache Shiro 1.3.2入门

    简介
    Apache Shiro是一个功能强大且灵活的开放源代码安全框架,可以清楚地处理认证,授权,企业会话管理和加密。Apache Shiro的首要目标是易于使用和理解。有时候安全性可能非常复杂和痛苦,框架应该尽可能地掩盖复杂性,并对外公开干净且直观的API,以简化开发人员为了确保其应用程序安全的工作。
    以下是您可以使用Apache Shiro完成的一些事情:
    1.认证用户以证实其身份。
    2.为用户执行访问控制,例如确定是否为用户分配了某个安全角色或者确定用户是否允许执行某项操作。
    3.在任何环境中使用Session API,即使没有Web或EJB容器,例如JavaSE环境下也可以使用。
    4.在认证,访问控制或会话生命周期中响应事件。
    5.汇总一个或多个用户安全数据的数据源,并将其作为单个复合用户“视图”呈现。
    6.启用单点登录(SSO)功能
    7.为用户关联启用'记住我'服务,无需登录
    ...
    等等 - 所有这些都集成到一个紧密结合的易用API中。
    Shiro试图为所有应用程序环境实现这些目标 - 从最简单的命令行应用程序到最大的企业应用程序,而不强制依赖于其他第三方框架,容器或应用程序服务器。当然该项目旨在尽可能融入这些环境中,但可以在任何环境中开箱即用。
     
    Apache Shiro是一个具有许多功能的综合应用程序安全框架。重点功能点如下图:
    Shiro的目标是Shiro开发团队称之为“应用程序安全的四大基石” - 认证,授权,会话管理和加密:
    Authentication:有时被称为“登录”,这是证实用户是他们所说的人的行为。
    Authorization:访问控制的过程,即确定“谁”有权限做“什么”。
    Session Manager:即使在非Web或EJB应用程序中,也可以管理用户特定的会话。
    Cryptography:使用加密算法保持数据安全,同时仍然易于使用。
    还有其他特性可以在不同的应用环境中支持和加强这些关注点,特别是:
    Web Support:Shiro的Web支持API可帮助轻松保护Web应用程序。
    Caching:缓存是Apache Shiro API中的第一级公民,可确保安全操作快速高效。
    Concurrency:Apache Shiro支持多线程应用程序。
    Testing:测试支持的存在是用来帮助您编写单元和集成测试,并确保您的代码和预期一样的安全。
    Run As:允许用户假装为其他用户身份(如果允许)的功能,有时在管理场景中很有用。
    Remember Me:在会话中记住用户的身份,这样他们只需在强制登录时才需要登录。
     
    术语
    Authentication
    认证是证实Subject(见下文)身份的过程,从本质上证明某人确实是他们所说的人,当尝试认证成功时,应用程序可以相信Subject保证是应用程序期望的。
     
    Authorization
    授权也称为访问控制,是确定用户-Subject(见下文)是否被允许执行某些操作的过程。通常通过检查和解释Subject的角色和权限(见下文),然后允许或拒绝访问所请求的资源或功能来完成。
     
    Cipher
    暗号就是执行加密活解密的算法。该算法通常依赖于称为密钥的信息,如果没有密钥,解密是非常困难的。
     
    Credential
    凭证是一条用于验证用户-Subject(见下文)身份的信息。在尝试认证的期间,一个(或多个)凭证与Principal(见下文)一起提交,以证实提交它们的用户-Subject(见下文)是实际上的关联的用户。凭证通常是非常秘密的事物,只有特定的用户-Subject(见下文)知道,例如密码或PGP密钥或生物特征属性或类似的机制。
     
    Cryptography
    密码学是通过隐藏信息或将其转化为无意义的信息的方式来保护信息免受不正当访问的做法,因此没有其他人可以读取它。Shiro专注于密码学的两个核心元素:使用公钥或私钥对如电子邮件等数据进行加密,以及对密码等数据进行不可逆加密的散列(又名消息摘要)。
     
    Hash
    散列函数是输入源(有时称为消息)的单向、不可逆转换,将其转换为编码的散列值(有时称为消息摘要)。它通常用于密码,数字指纹或带有底层字节数组的数据。
     
    Permission
    权限,至少在Shiro的解释下,是一个声明,它描述了应用程序中的原始功能,仅此而已。权限是安全策略中的最低级别构造。他们只定义“应用程序可以做什么”。他们没有描述“谁”能够执行这些行为。权限仅仅是一种行为的描述。例如访问某个页面,删除某条记录等。
     
    Principal
    principal是应用程序用户-Subject(见下文)的任何标识属性,标识属性可以是对应用程序有意义的任何内容 - 用户名,姓,特定名称,社会安全号码,用户ID等等。Shiro还引用了我们称之为Subject(见下文)primary principal的东西,primary principal是整个应用程序中可以唯一标识Subject(见下文)的任何principal。理想的primary principal是像用户名或用户ID这样的东西,它是RDBMS中用户表的主键。应用程序中的用户-Subject(见下文)只有一个primary principal。
     
    Realm
    realm是一个可以访问特定的应用程序安全数据(如用户,角色和权限)的组件。它可以被认为是特定的安全性DAO(数据访问对象)。Realm将这些特定的应用程序的数据转换为Shiro可以理解的格式,因此无论存在多少数据源或数据如何针对特定应用程序,Shiro都可以提供一种易于理解的Subject编程API。
    领域通常与数据源(如关系数据库,LDAP目录,文件系统或其他类似资源)具有1对1关联。因此,Realm接口的实现使用特定于数据源的API来发现授权数据(角色,权限等),例如JDBC,文件IO,Hibernate或JPA或任何其他数据访问API。
     
    Role
    角色的定义可能因您交谈的对象而有所不同。在许多应用程序中,它是一个模糊的概念,最多人们用它来隐式定义安全策略。Shiro倾向于将角色解释为权限的命名集合。这就是它 - 一个应用程序中唯一的名称,它集合了一个或多个Permission声明。
     
    Session
    会话是与在一段时间内与软件系统交互的单个用户-Subject(见下文)相关联的有状态的数据上下文。在Subject(见下文)使用应用程序时,可以从Session中添加/读取/删除数据,并且应用程序可以稍后在必要时使用此数据。Session在用户-Subject(见下文)退出应用程序或由于不活动而超时时终止。对于那些熟悉HttpSession的人来说,一个Shiro 会话可以达到同样的目的,但Shiro会话可以在任何环境中使用,即使没有可用的Servlet容器或EJB容器。
     
    Subject
    Subject只是一个花哨的安全术语,它基本上意味着一个应用程序用户的特定的安全“视图”。然而Subject并不总是需要表示为人类 - 它可以表示调用应用程序的外部进程,也可以表示在一段时间内间歇性执行某些事情的守护进程帐户(例如cron作业)。它基本上表示利用应用程序做某些事情的任何实体。
     
    架构
     
    下图是这几个概念的交互概述:
    下图为Shiro的核心体系架构:
    Subject
    Subject本质上是当前正在执行的用户的安全特定“视图”。尽管“用户”这个词通常意味着一个人,但Subject可以是一个人,但它也可以代表一个第三方服务,守护进程账户,cron作业或类似的任何东西 - 基本上就是任何当前与该软件进行交互的东西。
    Subject实例都绑定到(并且需要)SecurityManager。当你与Subject进行交互时,这些交互转化为与SecurityManager的交互。
    SecurityManager
    SecurityManager是Shiro的体系结构的核心,它主要用于协调其内部的安全组件。一旦为应用程序配置了SecurityManager及其内部的组件,它通常会被放在一边,应用程序开发人员几乎将所有时间用在Subject的API上。当你和Subject交互的时候,SecurityManager在幕后为任何Subject的安全操作做了所有的繁重工作。
    Authenticator
    认证器是负责执行和响应用户认证行为(例如登录)的组件。当用户尝试的登录时,该逻辑由Authenticator执行。Authenticator知道如何协调一个或多个存储有关用户/帐户信息的Realm。从这些Realm中获得的数据用于验证用户的身份,以确保用户确实是他们所说的人。
    Authentication Strategy,如果配置了一个或多个Realm,AuthenticationStrategy将协调这些Realm以确定身份验证尝试是成功或失败的条件,例如一个Realm成功但是其他的Realm失败,这次身份验证尝试是否成功?是否所有Realm都需要成功?只有第一个Realm需要成功?
    Authorizer
    授权器是负责确定程序中用户的访问控制的组件,它是一种表明用户是否被允许做某事的机制。正如Authenticator,Authorizer也知道如何协调多个后端数据源来访问角色和权限信息。Authorizer使用该信息来确定到底是否允许用户执行特定的操作。
    SessionManager
    SessionManager知道如何创建和管理用户Session的生命周期,提供在所有环境中的用户强大的会话体验。这是安全框架世界中的一项独特功能 - 即使没有可用的Web/Servlet或EJB容器,Shiro也能够在任何环境中本地管理用户会话。默认情况下,Shiro将使用现有的会话机制(如Servlet容器),但如果没有(例如在独立应用程序或非Web环境中),它将使用其内置的企业会话管理提供相同的编程体验。
    SessionDAO的存在是为了允许任何数据源被用来持久化会话。SessionDAO代表SessionManager执行Session的持久性操作(CRUD)。这允许任何数据存储插入到会话管理基础架构中。
    CacheManager
    CacheManager创建和管理被其他Shiro使用的Cache的生命周期。由于Shiro可以访问许多后端数据源以进行身份​​验证,授权和会话管理,因此缓存一直是框架中一流的架构特性,可在使用这些数据源时提高性能。任何现代化的开源和/或企业缓存产品都可以插入到Shiro中,以提供快速和高效的用户体验。
    Cryptography
    加密技术是企业安全框架的自然补充,Shiro的crypto软件包包含易于使用和理解的加密技术。
    Realm
    Realm充当Shiro和应用程序安全数据之间的“桥梁”或“连接器”。当实际需要与用户帐户等安全相关数据进行交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个Realm中查找这些数据。
    从这个意义上说,Realm本质上是一个安全特定的DAO:它封装数据源的连接细节,并根据需要使相关数据可用于Shiro。配置Shiro时,您必须至少指定一个Realm用于认证、授权。SecurityManager可配置多个Realm。
    Shiro提供开箱即用的Realm,可以连接到许多安全数据源(又名目录),如LDAP,关系数据库(JDBC),文本配置源如INI和属性文件等。如果默认的Realm不能满足您的需求,您可以插入自己的Realm实现来表示自定义数据源。
    像其他内部组件一样,Shiro SecurityManager管理着Realm如何使用以获取代表Subject安全和身份的数据。
    总结
    由于Shiro的API鼓励以Subject为中心的编程方法,所以大多数应用程序开发人员很少会与SecurityManager直接交互(但是框架开发人员有时会发现它很有用)。即使如此,了解SecurityManager功能如何,尤其是在为应用程序配置功能时仍然很重要。
     
    示例
    该示例依赖maven,使用ini文件作为应用程序的配置文件。
     
    shiro配置
    shiro的配置是从securityManager对象开始的,这个对象是线程安全且单个shiro应用只需要一个securityManager,因此Shiro提供了SecurityUtils让我们绑定它为全局的。
    shiro提供了编程式的配置,如:
    SecurityManager securityManager = new DefaultSecurityManager(realm);
    //设置 authenticator
    ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
    authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
    securityManager.setAuthenticator(authenticator);
    //设置 authorizer
    ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();
    authorizer.setPermissionResolver(new WildcardPermissionResolver());
    securityManager.setAuthorizer(authorizer);
    SecurityUtils.setSecurityManager(securityManager);
     
    不过,大多数应用程序都受益于基于文本的配置,这些配置可以独立于源代码进行修改,甚至可以让那些不熟悉Shiro API的人员更容易理解。为确保基于文本的通用配置机制能够在工作于所有最小的第三方依赖的环境中,Shiro支持INI格式来构建SecurityManager对象图及其支持组件。INI易于阅读,易于配置,设置简单,适合大多数应用。
    ini 配置文件类似于Java中的properties,不同的是ini配置文件中的key是每个部分不重复即可,而不是整个配置文件,每个部分可以被看作是单个properties,注释符可以是“#”和“;”,ini配置包含以下几个部分:
    [main]
    定义应用程序的SecurityManager实例及它的任何依赖对象如Realm,按照出现的顺序执行,后边的覆盖前边的配置。对象定义格式为:对象名=全限定类名(相对于调用public无参构造器创建对象)。对象属性定义格式为:对象名.属性名=常量值(相当于调用对象setter方法设置常量值)、对象名.属性名=$对象名(相当于调用setter方法设置对象引用)。默认情况已经创建了一个名为securityManager的对象 , 类型为org.apache.shiro.mgt.DefaultSecurityManager,可以自定义,但对象名必须为securityManager。
    对象嵌套注入,如:securityManager.sessionManager.globalSessionTimeout = 1800000
    字节数组注入,由于字节数组不能以文本格式指定,因此我们必须使用字节数组的文本编码。默认是Base64,也可以在字符串前加上0x表示十六进制编码,如:securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==、
    securityManager.rememberMeManager.cipherKey = 0x3707344A4093822299F31D008。
    Array/Set/List注入,只需以逗号分隔常量值或对象引用,如:securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2。
    Map注入,格式为map=key:value,key:value,keh和value可以是常量值和对象引用,常量值是字符串。
    上述编程式的配置相当于:
    authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
    authenticationStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
    authenticator.authenticationStrategy=$authenticationStrategy
    securityManager.authenticator=$authenticator
    authorizer=org.apache.shiro.authz.ModularRealmAuthorizer
    permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver
    authorizer.permissionResolver=$permissionResolver
    securityManager.authorizer=$authorizer
     
    [users]
    定义一组静态的用户,包括用户名、密码、角色,这在用户数量非常少的环境或用户不需要在运行时动态创建的情况下非常有用,格式为:用户名=密码,角色1,角色2。密码是必须的,角色是可选的。
     
    [roles]
    定义一组静态的角色,包括角色、权限,这在角色数量非常少的环境或角色不需要在运行时动态创建的情况下非常有用,格式为:角色=权限1,权限2。
     
    [urls]
    用于web应用程序,提供了基于url 安全相关的配置,格式为:url=拦截器[参数],拦截器。
    users和roles部分会分别被设置为org.apache.shiro.realm.text.IniRealm实例的users和roles属性。
     
    maven配置
    <?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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
     
    <groupId>com.xxx</groupId>
    <artifactId>xxx-web-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
     
    <name>xxx-web-demo Maven Webapp</name>
    <url>http://www.example.com</url>
     
    <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
     
    <build>
    <finalName>xxx-web-demo</finalName>
    <plugins>
    <plugin>
    <artifactId>maven-clean-plugin</artifactId>
    <version>3.0.0</version>
    </plugin>
    <plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.0.2</version>
    </plugin>
    <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.7.0</version>
    </plugin>
    <plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.20.1</version>
    </plugin>
    <plugin>
    <artifactId>maven-war-plugin</artifactId>
    <version>3.2.0</version>
    </plugin>
    <plugin>
    <artifactId>maven-install-plugin</artifactId>
    <version>2.5.2</version>
    </plugin>
    <plugin>
    <artifactId>maven-deploy-plugin</artifactId>
    <version>2.8.2</version>
    </plugin>
    </plugins>
    </build>
     
    <dependencies>
    <dependency>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    </dependency>
     
    <!--shiro-->
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.3.2</version>
    <exclusions>
    <exclusion>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.3.2</version>
    </dependency>
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
    </dependency>
     
    <!--slf4j-->
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.25</version>
    </dependency>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.25</version>
    </dependency>
     
    <!--javaee-->
    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
    </dependency>
     
    <dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.2</version>
    <scope>provided</scope>
    </dependency>
    </dependencies>
    </project>
     
    代码
    ShiroTestCase .java:
    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.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.subject.Subject;
    import org.apache.shiro.util.Factory;
    import org.junit.Test;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
     
    public class ShiroTestCase {
    private final static Logger LOGGER = LoggerFactory.getLogger(ShiroTestCase.class);
     
    @Test
    public void test() {
    //获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager工厂
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
     
    //得到SecurityManager实例
    SecurityManager securityManager = factory.getInstance();
     
    //绑定SecurityManager实例到SecurityUtils
    SecurityUtils.setSecurityManager(securityManager);
     
    //得到Subject
    Subject subject = SecurityUtils.getSubject();
     
    //获取当前会话,如果会话不存在,就创建一个,相当于subject.getSession(true)
    Session session = subject.getSession();
    //设置会话的属性
    session.setAttribute("someKey", "aValue");
     
    //判断用户是否登录
    if (!subject.isAuthenticated()) {
    //创建用户名/密码身份验证Token
    UsernamePasswordToken token = new UsernamePasswordToken("admin", "secret");
    try {
    //登录,即身份验证
    subject.login(token);
    LOGGER.info("登录成功");
    } catch (AuthenticationException e) {
    LOGGER.error("登录失败", e);
    }
    }
     
    //从会话中获取属性
    String value = (String) session.getAttribute("someKey");
    if (value.equals("aValue")) {
    LOGGER.info("会话有效");
    }
     
    //判断用户是否拥有指定的角色
    if (subject.hasRole("admin")) {
    LOGGER.info("有admin角色");
    } else {
    LOGGER.info("没有admin角色");
    }
     
    //判断用户是否拥有指定的权限
    if (subject.isPermitted("user:create")) {
    LOGGER.info("有user:create权限");
    } else {
    LOGGER.info("没有user:create权限");
    }
     
    //注销
    subject.logout();
     
    try {
    //从会话中获取属性,如果已经注销,就会抛出异常
    session.getAttribute("someKey");
    } catch (UnknownSessionException e) {
    LOGGER.error("会话无效", e);
    }
    }
    }
     
    执行结果:
    [main] INFO org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler...
     
    [main] INFO shiro.ShiroTestCase - 登录成功
    [main] INFO shiro.ShiroTestCase - 会话有效
    [main] INFO shiro.ShiroTestCase - 有admin角色
    [main] INFO shiro.ShiroTestCase - 有user:create权限
    [main] ERROR shiro.ShiroTestCase - 会话无效
    org.apache.shiro.session.UnknownSessionException: There is no session with id [741feb55-2b3c-4ea6-b332-e35e46572304]
    at org.apache.shiro.session.mgt.eis.AbstractSessionDAO.readSession(AbstractSessionDAO.java:170)
    at org.apache.shiro.session.mgt.DefaultSessionManager.retrieveSessionFromDataSource(DefaultSessionManager.java:236)
    at org.apache.shiro.session.mgt.DefaultSessionManager.retrieveSession(DefaultSessionManager.java:222)
    at org.apache.shiro.session.mgt.AbstractValidatingSessionManager.doGetSession(AbstractValidatingSessionManager.java:118)
    at org.apache.shiro.session.mgt.AbstractNativeSessionManager.lookupSession(AbstractNativeSessionManager.java:148)
    at org.apache.shiro.session.mgt.AbstractNativeSessionManager.lookupRequiredSession(AbstractNativeSessionManager.java:152)
    at org.apache.shiro.session.mgt.AbstractNativeSessionManager.getAttribute(AbstractNativeSessionManager.java:249)
    at org.apache.shiro.session.mgt.DelegatingSession.getAttribute(DelegatingSession.java:141)
    at org.apache.shiro.session.ProxiedSession.getAttribute(ProxiedSession.java:121)
    at shiro.ShiroTestCase.test(ShiroTestCase.java:79)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
  • 相关阅读:
    Asp.Net Web API 2第八课——Web API 2中的属性路由
    Asp.Net Web API 2第七课——Web API异常处理
    Asp.Net Web API 2第六课——Web API路由和动作选择
    Asp.Net Web API 2第五课——Web API路由
    开始学习python
    BMI 小程序 购物车
    深浅copy 文件操作
    字典 dict 集合set
    基本数据类型 (str,int,bool,tuple,)
    python 运算符
  • 原文地址:https://www.cnblogs.com/gjb724332682/p/9090018.html
Copyright © 2011-2022 走看看