Introduction to Apache Shiro – 阿帕奇-希罗简介

最后修改: 2017年 8月 27日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we’ll look at Apache Shiro, a versatile Java security framework.

在本文中,我们将了解Apache Shiro,一个多功能的Java安全框架。

The framework is highly customizable and modular, as it offers authentication, authorization, cryptography and session management.

该框架是高度可定制和模块化的,因为它提供认证、授权、加密和会话管理。

2. Dependency

2.依赖性

Apache Shiro has many modules. However, in this tutorial, we use the shiro-core artifact only.

Apache Shiro有许多modules。然而,在本教程中,我们只使用shiro-core工件。

Let’s add it to our pom.xml:

让我们把它添加到我们的pom.xml

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>

The latest version of the Apache Shiro modules can be found on Maven Central.

最新版本的Apache Shiro模块可以在Maven Central上找到

3. Configuring Security Manager

3.配置安全管理器

The SecurityManager is the center piece of Apache Shiro’s framework. Applications will usually have a single instance of it running.

SecurityManager是Apache Shiro框架的核心部分。应用程序通常会有一个单一的实例在运行。

In this tutorial, we explore the framework in a desktop environment. To configure the framework, we need to create a shiro.ini file in the resource folder with the following content:

在本教程中,我们在桌面环境中探索该框架。为了配置该框架,我们需要在资源文件夹中创建一个shiro.ini文件,内容如下。

[users]
user = password, admin
user2 = password2, editor
user3 = password3, author

[roles]
admin = *
editor = articles:*
author = articles:compose,articles:save

The [users] section of the shiro.ini config file defines the user credentials that are recognized by the SecurityManager. The format is: principal (username) = password, role1, role2, …, role.

[users]配置文件的shiro.ini部分定义了SecurityManager所识别的用户凭证。其格式为。p校长(用户名)=密码,角色1,角色2,…,角色

The roles and their associated permissions are declared in the [roles] section. The admin role is granted permission and access to every part of the application. This is indicated by the wildcard (*) symbol.

角色及其相关权限在[角色]部分声明。admin角色被授予权限,可以访问应用程序的每个部分。这是由通配符(*)符号表示的。

The editor role has all permissions associated with articles while the author role can only compose and save an article.

编辑角色拥有与文章相关的所有权限,而作者角色只能创作保存文章。

The SecurityManager is used to configure the SecurityUtils class. From the SecurityUtils we can obtain the current user interacting with the system and perform authentication and authorization operations.

SecurityManager用于配置SecurityUtils类。从SecurityUtils中,我们可以获得当前与系统交互的用户,并执行认证和授权操作。

Let’s use the IniRealm to load our user and role definitions from the shiro.ini file and then use it to configure the DefaultSecurityManager object:

让我们使用IniRealmshiro.ini文件加载我们的用户和角色定义,然后用它来配置DefaultSecurityManager对象。

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();

Now that we have a SecurityManager that is aware of user credentials and roles defined in the shiro.ini file, let’s proceed to user authentication and authorization.

现在我们有了一个SecurityManager,它知道shiro.ini文件中定义的用户凭证和角色,让我们继续进行用户认证和授权。

4. Authentication

4.认证

In Apache Shiro’s terminologies, a Subject is any entity interacting with the system. It may either be a human, a script, or a REST Client.

在Apache Shiro的术语中,Subject是与系统交互的任何实体。它可以是人,也可以是脚本,或者是REST客户端。

Calling SecurityUtils.getSubject() returns an instance of the current Subject, that is, the currentUser.

调用SecurityUtils.getSubject()返回当前Subject的实例,即currentUser

Now that we have the currentUser Object, we can perform authentication on the supplied credentials:

现在我们有了currentUser对象,我们可以对提供的凭证进行认证。

if (!currentUser.isAuthenticated()) {               
  UsernamePasswordToken token                       
    = new UsernamePasswordToken("user", "password");
  token.setRememberMe(true);                        
  try {                                             
      currentUser.login(token);                       
  } catch (UnknownAccountException uae) {           
      log.error("Username Not Found!", uae);        
  } catch (IncorrectCredentialsException ice) {     
      log.error("Invalid Credentials!", ice);       
  } catch (LockedAccountException lae) {            
      log.error("Your Account is Locked!", lae);    
  } catch (AuthenticationException ae) {            
      log.error("Unexpected Error!", ae);           
  }                                                 
}

First, we check if the current user has not been authenticated already. Then we create an authentication token with the user’s principal (username) and credential (password).

首先,我们检查当前用户是否已经被认证过。然后,我们用用户的委托人(用户名)和凭证(密码)创建一个认证令牌。

Next, we attempt to login in with the token. If the supplied credentials are correct, everything should go fine.

接下来,我们尝试用令牌登录。如果提供的凭证是正确的,一切都应该很顺利。

There are different exceptions for different cases. It’s also possible to throw a custom exception that better suits the application requirement. This can be done by subclassing the AccountException class.

对于不同的情况有不同的异常。也可以抛出一个更适合应用要求的自定义异常。这可以通过子类化AccountException类来实现。

5. Authorization

5.授权

Authentication is trying to validate the identity of a user while authorization is trying to control access to certain resources in the system.

认证是试图验证一个用户的身份,而授权是试图控制对系统中某些资源的访问。

Recall that we assign one or more roles to each user we have created in the shiro.ini file. Furthermore, in the roles section, we define different permissions or access levels for each role.

回顾一下,我们为我们在shiro.ini文件中创建的每个用户分配了一个或多个角色。此外,在角色部分,我们为每个角色定义不同的权限或访问级别。

Now let’s see how we can use that in our application to enforce user access control.

现在让我们看看如何在我们的应用程序中使用它来实施用户访问控制。

In the shiro.ini file, we give the admin total access to every part of the system.

shiro.ini文件中,我们给予管理员对系统的每一部分的完全访问权。

The editor has total access to every resource/operation regarding articles, and an author is restricted to just composing and saving articles only.

编辑可以完全访问有关文章的所有资源/操作,而作者则只限于撰写和保存文章

Let’s welcome the current user based on role:

让我们根据角色来欢迎当前的用户。

if (currentUser.hasRole("admin")) {       
    log.info("Welcome Admin");              
} else if(currentUser.hasRole("editor")) {
    log.info("Welcome, Editor!");           
} else if(currentUser.hasRole("author")) {
    log.info("Welcome, Author");            
} else {                                  
    log.info("Welcome, Guest");             
}

Now, let’s see what the current user is permitted to do in the system:

现在,让我们看看当前用户在系统中被允许做什么。

if(currentUser.isPermitted("articles:compose")) {            
    log.info("You can compose an article");                    
} else {                                                     
    log.info("You are not permitted to compose an article!");
}                                                            
                                                             
if(currentUser.isPermitted("articles:save")) {               
    log.info("You can save articles");                         
} else {                                                     
    log.info("You can not save articles");                   
}                                                            
                                                             
if(currentUser.isPermitted("articles:publish")) {            
    log.info("You can publish articles");                      
} else {                                                     
    log.info("You can not publish articles");                
}

6. Realm Configuration

6.境界配置

In real applications, we’ll need a way to get user credentials from a database rather than from the shiro.ini file. This is where the concept of Realm comes into play.

在实际应用中,我们需要一种方法来从数据库而不是从shiro.ini文件中获取用户证书。这就是Realm的概念发挥作用的地方。

In Apache Shiro’s terminology, a Realm is a DAO that points to a store of user credentials needed for authentication and authorization.

在Apache Shiro的术语中,Realm是一个DAO,它指向认证和授权所需的用户凭证的存储。

To create a realm, we only need to implement the Realm interface. That can be tedious; however, the framework comes with default implementations that we can subclass from. One of these implementations is JdbcRealm.

要创建一个境界,我们只需要实现Realm接口。这可能很繁琐;但是,框架附带了默认的实现,我们可以从中进行子类化。其中一个实现是JdbcRealm

We create a custom realm implementation that extends JdbcRealm class and overrides the following methods: doGetAuthenticationInfo(), doGetAuthorizationInfo(), getRoleNamesForUser() and getPermissions().

我们创建了一个自定义的境界实现,它扩展了JdbcRealm类并重写了以下方法。doGetAuthenticationInfo(), doGetAuthorizationInfo(), getRoleNamesForUser()getPermissions()

Let’s create a realm by subclassing the JdbcRealm class:

让我们通过子类化JdbcRealm类来创建一个境界。

public class MyCustomRealm extends JdbcRealm {
    //...
}

For the sake of simplicity, we use java.util.Map to simulate a database:

为了简单起见,我们使用java.util.Map来模拟一个数据库。

private Map<String, String> credentials = new HashMap<>();
private Map<String, Set<String>> roles = new HashMap<>();
private Map<String, Set<String>> perm = new HashMap<>();

{
    credentials.put("user", "password");
    credentials.put("user2", "password2");
    credentials.put("user3", "password3");
                                          
    roles.put("user", new HashSet<>(Arrays.asList("admin")));
    roles.put("user2", new HashSet<>(Arrays.asList("editor")));
    roles.put("user3", new HashSet<>(Arrays.asList("author")));
                                                             
    perm.put("admin", new HashSet<>(Arrays.asList("*")));
    perm.put("editor", new HashSet<>(Arrays.asList("articles:*")));
    perm.put("author", 
      new HashSet<>(Arrays.asList("articles:compose", 
      "articles:save")));
}

Let’s proceed and override the doGetAuthenticationInfo():

让我们继续并重写doGetAuthenticationInfo()

protected AuthenticationInfo 
  doGetAuthenticationInfo(AuthenticationToken token)
  throws AuthenticationException {
                                                                 
    UsernamePasswordToken uToken = (UsernamePasswordToken) token;
                                                                
    if(uToken.getUsername() == null
      || uToken.getUsername().isEmpty()
      || !credentials.containsKey(uToken.getUsername())) {
          throw new UnknownAccountException("username not found!");
    }
                                        
    return new SimpleAuthenticationInfo(
      uToken.getUsername(), 
      credentials.get(uToken.getUsername()), 
      getName()); 
}

We first cast the AuthenticationToken provided to UsernamePasswordToken. From the uToken, we extract the username (uToken.getUsername()) and use it to get the user credentials (password) from the database.

我们首先将提供的AuthenticationToken转换为UsernamePasswordToken。从uToken中,我们提取用户名(uToken.getUsername())并使用它从数据库中获取用户证书(密码)。

If no record is found – we throw an UnknownAccountException, else we use the credential and username to construct a SimpleAuthenticatioInfo object that’s returned from the method.

如果没有找到记录–我们就抛出一个UnknownAccountException,否则我们就使用证书和用户名来构建一个SimpleAuthenticatioInfo对象,该对象将从该方法返回。

If the user credential is hashed with a salt, we need to return a SimpleAuthenticationInfo with the associated salt:

如果用户凭证是用盐加密的,我们需要返回一个带有相关盐的SimpleAuthenticationInfo

return new SimpleAuthenticationInfo(
  uToken.getUsername(), 
  credentials.get(uToken.getUsername()), 
  ByteSource.Util.bytes("salt"), 
  getName()
);

We also need to override the doGetAuthorizationInfo(), as well as getRoleNamesForUser() and getPermissions().

我们还需要覆盖doGetAuthorizationInfo(),以及getRoleNamesForUser()getPermissions()

Finally, let’s plug the custom realm into the securityManager. All we need to do is replace the IniRealm above with our custom realm, and pass it to the DefaultSecurityManager‘s constructor:

最后,让我们将自定义境界插入securityManager中。我们需要做的就是用我们的自定义境界替换上面的IniRealm,并将其传递给DefaultSecurityManager的构造函数。

Realm realm = new MyCustomRealm();
SecurityManager securityManager = new DefaultSecurityManager(realm);

Every other part of the code is the same as before. This is all we need to configure the securityManager with a custom realm properly.

代码的其他部分和以前一样。这就是我们需要的所有内容,以正确配置带有自定义境界的securityManager

Now the question is – how does the framework match the credentials?

现在的问题是–框架如何与凭证相匹配?

By default, the JdbcRealm uses the SimpleCredentialsMatcher, which merely checks for equality by comparing the credentials in the AuthenticationToken and the AuthenticationInfo.

默认情况下,JdbcRealm使用SimpleCredentialsMatcher,它只是通过比较AuthenticationTokenAuthenticationInfo中的凭证来检查是否相等。

If we hash our passwords, we need to inform the framework to use a HashedCredentialsMatcher instead. The INI configurations for realms with hashed passwords can be found here.

如果我们对密码进行散列,我们需要通知框架使用HashedCredentialsMatcher来代替。可以在这里找到带有散列密码的领域的INI配置。

7. Logging Out

7.注销

Now that we’ve authenticated the user, it’s time to implement log out. That’s done simply by calling a single method – which invalidates the user session and logs the user out:

现在我们已经验证了用户的身份,是时候实现注销了。这只是通过调用一个方法来实现的–该方法使用户会话无效并将用户注销。

currentUser.logout();

8. Session Management

8.会话管理

The framework naturally comes with its session management system. If used in a web environment, it defaults to the HttpSession implementation.

该框架自然会有其会话管理系统。如果在Web环境中使用,它默认为HttpSession实现。

For a standalone application, it uses its enterprise session management system. The benefit is that even in a desktop environment you can use a session object as you would do in a typical web environment.

对于一个独立的应用程序,它使用其企业会话管理系统。其好处是,即使在桌面环境中,你也可以像在典型的网络环境中那样使用会话对象。

Let’s have a look at a quick example and interact with the session of the current user:

让我们来看一个快速的例子,与当前用户的会话进行交互。

Session session = currentUser.getSession();                
session.setAttribute("key", "value");                      
String value = (String) session.getAttribute("key");       
if (value.equals("value")) {                               
    log.info("Retrieved the correct value! [" + value + "]");
}

9. Shiro for a Web Application With Spring

9.使用Spring的Web应用程序的Shiro

So far we’ve outlined the basic structure of Apache Shiro and we have implemented it in a desktop environment. Let’s proceed by integrating the framework into a Spring Boot application.

到目前为止,我们已经概述了Apache Shiro的基本结构,并在一个桌面环境中实现了它。让我们继续将该框架集成到Spring Boot应用程序中。

Note that the main focus here is Shiro, not the Spring application – we’re only going to use that to power a simple example app.

请注意,这里的主要重点是Shiro,而不是Spring应用程序–我们只打算用它来驱动一个简单的示例应用程序。

9.1. Dependencies

9.1.依赖性

First, we need to add the Spring Boot parent dependency to our pom.xml:

首先,我们需要在pom.xml中添加Spring Boot的父依赖关系。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.2</version>
</parent>

Next, we have to add the following dependencies to the same pom.xml file:

接下来,我们必须在同一个pom.xml文件中添加以下依赖项。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>${apache-shiro-core-version}</version>
</dependency>

9.2. Configuration

9.2.配置

Adding the shiro-spring-boot-web-starter dependency to our pom.xml will by default configure some features of the Apache Shiro application such as the SecurityManager.

在我们的pom.xml中添加shiro-spring-boot-web-starter依赖项,将默认配置Apache Shiro应用程序的一些功能,比如SecurityManager

However, we still need to configure the Realm and Shiro security filters. We will be using the same custom realm defined above.

然而,我们仍然需要配置Realm和Shiro安全过滤器。我们将使用上面定义的同一个自定义境界。

And so, in the main class where the Spring Boot application is run, let’s add the following Bean definitions:

因此,在运行Spring Boot应用程序的主类中,让我们添加以下Bean定义。

@Bean
public Realm realm() {
    return new MyCustomRealm();
}
    
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter
      = new DefaultShiroFilterChainDefinition();

    filter.addPathDefinition("/secure", "authc");
    filter.addPathDefinition("/**", "anon");

    return filter;
}

In the ShiroFilterChainDefinition, we applied the authc filter to /secure path and applied the anon filter on other paths using the Ant pattern.

ShiroFilterChainDefinition中,我们将authc过滤器应用于/secure路径,并使用Ant模式对其他路径应用anon过滤器。

Both authc and anon filters come along by default for web applications. Other default filters can be found here.

对于网络应用来说,authcanon过滤器都是默认出现的。其他默认过滤器可以在这里找到。

If we did not define the Realm bean, ShiroAutoConfiguration will, by default, provide an IniRealm implementation that expects to find a shiro.ini file in src/main/resources or src/main/resources/META-INF.

如果我们没有定义RealmBean,ShiroAutoConfiguration将默认提供一个IniRealm实现,期望在src/main/resourcessrc/main/resources/META-INF.中找到shiro.ini>文件。

If we do not define a ShiroFilterChainDefinition bean, the framework secures all paths and sets the login URL as login.jsp.

如果我们不定义一个ShiroFilterChainDefinitionBean,框架会保护所有路径,并将登录URL设置为login.jsp

We can change this default login URL and other defaults by adding the following entries to our application.properties:

我们可以通过在application.properties中添加以下条目来改变这个默认登录URL和其他默认值。

shiro.loginUrl = /login
shiro.successUrl = /secure
shiro.unauthorizedUrl = /login

Now that the authc filter has been applied to /secure, all requests to that route will require a form authentication.

现在,authc过滤器已被应用于/secure,所有对该路由的请求都需要表单验证。

9.3. Authentication and Authorization

9.3.认证和授权

Let’s create a ShiroSpringController with the following path mappings: /index, /login, /logout and /secure.

让我们创建一个ShiroSpringController,其路径映射如下。/index, /login, /logout/secure.

The login() method is where we implement actual user authentication as described above. If authentication is successful, the user is redirected to the secure page:

login()方法是我们实现上述实际用户认证的地方。如果认证成功,用户将被重定向到安全页面。

Subject subject = SecurityUtils.getSubject();

if(!subject.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken(
      cred.getUsername(), cred.getPassword(), cred.isRememberMe());
    try {
        subject.login(token);
    } catch (AuthenticationException ae) {
        ae.printStackTrace();
        attr.addFlashAttribute("error", "Invalid Credentials");
        return "redirect:/login";
    }
}

return "redirect:/secure";

And now in the secure() implementation, the currentUser was obtained by invoking the SecurityUtils.getSubject(). The role and permissions of the user are passed on to the secure page, as well the user’s principal:

而现在在secure()实现中,currentUser是通过调用SecurityUtils.getSubject()获得的。用户的角色和权限被传递给安全页面,以及用户的负责人。

Subject currentUser = SecurityUtils.getSubject();
String role = "", permission = "";

if(currentUser.hasRole("admin")) {
    role = role  + "You are an Admin";
} else if(currentUser.hasRole("editor")) {
    role = role + "You are an Editor";
} else if(currentUser.hasRole("author")) {
    role = role + "You are an Author";
}

if(currentUser.isPermitted("articles:compose")) {
    permission = permission + "You can compose an article, ";
} else {
    permission = permission + "You are not permitted to compose an article!, ";
}

if(currentUser.isPermitted("articles:save")) {
    permission = permission + "You can save articles, ";
} else {
    permission = permission + "\nYou can not save articles, ";
}

if(currentUser.isPermitted("articles:publish")) {
    permission = permission  + "\nYou can publish articles";
} else {
    permission = permission + "\nYou can not publish articles";
}

modelMap.addAttribute("username", currentUser.getPrincipal());
modelMap.addAttribute("permission", permission);
modelMap.addAttribute("role", role);

return "secure";

And we’re done. That’s how we can integrate Apache Shiro into a Spring Boot Application.

然后我们就完成了。这就是我们如何将Apache Shiro集成到Spring Boot应用程序中。

Also, note that the framework offers additional annotations that can be used alongside filter chain definitions to secure our application.

另外,请注意,该框架提供了额外的注释,可以与过滤器链定义一起使用,以确保我们的应用程序安全。

10. JEE Integration

10.JEE整合

Integrating Apache Shiro into a JEE application is just a matter of configuring the web.xml file. As usual, the configuration expects shiro.ini to be in the class path. A detailed example configuration is available here. Also, the JSP tags can be found here.

将Apache Shiro集成到JEE应用程序中,只需要配置web.xml文件。像往常一样,该配置希望shiro.ini在类路径中。详细的配置示例可在这里。另外,JSP标签可以在这里找到。

11. Conclusion

11.结论

In this tutorial, we looked at the Apache Shiro’s authentication and authorization mechanisms. We also focused on how to define a custom realm and plug it into the SecurityManager.

在本教程中,我们研究了Apache Shiro的认证和授权机制。我们还重点讨论了如何定义一个自定义领域并将其插入SecurityManager中。

As always, the complete source code is available over on GitHub.

一如既往,完整的源代码可在GitHub上获得