https://www.infoq.com/articles/apache-shiro/
Application Security With Apache Shiro
Are you frustrated when you try to secure your applications? Do you feel existing Java security solutions are difficult to use and only confuse you further? This article introduces Apache Shiro, a Java security framework that provides a simple but powerful approach to application security. It explains Apache Shiro’s project goals, architectural philosophies and how you might use Shiro to secure your own applications.
What is Apache Shiro?
Shiro是个安全框架,主要功能包括认证、授权、加密和session管理。
应用安全的四大基石(Shiro发力的方向)
Shiro的其它特性还包括web应用安全,单元测试,多线程支持(对上述特性的加强)
Why was Apache Shiro created?
Shiro的发展历程:
2008年加入Apache项目之前已经存在了5年(JSecurity),在2003年,对于Java应用开发者,没有很多通用的安全框架替代品。当时应用的是JAAS(Java Authentication and Authorization Service),但是它存在太多缺点(认证还可以,但是授权很难用),并且JAAS深度绑定在虚拟机级别的安全特性(例如决定一个类是否应该允许被加载进JVM),作为应用开发人员,不想考虑这些问题。当时的session处理机制,只有HttpSessions,它需要一个web容器(或EJB2.1 Stateful Session Beans),即需要EJB容器,而一个和容器解耦的、能在所有环境使用的session管理机制才是开发人员想要的。加密,数据加密是一个很常见的需求,但是Java Cryptography Architecture非常难理解(除非是密码学专家),它的API充满了受检异常,而且很难用。而开发人员只需要简单的加密和解密的工具
在2003年的(应用)安全环境下看,当时没有任何一个单一且统一的框架可以同时包含开发人员所有的安全需求。因此JSecurity(Shiro)出生了
Why would you use Apache Shiro today?
这个框架的蓝图(landscape)从2003年开始已经变了很多,所以现在仍然有很多理由使用Shiro
Who’s Using Shiro?
Shiro和它的前身(JSecurity)在变成Apache项目基金会顶级项目之后已经在各种规模的公司项目使用。上面兼容的那些项目很多都正在使用Shiro。很多商业公司(Katasoft、New York commercial bank)也在使用Shiro
Core Concepts: Subject, SecurityManager, and Realms
Shiro的架构包含三个主要概念:Subject SecurityManager Realm
Subject
当我们想让自己的应用变得安全,那么最相关的问题会浮现出来(当前用户是谁?当前用户能干什么?)。所以我们能想到的最自然的提高应用安全性的方法就是基于当前的用户来做。Shiro的API用Subject概念来表达这种想法
Subject在安全术语中意思是“当前正在作为的用户”。不单说“User”是因为这个词的隐含意义是人类。在安全世界中,Subject可以是人类,也可以是第三方进程,守护账户(daemon account)等等。简单来说就是目前正在和软件交互的事物(the thing)
Subject currentUser = SecurityUtils.getSubject();
获取到Subject对象后,可以直接访问你希望通过Shiro来控制当前用户的90%的行为(登录、登出、访问它们的session、执行授权检查等等),这里的关键点(key point)是Shiro的API是教导式(intuitive),因为它反映了开发者通常认为的”每个用户“的安全控制,使用Subject很容易,即在任何需要安全相关行为的地方都可以使用
SecurityManager
Subject的幕后伙伴是(behind the scenes)SecurityManager。Subject只代表当前用户的安全操作,但是SecurityManager管理的是所有用户的安全操作。这是Shiro架构的核心,并且它的行为就像是一系列连接着很多内部的安全组件的“umbrella”对象,最终形成了object graph。然而,只要SecurityManager和它的内部object graph被配置好,那么哦我们可以忽略它。大部分时间基本都是和Subject API打交道
根据应用的具体情况来设置SecurityManager。例如,一个web应用通常要在web.xml指定一个Shiro Servlet Filter,这会设置一个SecurityManager实例。如果运行的是一个单机应用,那么需要另一种方式来配置它。但是有很多的配置选项
每个应用都只有一个SecurityManager实例。像几乎Shiro中的一切,默认的SecurityManager实现是POJO并且通过所有POJO兼容的配置方式来配置(Java代码,Spring XML,YAML,properties文件,ini文件等等)。基本上所有可以实例化类和调用JavaBeans兼容方法的事情都可以做(不大了解)
Shiro提供了默认的公分母(common denominator)方案(基于文本的INI配置)。INI易读易用,需要非常少的依赖。只要对object graph navigation有简单的了解,INI就可以高效配置简单的object graph(例如SecurityManager)
[main]
cm = org.apache.shiro.authc.credential.HashedCredentialsMatcher
cm.hashAlgorithm = SHA-512
cm.hashIterations = 1024
cm.storedCredentialsHexEncoded = false
iniRealm.credentialsMatcher = $cm
[users]
jdoe = TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJpcyByZWFzb2
asmith = IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbXNoZWQsIG5vdCB
In Listing 2, we see the example INI configuration we will use to configure the
[main]小结是配置SecurityManager对象和/或者任何SecurityManager使用的对象(Realm),这个例子中,有两个对象被配置
SecurityManager instance. There are two INI sections: [main] and [users].
The [main] section is where you configure the SecurityManager object and/or any objects (like Realms) used by the SecurityManager. In this example, we see two objects being configured:
The cm object, which is an instance of Shiro’s HashedCredentialsMatcher class. As you can see, various properties of the cm instance are being configured via ‘nested dot‘ syntax - a convention used by the IniSecurityManagerFactory shown in Listing 3, to represent object graph navigation and property setting.
The iniRealm object, which is a component used by the SecurityManager to represent user accounts defined in the INI format.
The [users] section is where you can specify a static list of user accounts - convenient for simple applications or when testing.
For the purposes of this introduction, it is not important to understand the intricacies of each section, but rather to see that INI configuration is one simple way of configuring Shiro. For more detailed information on INI configuration, please see Shiro‘s documentation.
Listing 3. Loading shiro.ini Configuration File
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.util.Factory;
...
//1. Load the INI configuration
Factory
new IniSecurityManagerFactory("classpath:shiro.ini");
//2. Create the SecurityManager
SecurityManager securityManager = factory.getInstance();
//3. Make it accessible
SecurityUtils.setSecurityManager(securityManager);
In Listing 3, we see a three-step process in this simple example:
Load the INI configuration that will configure the SecurityManager and its constituent components.
Create the SecurityManager instance based on the configuration (using Shiro’s Factory concept that represents the Factory Method design pattern).
Make the SecurityManager singleton accessible to the application. In this simple example, we set it as a VM-static singleton, but this is usually not necessary - your application configuration mechanism can determine if you need to use static memory or not.
Realms
The third and final core concept in Shiro is that of a Realm. A Realm acts as the ‘bridge’ or ‘connector’ between Shiro and your application’s security data. That is, when it comes time to actually interact with security-related data like user accounts to perform authentication (login) and authorization (access control), Shiro looks up many of these things from one or more Realms configured for an application.
In this sense a Realm is essentially a security-specific DAO: it encapsulates connection details for data sources and makes the associated data available to Shiro as needed. When configuring Shiro, you must specify at least one Realm to use for authentication and/or authorization. More than one Realm may be configured, but at least one is required.
Shiro provides out-of-the-box Realms to connect to a number of security data sources (aka directories) such as LDAP, relational databases (JDBC), text configuration sources like INI and properties files, and more. You can plug-in your own Realm implementations to represent custom data sources if the default Realms do not meet your needs. Listing 4 below is an example of configuring Shiro (via INI) to use an LDAP directory as one of the application’s Realms.
Listing 4. Example realm configuration snippet to connect to LDAP user data store
[main]
ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
ldapRealm.userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
ldapRealm.contextFactory.url = ldap://ldapHost:389
ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
Now that we’ve seen how to set up a basic Shiro environment, let’s discuss how you, as a developer, would go about using the framework.
Authentication
Authentication is the process of verifying a user‘s identity. That is, when a user authenticates with an application, they are proving they actually are who they say they are. This is also sometimes referred to as ‘login‘. This is typically a three-step process.
Collect the user’s identifying information, called principals, and supporting proof of identity, called credentials.
Submit the principals and credentials to the system.
If the submitted credentials match what the system expects for that user identity (principal), the user is considered authenticated. If they don’t match, the user is not considered authenticated.
A common example of this process that everyone is familiar with is that of the username/password combination. When most users login to a software application, they usually provide their username (the principal) and their supporting password (the credential). If the password (or representation of it) stored in the system matches what the user specifies, they are considered authenticated.
Shiro supports this same workflow in a simple and intuitive way. As we’ve said, Shiro has a Subject-centric API - almost everything you care to do with Shiro at runtime is achieved by interacting with the currently executing Subject. So, to login a Subject, you simply call its login method, passing an AuthenticationToken instance that represents the submitted principals and credentials (in this case, a username and password). This example is shown in Listing 5 below.
Listing 5. Subject Login
//1. Acquire submitted principals and credentials:
AuthenticationToken token =
new UsernamePasswordToken(username, password);
//2. Get the current Subject:
Subject currentUser = SecurityUtils.getSubject();
//3. Login:
currentUser.login(token);
As you can see, Shiro’s API easily reflects the common workflow. You’ll continue to see this simplicity as a theme for all of the Subject’s operations. When the login method is called, the SecurityManager will receive the AuthenticationToken and dispatch it to one or more configured Realms to allow each to perform authentication checks as required. Each Realm has the ability to react to submitted AuthenticationTokens as necessary. But what happens if the login attempt fails? What if the user specified an incorrect password? You can handle failures by reacting to Shiro’s runtime AuthenticationException as shown in Listing 6.
Listing 6. Handle Failed Login
//3. Login:
try {
currentUser.login(token);
} catch (IncorrectCredentialsException ice) { …
} catch (LockedAccountException lae) { …
}
…
catch (AuthenticationException ae) {…
}
You can choose to catch one of the AuthenticationException subclasses and react specifically, or generically handle any AuthenticationException (for example, show the user a generic “Incorrect username or password” message). The choice is yours depending on your application requirements.
After a Subject logs-in successfully, they are considered authenticated and usually you allow them to use your application. But just because a user proved their identity doesn’t mean that they can do whatever they want in your application. That begs the next question, “how do I control what the user is allowed to do or not?” Deciding what users are allowed to do is called authorization. We’ll cover how Shiro enables authorization next.
Authorization
Authorization is essentially access control - controlling what your users can access in your application, such as resources, web pages, etc. Most users perform access control by employing concepts such as roles and permissions. That is, a user is usually allowed to do something or not based on what roles and/or permissions are assigned to them. Your application can then control what functionality is exposed based on checks for these roles and permissions. As you might expect, the Subject API allows you to perform role and permission checks very easily. For example, the code snippet in Listing 7 shows how to check if a Subject has been assigned a certain role.
Listing 7. Role Check
if ( subject.hasRole(“administrator”) ) {
//show the ‘Create User’ button
} else {
//grey-out the button?
}
As you can see, your application can enable or disable functionality based on access control checks.
Permission checks are another way to perform authorization. Checking for roles as in the example above suffers from one significant flaw: you can’t add or delete roles at runtime. Your code is hard-coded with role names, so if you changed the role names and/or configuration, your code would be broken! If you need to be able to change a role’s meaning at runtime, or add or delete roles as desired, you have to rely on something else.
To that end, Shiro supports its notion of permissions. A permission is a raw statement of functionality, for example ‘open a door’, ‘create a blog entry’, ‘delete the ‘jsmith’ user’, etc. By having permissions reflect your application’s raw functionality, you only need to change permission checks when you change your application’s functionality. In turn, you can assign permissions to roles or to users as necessary at runtime.
As an example, shown in Listing 8 below, we can rewrite our previous role check and instead use a permission check.
Listing 8. Permission Check
if ( subject.isPermitted(“user:create”) ) {
//show the ‘Create User’ button
} else {
//grey-out the button?
}
This way, any role or user assigned the “user:create” permission can click the ‘Create User’ button, and those roles and assignments can even change at runtime, providing you with a very flexible security model.
The “user:create” string is an example of a permission string that adheres to certain parsing conventions. Shiro supports this convention out of the box with its WildcardPermission. Although out of scope for this introduction article, you’ll see that the WildcardPermission can be extremely flexible when creating security policies, and even supports things like instance-level access control.
Listing 9. Instance-Level Permission Check
if ( subject.isPermitted(“user:delete:jsmith”) ) {
//delete the ‘jsmith’ user
} else {
//don’t delete ‘jsmith’
}
This example shows that you can control, even down to a very fine-grained instance level, access to individual resources if you needed to. You could even invent your own permission syntax if you wanted to. See the Shiro Permission documentation for more information. Finally, just as with authentication, the above calls eventually make their way to the SecurityManager, which will consult one or more Realms to make the access control decisions. This allows a Realm to respond to both authentication and authorization operations as necessary.
So that is a brief overview of Shiro’s authorization capabilities. And while most security frameworks stop at authentication and authorization, Shiro provides much more. Next we’ll talk about Shiro’s advanced Session Management capabilities.
Session Management
Apache Shiro provides something unique in the world of security frameworks: a consistent Session API usable in any application and any architectural tier. That is, Shiro enables a Session programming paradigm for any application - from small daemon standalone applications to the largest clustered web applications. This means that application developers who wish to use sessions are no longer forced to use Servlet or EJB containers if they don’t need them otherwise. Or, if using these containers, developers now have the option of using a unified and consistent session API in any tier, instead of servlet or EJB-specific mechanisms.
But perhaps one of the most important benefits of Shiro’s sessions is that they are container-independent. This has subtle but extremely powerful implications. For example, let’s consider session clustering. How many container-specific ways are there to cluster sessions for fault-tolerance and failover? Tomcat does it differently than Jetty, which does it differently than Websphere, etc. But with Shiro sessions, you obtain a container-independent clustering solution. Shiro’s architecture allows for pluggable Session data stores, such as enterprise caches, relational databases, NoSQL systems and more. This means that you can configure session clustering once and it will work the same way regardless of your deployment environment - Tomcat, Jetty, JEE Server or standalone application. There is no need to reconfigure your app based on how you deploy your application.
Another benefit of Shiro’s sessions is session data can be shared across client technologies if desired. For example, a Swing desktop client can participate in the same web application session if desired - useful if the end-user is using both simultaneously. So how do you access a Subject’s session in any environment? There are two Subject methods as shown in the example below.
Listing 10. Subject’s Session
Session session = subject.getSession();
Session session = subject.getSession(boolean create);
As you can see, the methods are identical in concept to the HttpServletRequest API. The first method will return the Subject’s existing Session, or if there isn’t one yet, it will create a new one and return it. The second method accepts a boolean argument that determines whether or not a new Session will be created if it does not yet exist. Once you acquire the Subject’s Session, you can use it almost identically to an HttpSession. The Shiro team felt that the HttpSession API was most comfortable to Java developers, so we retained much of its feel. The big difference, of course, is that you can use Shiro Sessions in any application, not just web applications. Listing 11 shows this familiarity.
Listing 11. Session methods
Session session = subject.getSession();
session.getAttribute(“key”, someValue);
Date start = session.getStartTimestamp();
Date timestamp = session.getLastAccessTime();
session.setTimeout(millis);
...
Cryptography
Cryptography is the process of hiding or obfuscating data so prying eyes can’t understand it. Shiro’s goal in cryptography is to simplify and make usable the JDK’s cryptography support.
It is important to note that cryptography is not specific to Subjects in general, so it is one area of Shiro’s API that is not Subject-specific. You can use Shiro’s cryptography support anywhere, even if a Subject is not being used. The two areas where Shiro really focuses its cryptography support is in the areas of cryptographic hashes (aka message digests) and cryptographic ciphers. Let‘s take a look at these two in more detail.
Hashing
If you have used the JDK’s MessageDigest class, you quickly realize that it is a bit cumbersome to work with. It has an awkward static method factory-based API instead of an object-oriented one, and you are forced to catch checked exceptions that may never need to be caught. If you need to hex-encode or Base64-encode message digest output, you’re on your own - there’s no standard JDK support for either. Shiro addresses these issues in a clean and intuitive hashing API.
For example, let’s consider the relatively common case of MD5-hashing a file and determining the hex value of that hash. Called a ‘checksum’, this is used regularly when providing file downloads - users can perform their own MD5 hash on the downloaded file and assert that their checksum matches the one on the download site. If they match, the user can sufficiently assume that the file hasn’t been tampered with in transit.
Here is how you might try to do this without Shiro:
Convert the file to a byte array. There is nothing in the JDK to assist with this, so you’ll need to create a helper method that opens a FileInputStream, uses a byte buffer, and throws the appropriate IOExceptions, etc.
Use the MessageDigest class to hash the byte array, dealing with the appropriate exceptions, as shown in Listing 12 below.
Encode the hashed byte array into hex characters. There is nothing in the JDK to assist with this either, so you’ll need to create another helper method and probably use bitwise operations and bitshifting in your implementation.
Listing 12. JDK’s MessageDigest
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.digest(bytes);
byte[] hashed = md.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
That’s a significant amount of work for something so simple and relatively common. Now here’s how to do the exact same thing with Shiro.
String hex = new Md5Hash(myFile).toHex();
It is remarkably simpler and easier to understand what is going on when you use Shiro to simplify all of that work. SHA-512 hashing and Base64-encoding of passwords is just as easy.
String encodedPassword =
new Sha512Hash(password, salt, count).toBase64();
You can see how much Shiro simplifies hashing and encoding, saving you a bit of sanity in the process.
Ciphers
Ciphers are cryptographic algorithms that can reversibly transform data using a key. We use them to keep data safe, especially when transferring or storing data, times when data is particularly susceptible to prying eyes.
If you’ve ever used the JDK Cryptography APIs, and in particular the javax.crypto.Cipher class, you know that it can be an incredibly complex beast to tame. For starters, every possible Cipher configuration is always represented by an instance of javax.crypto.Cipher. Need to do public/private key cryptography? You use the Cipher. Need to use a Block Cipher for streaming operations? You use the Cipher. Need to create an AES 256-bit Cipher to secure data? You use the Cipher. You get the idea.
And how do you create the Cipher instance you need? You create a complex, unintuitive token-delimited String of cipher options, called a “transformation string”, and you pass this string to a Cipher.getInstance static factory method. With this cipher option String approach, there is no type-safety to ensure you’re using valid options. This also implicitly means that there is no JavaDoc to help you understand relevant options. And you are further required to deal with checked exceptions in case your String is incorrectly formulated, even if you know that the configuration is correct. As you can see, working with JDK Ciphers is quite a cumbersome task. These techniques were once standard for Java APIs a long time ago, but times have changed, and we want a much easier approach.
Shiro tries to simplify the entire concept of cryptographic ciphers by introducing its CipherService API. A CipherService is what most developers want when securing data: a simple, stateless, thread-safe API that can encrypt or decrypt data in its entirety in one method call. All you need to do is provide your key, and you can encrypt or decrypt as necessary. For example, 256-bit AES encryption can be used as shown in the Listing 13 below.
Listing 13. Apache Shiro’s Encryption API
AesCipherService cipherService = new AesCipherService();
cipherService.setKeySize(256);
//create a test key:
byte[] testKey = cipherService.generateNewKey();
//encrypt a file’s bytes:
byte[] encrypted =
cipherService.encrypt(fileBytes, testKey);
The Shiro example is simpler compared to the JDK’s Cipher API:
You can instantiate a CipherService directly - no strange or confusing factory methods.
Cipher configuration options are represented as JavaBeans-compatible getters and setters - there is no strange and difficult to understand “transformation string”.
Encryption and decryption is executed in a single method call.
No forced checked exceptions. Catch Shiro’s CryptoException if you want.
There are other benefits to Shiro’s CipherService API, such as the ability to support both byte array-based encryption/decryption (called ‘block’ operations) as well as stream-based encryption/decryption (for example, encrypting audio or video).
Java Cryptography doesn’t need to be painful. Shiro’s Cryptography support is meant to simplify your efforts to keep your data secure.
Web Support
Last, but not least, we’ll briefly introduce Shiro’s web support. Shiro ships with a robust web support module to help secure web applications. It is simple to set up Shiro for a web application. The only thing necessary is to define a Shiro Servlet Filter in web.xml. Listing 14 shows this code.
Listing 14. ShiroFilter in web.xml
org.apache.shiro.web.servlet.IniShiroFilter
Hello
原文:https://www.cnblogs.com/mouseGo/p/14062312.html