Let me share with you an original Shiro tutorial by Songo. I haven’t finished it yet, but I’ll finish it first.

1. Introduction of Shiro

Apache Shiro is an open source security framework that provides authentication, authorization, cryptography, and session management. The Shiro framework is intuitive, easy to use, and provides robust security that, while not as powerful as SpringSecurity, can be used in normal projects.

1.1 origin

Shiro grew out of JSecurity, which was founded in 2004 by Les Hazlewood and Jeremy Haile. They couldn’t find the right Java security framework at the application level and were very disappointed with JAAS. JSecurity was hosted on SourceForge between 2004 and 2008, with contributors including Peter Ledbrook, Alan Ditzel, and Tim Veil. In 2008, the JSecurity project contributed to the Apache Software Foundation (ASF) and was accepted as the Apache Incubator Project, managed by the tutor with the goal of becoming a top-level Apache project. During this time, Jsecurity was briefly renamed Ki, and later renamed “Shiro” by the community due to trademark issues. Subsequently the project continued to incubate in Apache Incubator with the addition of contributor Kalle Korhonen. In July 2010, the Shiro community released version 1.0, after which the community created its project Management committee and elected Les Hazlewood as its chair. On September 22, 2010, Shrio became an Apache Software Foundation Top-level Program (TLP).

1.2 What are the Functions

Apache Shiro is a powerful and flexible open source security framework that cleanly handles authentication, authorization, enterprise session management, and encryption. The primary goal of Apache Shiro is ease of use and understanding. Security can sometimes be complicated, even painful, but it doesn’t have to be. The framework should mask as much complexity as possible, exposing a clean and intuitive API that simplifies the time developers spend on application security.

Here’s what you can do with Apache Shiro:

  1. Authenticate users to verify their identity

  2. Perform access control on the user, for example, determine whether the user is assigned a defined security role. Determine whether the user is allowed to do something

  3. Use the Session API in any environment, even without a Web container

  4. React to events during authentication, access control, or during the life cycle of a session

  5. A data source that aggregates one or more user security data as a single composite user “view”

  6. Single sign-on (SSO) function

  7. Enable the “Remember Me” service for users not associated with a login

    , etc.

Apache Shiro is a comprehensive program security framework with many features. The chart below illustrates Shiro’s focus:

Shiro has four cornerstones — authentication, authorization, session management, and encryption.

  1. Authentication: Sometimes referred to simply as “login”, this is the act of proving who the user is.
  2. Authorization: The process of determining who gets access to what.
  3. Session Management: Manages user-specific sessions, even in non-Web or EJB applications.
  4. Cryptography: Keeps data secure and easy to use by using encryption algorithms.

In addition, Shiro provides additional features to address security issues in different environments, especially the following:

  1. Web Support: Shiro’s Web-enabled API makes it easy to help secure Web applications.
  2. Caching: Caching is the first tier citizen in Apache Shiro to ensure that security operations are fast and efficient.
  3. Concurrency: Apache Shiro takes advantage of its Concurrency nature to support multithreaded applications.
  4. Testing: Testing support exists to help you write unit and integration tests.
  5. “Run As” : a feature that allows a user to assume the identity of another user (if allowed), sometimes useful in managing scripts.
  6. “Remember Me” : Remember the user’s identity during the session so that the user only needs to log in when forced.

2. Start with a simple case study

2.1 download shiro

To learn shiro, we first need to download Shiro from shiro’s official website at shiro.apache.org/ and as of this writing, sh… In 2017-2019 there was a pause for more than two years, I once thought that this project gg), this article will use this version. Shiro can also be downloaded from Github. The two source code download addresses are as follows:

1.apache shiro 2.github-shiro

Above, I mainly introduce the source code download with my friends, and did not involve the download of JAR package, JAR package we will use Maven directly then.

2.2 Creating a Demo project

Instead of rushing to write the code, let’s open up the source code we just downloaded, which has a sample directory as follows:

The samples directory is the official demo for us. There is a QuickStart project, which is a Maven project, and we will use this QuickStart to create our own demo project.

1. Create a JavaSE project using Maven and add the following dependencies to the POM file:

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-all</artifactId>
	<version>RELEASE</version>
</dependency>
Copy the code

2. Configure users

Using the shro. ini file in the QuickStart project, we can configure a user as follows: First create a shro. ini file in the Resources directory with the following contents:

[users]
sang=123,admin
[roles]
admin=*
Copy the code

The above configuration indicates that we have created a user named Sang, whose password is 123, whose role is admin, and admin has permission to operate all resources.

3. Log in

OK, now that we’ve done the above steps, we can look at how to implement a simple login operation. We will use the quickStart class for this login. First we will create a SecurityManager through shro.ini and set this SecurityManager to singleton mode as follows:

Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Copy the code

After this, we have configured a basic Shiro environment. Note that the user and role information is configured in the shiro.ini configuration file. We can then obtain a Subject that is our current user object as follows:

Subject currentUser = SecurityUtils.getSubject();
Copy the code

HttpSession (HttpSession); HttpSession (HttpSession); HttpSession (HttpSession);

/ / get the session
Session session = currentUser.getSession();
// Set the session property value
session.setAttribute("someKey"."aValue");
// Get the session attribute values
String value = (String) session.getAttribute("someKey");
Copy the code

If the isAuthenticated method returns false, the user is not logged in, so you can log in. If the isAuthenticated method returns false, you can log in. The login method is as follows:

if(! currentUser.isAuthenticated()) { UsernamePasswordToken token =new UsernamePasswordToken("sang"."123");
    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 (AuthenticationException ae) {
    }
}
Copy the code

First, the UsernamePasswordToken is constructed, and the two parameters are our user name and password. Then, the login method in the Subject is called to login. When the user name, password, or account lock and other problems occur, the system will inform the caller of these problems by throwing exceptions.

After successful login, we can obtain the user name of the current login user in the following ways:

log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
Copy the code

We can also call the hasRole and isPermitted methods in the Subject to determine whether the current user has a certain role or permission as follows:

if (currentUser.hasRole("admin")) {
    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.");
}
Copy the code

Finally, we can logout of the login using the logout method as follows:

currentUser.logout();
Copy the code

OK, so far, we have briefly introduced the login operation in Shiro through the official case. For the complete case, you can refer to the official demo.

3. Talk about Realm in Shiro

3.1 What is the Login process

First let’s take a look at this login flowchart from Shiro’s official documentation:

Referring to this figure, our login will go through the following steps:

  1. The application code calls the subject.login method to pass in an AuthenticationToken instance created containing the Principals and Credentials of end users (UsernamePasswordToken in the previous example).
  2. The Subject instance, It is usually the DelegatingSubject (or subclass) that delegates the application’s SecurityManager to begin the real validation work by calling securityManager.login(token) (break point in the Login method of the DelegatingSubject class) You can see it.
  3. SubjectManager as a basic part of the “umbrella”, receives the token and simply delegate to internal Authenticator instance by calling the Authenticator, authenticate (token). It’s usually a ModularRealmAuthenticator instance, supporting the coordination of one or more in the authentication Realm instance. ModularRealmAuthenticator essentially provides a PAM – Apache Shiro style paradigm (including in PAM terminology each Realm is a ‘module’).
  4. If the application is configured with more than one Realm, ModularRealmAuthenticator instance will use the configured AuthenticationStrategy attempt to launch the Multi – Realm authentication. Before, during, and after Realms is called for authentication, the AuthenticationStrategy is called so that it can react to the results of each Realm. If only a single Realm is configured, it will be called directly, because there is no need to use AuthenticationStrategy for a single Realm application.
  5. Each configured Realm is used to help see if it supports the AuthenticationToken submitted. If so, the realm-enabled getAuthenticationInfo method will be called with the submitted token.

OK, through the above introduction, I believe that partners have a certain understanding of the whole login process, partners can verify the five steps we mentioned above through the interruption point. In the above five steps, you can see that there is a Realm that does a lot of the work.

3.2 What is Realm

Realms acts as a “bridge” or “connector” between Shiro and your application’s secure data, as explained in the Realm documentation. Shiro looks for many of these things from one or more realms configured for applications when it is actually security-related data such as user account interactions used to perform authentication (login) and authorization (access control). In this sense, a Realm is essentially a specifically secure DAO: it encapsulates the connection details of the data source, making the relevant data available that Shiro needs. When configuring Shiro, you must specify at least one Realm for authentication and/or authorization. SecurityManager may be configured with multiple Realms, but at least one is required. Shiro provides ready-to-use Realms to connect to secure data sources (i.e., directories) such as LDAP, relational databases (JDBC), text configuration sources, like INI and properties files, and more. You can insert your own Realm implementation to represent custom data sources if the Realm does not meet your needs by default.

So let’s take a look at a simple example of what Realm can do. Note that this article builds on the previous example. Start with a custom MyRealm that looks like this:

public class MyRealm implements Realm {
    public String getName(a) {
        return "MyRealm";
    }
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String password = new String(((char[]) token.getCredentials()));
        String username = token.getPrincipal().toString();
        if (!"sang".equals(username)) {
            throw new UnknownAccountException("User does not exist");
        }
        if (!"123".equals(password)) {
            throw new IncorrectCredentialsException("Incorrect password");
        }
        return newSimpleAuthenticationInfo(username, password, getName()); }}Copy the code

The supports method is used to determine the tokens supported by this Realm. The supports method is supported by the getName method. = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = If there is a problem with login authentication, throw an exception, and the exception will be caught when the login is executed. (Note that MyRealm implements the Realm interface, so the username and password will need to be checked manually. Later articles will cover other methods.)

OK, now that MyRealm is created, we need to do a simple configuration to make MyRealm work. Comment out everything in the shiro.ini file and add the following two lines:

MyRealm= org.sang.MyRealm
securityManager.realms=$MyRealm
Copy the code

The first line defines a realm, and the second line passes that definition to the securityManger, where it actually calls the setRealms method of the RealmSecurityManager class. Once you have done that, you can break some key points in MyRealm and execute the main method again to see the login process.

4. Let’s talk more about Realm in Shiro

4.1 Realm Inheritance Relationships

We can see that there are many subclasses of Realm by looking at the class inheritance relationship. Here are some examples:

  1. IniRealm

Perhaps unbeknownst to us, this class was actually used in our second article. This class starts with the following two lines:

public static final String USERS_SECTION_NAME = "users";
public static final String ROLES_SECTION_NAME = "roles";
Copy the code

The two lines of configuration represent the shiro.ini file, where the table under [Users] represents user names, passwords, and roles, and the table under [roles] represents roles and permissions.

  1. PropertiesRealm

PropertiesRealm provides another way to define users and roles, as follows:

user.user1=password,role1 role.role1=permission1

  1. JdbcRealm

This, as the name implies, is to query the user’s role, permissions and other information from the database. Opening the JdbcRealm class, we see the following lines in the source code:

protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
Copy the code

From these rows of preset SQL we can roughly infer the names and fields of the tables in the database. Of course, we can also customize the SQL. JdbcRealm is actually a subclass of AuthenticatingRealm, and we’ll talk more about AuthenticatingRealm later, so we won’t expand it here. Let’s take a closer look at this JdbcRealm.

4.2 JdbcRealm

  1. The preparatory work

To use JdbcRealm, you need to use the database connection pool. Here I use the Druid database connection pool, so first add the following dependencies:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>RELEASE</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>
Copy the code
  1. Database creation

To use JdbcRealm, I first need to create a database. According to the default SQL in JdbcRealm, I define the database table structure as follows:

So I’m going to use foreign keys just to give you a little bit of intuition about the table relationships, but in practice, it depends. Then add a few pieces of test data to the table. Database scripts can be downloaded at github (github.com/lenve/shiro…

  1. Configuration file processing

Then comment out all the configuration in shro.ini and add the following configuration:

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/shiroDemo
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm
Copy the code

The only thing you need to note is that permissionsLookupEnabled must be set to true. Otherwise, JdbcRealm will not query permissions.

  1. test

OK, after completing the steps above, we can test the user login, user roles and user permissions in the same way as in the second article.

  1. Custom query SQL

If you want to create a custom SQL query, you can create a custom SQL query. I’ll take a simple example here, like if I want to customize the authenticationQuery pair for the corresponding SQL, look at the JdbcRealm source code, Select password from users where username =? If I need to change my table name to employee instead of Users, add the following configuration to shro.ini:

jdbcRealm.authenticationQuery=select password from employee where username = ?
Copy the code

OK, this buddy down to try, I will not demonstrate here.

5. Multi-realm authentication policy in Shiro

5.1 Multi-Realm Authentication Policy

I don’t know if you remember this login flowchart:

We can clearly see from this diagram that there can be multiple realms, but so far all of our cases have been single realms, so let’s look at a simple multi-realm case first.

In the previous article we created a MyRealm and also used JdbcRealm, but both were used separately. Now I want to use both together by modifying the shiro. Ini configuration as follows:

MyRealm= org.sang.MyRealm

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/shiroDemo
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
securityManager.realms=$jdbcRealm,$MyRealm
Copy the code

MyRealm getAuthenticationInfo = sang/123; MyRealm getAuthenticationInfo = sang/123;

public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    String password = new String(((char[]) token.getCredentials()));
    String username = token.getPrincipal().toString();
    if (!"A little Rain in the South".equals(username)) {
        throw new UnknownAccountException("User does not exist");
    }
    if (!"456".equals(password)) {
        throw new IncorrectCredentialsException("Incorrect password");
    }
    return new SimpleAuthenticationInfo(username, password, getName());
}
Copy the code

At this point, we configured both realms, and used our original test code to log in. At this point, we found that WE could log in using either Jiangnan Little Rain /456 or Sang /123. After logging in with sang/123, the user’s role information was the same as before. The user has no role after logging in successfully with Jiangjiangyuyu /456, which is understandable because we have not configured any permissions for the user in MyRealm. Now that I have two realms, I only need one of these realms to be authenticated for my current user.

5.2 Principle Tracing

Well, with the above questions, then we are on the Subject of the login method breakpoints, following the execution of the program step, we come to the ModularRealmAuthenticator class doMultiRealmAuthentication method, as follows:

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    this.assertRealmsConfigured();
    Collection<Realm> realms = this.getRealms();
    return realms.size() == 1?this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken):this.doMultiRealmAuthentication(realms, authenticationToken);
}
Copy the code

In this method, first will get the current how many a realm, if only one is executed doSingleRealmAuthentication method for processing, if there are multiple realm, execute doMultiRealmAuthentication method for processing. DoSingleRealmAuthentication methods part of the source code is as follows:

protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {... . AuthenticationInfo info = realm.getAuthenticationInfo(token);if(info == null) {
        String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "].";
        throw new UnknownAccountException(msg);
    } else {
        returninfo; }}Copy the code

GetAuthenticationInfo = getAuthenticationInfo = getAuthenticationInfo = getAuthenticationInfo

What if there are multiple realms? Let’s take a look at doMultiRealmAuthentication method implementation, part of the source code is as follows:

protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
    AuthenticationStrategy strategy = this.getAuthenticationStrategy();
    AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
    Iterator var5 = realms.iterator();
    while(var5.hasNext()) {
        Realm realm = (Realm)var5.next();
        aggregate = strategy.beforeAttempt(realm, token, aggregate);
        if(realm.supports(token)) {
            AuthenticationInfo info = null;
            Throwable t = null;
            try {
                info = realm.getAuthenticationInfo(token);
            } catch (Throwable var11) {
            }
            aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
        } else {
            log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
        }
    }
    aggregate = strategy.afterAllAttempts(token, aggregate);
    return aggregate;
}
Copy the code

Here I mainly talk about the realization of this method:

  1. Get the multi-realm authentication policy first

  2. Build an AuthenticationInfo to store the information that will be returned later after successful authentication

  3. Walk through a Realm and call the getAuthenticationInfo method in each Realm to see if authentication succeeds

  4. After each time AuthenticationInfo is retrieved, the afterAttempt method is called to merge the results

  5. After traversing all realms, afterAllAttempts are called to merge the results and determine if none of them match

5.3 Configuring authentication Policies freely

Shiro supports three different authentication policies, as follows:

  1. AllSuccessfulStrategy, which means that all realms are authenticated successfully

  2. AtLeastOneSuccessfulStrategy, said that as long as there is a Realm authentication success even if authentication is successful, that this policy by default

  3. FirstSuccessfulStrategy, which means that authentication is successful if the first Realm authentication is successful

The configuration mode is also very simple. In shiro. Ini, add the following configuration based on the above configuration:

authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
Copy the code

At this point, the login test will require each Realm to be authenticated.

6. Password encryption in Shiro

6.1 Why Should Passwords Be Encrypted

On December 21, 2011, a database containing 6 million CSDN user data was published on the network. The data were all stored in plain text, including user names, passwords and registered email addresses. After the incident, CSDN issued a statement on weibo, official website and other channels, explaining that the database was used for backup in 2009, but was leaked for unknown reasons and had been reported to the police. Later, he issued a public apology letter on the official website. In the following ten days, kingsoft, netease, JINGdong, Dangdang, Sina and other companies were involved in the incident. The most shocking thing in the whole event is that CSDN stores user passwords in plain text. Since many users share the same password with multiple websites, the disclosure of one website’s password will cause great security risks. Because of all this, when we build systems now, we encrypt our passwords.

Password encryption A hash function, also known as a hash algorithm or hash function, is a method of creating small digital “fingerprints” from any kind of data. A hash function compresses a message or data into a summary, making the amount of data smaller and the format of the data fixed. This function scrambles and mixes the data to recreate a fingerprint called a hash value. A hash value is usually represented by a short random string of letters and numbers. Good hash functions have fewer hash collisions in the input field. In hash tables and data processing, not suppressing conflicts to differentiate data can make database records harder to find. We commonly use the following hash functions:

  1. MD5 message digest algorithm

The MD5 message digest algorithm is a widely used password hash function that produces a 128-bit (16-byte) hash value to ensure complete and consistent transmission of information. Designed by American cryptographer Ronald Livest, MD5 was unveiled in 1992 as a replacement for the MD4 algorithm. The procedure of this algorithm is regulated in RFC 1321. The conversion of an operation on a piece of data, such as a piece of text, to another fixed-length value is the basis of a hash algorithm. Since 1996, there have been proven weaknesses that can be cracked, and for data that requires a high degree of security, experts generally recommend switching to other algorithms, such as SHA-2. In 2004, MD5 was proved to be unable to prevent collisions and therefore not suitable for security authentication such as SSL public key authentication or digital signatures.

  1. Secure hashing algorithm

Secure Hash Algorithm is a family of cryptographic Hash functions certified by FIPS. An algorithm that computes a fixed – length string (also called message digest) corresponding to a numeric message. And if the input messages are different, there is a high probability that they will correspond to different strings. The SHA family of algorithms, designed by the NATIONAL Security Agency of the United States and published by the National Institute of Standards and Technology, is a GOVERNMENT standard in the United States. Released in 1995, SHA-1 is widely used in many security protocols, including TLS and SSL, PGP, SSH, S/MIME, and IPsec, and was once considered the successor to MD5. However, sha-1 security has not been accepted in most encryption scenarios since 2000. In 2017, Dutch cryptography research group CWI and Google officially announced that they had cracked SHA-1; Sha-2 was released in 2001 and includes SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, and SHA-512/256. While there has been no effective attack on SHA-2 so far, its algorithm remains largely similar to SHA-1’s; So some people are developing alternative hashing algorithms; Sha-3 was officially released in 2015. Sha-3 is not intended to replace SHA-2, as sha-2 currently has no significant weaknesses. Because of the success of the MD5 hack, as well as the theoretical hack of SHA-0 and SHA-1, NIST felt the need for a different, alternative cryptography hashing algorithm, now known as SHA-3.

6.2 How to Encrypt in Shiro

Shiro supports both of the above hashing algorithms. For MD5, Shiro generates message digest as follows:

Md5Hash md5Hash = new Md5Hash("123".null.1024);
Copy the code

The first parameter is the plaintext of the password to generate, the second parameter is the salt value of the password, and the third parameter is the number of iterations to generate the message digest.

Shiro supports secure hashing algorithms as follows (multiple algorithms are supported, here is an example) :

Sha512Hash sha512Hash = new Sha512Hash("123".null.1024);
Copy the code

The meanings of the three parameters are basically the same as those above. Shiro also provides a general algorithm as follows:

SimpleHash md5 = new SimpleHash("md5"."123".null.1024);
SimpleHash sha512 = new SimpleHash("sha-512"."123".null.1024);
Copy the code

When the user registers, we can encrypt the password in the above way and store the encrypted string in the database. For the sake of simplicity, I will not write the registration function here. I will change the password 123 of the user in the database yesterday to the string corresponding to sha512, as follows:

cb5143cfcf5791478e057be9689d2360005b3aac951f947af1e6e71e3661bf95a7d14183dadfb0967bd6338eb4eb2689e9c227761e1640e6a033b872 5fabc783Copy the code

In the meantime, to avoid interference from other realms, I only configure one JdbcRealm in the database.

If I don’t make any other changes at this point, the login will definitely fail for a simple reason: the password I entered is 123, but the database password is a long string, so the login will definitely fail. From the break point, we can see that the final password match is performed in the doCredentialsMatch method of the SimpleCredentialsMatcher class in a very simple way, The password entered by the user and the password in the database are directly used to generate byte arrays and then compared. The final comparison is made in the isEqual method of the MessageDigest class. Part of the logic is as follows:

protected boolean equals(Object tokenCredentials, Object accountCredentials) {... .// Gets an array of bytes for the user's password
        byte[] tokenBytes = this.toBytes(tokenCredentials);
        // Get a byte array of database passwords
        byte[] accountBytes = this.toBytes(accountCredentials);
        returnMessageDigest.isEqual(tokenBytes, accountBytes); . }Copy the code

MessageDigest’s isEqual method is as follows:

public static boolean isEqual(byte[] digesta, byte[] digestb) {
    if (digesta == digestb) return true;
    if (digesta == null || digestb == null) {
        return false;
    }
    if(digesta.length ! = digestb.length) {return false;
    }

    int result = 0;
    // time-constant comparison
    for (int i = 0; i < digesta.length; i++) {
        result |= digesta[i] ^ digestb[i];
    }
    return result == 0;
}
Copy the code

All are easy to understand comparison code, I won’t go over here. We failed to log in because we did not encrypt the password entered by the user. After analyzing the source code, We found that because we got a password matcher called SimpleCredentialsMatcher in the assertCredentialsMatch method of the AuthenticatingRealm class, the matching method in this password matcher is a simple comparison, So it would be nice if we could replace this password comparator. Let’s look at the CredentialsMatcher inheritance:

We find that this one happens to have a Sha512CredentialsMatcher matcher, whose doCredentialsMatch method is in its parent HashedCredentialsMatcher. The method contents are as follows:

public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenHashedCredentials = hashProvidedCredentials(token, info);
    Object accountCredentials = getCredentials(info);
    return equals(tokenHashedCredentials, accountCredentials);
}
Copy the code

Instead of getting tokenHashedCredentials as simple as before, we call the hashProvidedCredentials method, which ends up in the following overloaded method:

protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
    String hashAlgorithmName = assertHashAlgorithmName();
    return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
}
Copy the code

These lines of code are familiar, and it is clear that the system has converted the password entered by the user. With that in mind, all I need to do is change shiro. Ini to look like this:

sha512=org.apache.shiro.authc.credential.Sha512CredentialsMatcher
# number of iterations
sha512.hashIterations=1024
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/shiroDemo
dataSource.username=root
dataSource.password=123
jdbcRealm.dataSource=$dataSource
jdbcRealm.permissionsLookupEnabled=true
# Modify the credentialsMatcher attribute in JdbcRealm
jdbcRealm.credentialsMatcher=$sha512
securityManager.realms=$jdbcRealm
Copy the code

After that, we will test the login and the login will be successful.

This section case download: github.com/lenve/shiro…

7. Salt passwords in Shiro

7.1 Why Salt the Password

Whether it is a message digest algorithm or a secure hash algorithm, if the original text is the same, the ciphertext will be the same. In this case, if two users’ passwords are the same in the original text, the ciphertext will be the same in the database, or it is not secure, we need to do further processing, the common solution is to add salt. Where does the salt come from? We can either use a user ID (because in general, the user ID is unique) or we can use a random character. I’m going with the first option.

7.2 How is salt added in Shiro

Shiro adds salt simply by adding it to the password ciphertext generated during user registration in the following ways:

Md5Hash md5Hash = new Md5Hash("123"."sang".1024);
Sha512Hash sha512Hash = new Sha512Hash("123"."sang".1024);
SimpleHash md5 = new SimpleHash("md5"."123"."sang".1024);
SimpleHash sha512 = new SimpleHash("sha-512"."123"."sang".1024)
Copy the code

Then we first put the string generated by SHA512 into the database. Next I need to configure my jdbcRealm because I need to specify what my salt is. In this case, my salt is my username. Each user’s username is different, so there is no way to write it out. In JdbcRealm, the system provides four different saltstyles, as follows:

SaltStyle meaning
NO_SALT By default, the password is unsalted
CRYPT Passwords are stored with Unix encryption
COLUMN The SALT is a separate column stored in the database
EXTERNAL The salt is not stored in the database and needs to be obtained through the jdbCrealm.getSaltForUser (String) function

Four different saltstyles correspond to four different password handling methods, part of the source code is as follows:

switch (saltStyle) {
case NO_SALT:
    password = getPasswordForUser(conn, username)[0];
    break;
case CRYPT:
    // TODO: separate password and hash from getPasswordForUser[0]
    throw new ConfigurationException("Not implemented yet");
    //break;
case COLUMN:
    String[] queryResults = getPasswordForUser(conn, username);
    password = queryResults[0];
    salt = queryResults[1];
    break;
case EXTERNAL:
    password = getPasswordForUser(conn, username)[0];
    salt = getSaltForUser(username);
}
Copy the code

In the case of COLUMN, the SQL query result should contain two columns, the first COLUMN is the password and the second COLUMN is the salt. The default SQL execution here is defined at the beginning of JdbcRealm as follows:

protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
Copy the code

The default salt is provided by password_salt in the table, but the default salt is provided by username, so I will need to customize this SQL later. To customize, simply modify the shiro.ini file and add the following two lines:

jdbcRealm.saltStyle=COLUMN
jdbcRealm.authenticationQuery=select password,username from users where username=?
Copy the code

First set saltStyle to COLUMN, and then redefine the SQL corresponding to authenticationQuery. Note that the order of the returned columns is important and cannot be manipulated arbitrarily. After that, the system automatically treats the USERNAME field as the salt.

However, since enumerations are not supported in the INI file, the value of saltStyle is actually an enumeration type, so when we test it, we need to add an enumeration converter to our main method, as follows:

BeanUtilsBean.getInstance().getConvertUtils().register(new AbstractConverter() {
    @Override
    protected String convertToString(Object value) throws Throwable {
        return ((Enum) value).name();
    }

    @Override
    protected Object convertToType(Class type, Object value) throws Throwable {
        return Enum.valueOf(type, value.toString());
    }

    @Override
    protected Class getDefaultType(a) {
        return null;
    }
}, JdbcRealm.SaltStyle.class);
Copy the code

Of course, later on when we integrate Shiro and web projects, we won’t need this converter.

After that, we can test the login again and see that there are no problems.

7.3 How do I Configure Salt for Non-JDBCREALM

JdbcRealm: JdbcRealm: JdbcRealm: JdbcRealm

One drawback of this approach is that we need to do password matching ourselves. Generally, in projects, We customize a Realm by inheriting AuthenticatingRealm or AuthorizingRealm, because both methods override getAuthenticationInfo, and in getAuthenticationInfo, DoGetAuthenticationInfo (); assertCredentialsMatch (); doGetAuthenticationInfo;

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info = getCachedAuthenticationInfo(token);
    if (info == null) {
        // Call doGetAuthenticationInfo to get info, which we implemented ourselves in our custom Realm
        info = doGetAuthenticationInfo(token);
        log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
        if(token ! =null&& info ! =null) { cacheAuthenticationInfoIfPossible(token, info); }}else {
        log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
    }
    if(info ! =null) {
        // After obtaining the info, perform password comparison
        assertCredentialsMatch(token, info);
    } else {
        log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
    }

    return info;
}
Copy the code

For the reasons described above, I first inherit AuthenticatingRealm as follows:

public class MyRealm extends AuthenticatingRealm {
    public String getName(a) {
        return "MyRealm";
    }
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = token.getPrincipal().toString();
        if (!"sang".equals(username)) {
            throw new UnknownAccountException("User does not exist");
        }
        String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7e e470d9e58";
        return newSimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName()); }}Copy the code

Here are a few things I can say about this class:

  1. The user name needs to be queried from the database. If the user information cannot be found, UnknownAccountException is thrown

  2. In the return SimpleAuthenticationInfo, the second parameter is the password, which is normally retrieved from the database and written out here

  3. The third parameter is the salt value, so when SimpleAuthenticationInfo is constructed and returned, Shiro will determine whether the user entered the correct password

The above core step is the third step, the system to automatically compare the password input is correct, in the process of comparison, the password entered by the user needs to be salted encryption, since the salt encryption, will involve credentialsMatcher, The credentialsMatcher we use here is actually the same credentialsMatcher we use in JdbcRealm. Simply add the following line to the configuration file:

MyRealm.credentialsMatcher=$sha512
Copy the code

Sha512 is the same as we defined above, so we won’t repeat it here.

This section case download: github.com/lenve/shiro…

8. Customize Realm with roles and permissions in Shiro

Password encryption and salt partners should have no problem, but the previous several articles brought us a new problem: The IniRealm, JdbcRealm, and MyRealm can be used for user authentication and authorization. The MyRealm can be used for user authentication and authorization. In this article, we’ll take a look at how custom Realms implement authorization.

8.1 Problem Tracing

In the previous article, we did not implement the custom Realm authorization operation, but this does not affect the use of the hasRole method to obtain user permissions. ModularRealmAuthorizer class hasRole method (ModularRealmAuthorizer)

public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
    assertRealmsConfigured();
    for (Realm realm : getRealms()) {
        if(! (realminstanceof Authorizer)) continue;
        if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
            return true; }}return false;
}
Copy the code

What we see here is that we’re going to iterate through all of these realms, and if this realm is an instance of Authorizer, we’re going to do further authorization, if it’s not an instance of Authorizer, we’re going to skip it, And we only have a custom MyRealm that inherits from AuthenticatingRealm, which is obviously not an instance of Authorizer, so it must return false, authorization failed, so to solve the authorization problem, the first step, Let’s make MyRealm an instance of Authorizer.

8.2 Solution

The following is the Authorizer’s inheritance:

AuthorizingRealm: AuthorizingRealm: AuthorizingRealm: AuthorizingRealm: AuthorizingRealm: AuthorizingRealm: AuthorizingRealm: AuthorizingRealm: AuthorizingRealm: AuthorizingRealm: AuthorizingRealm

public abstract class AuthorizingRealm extends AuthenticatingRealm
        implements Authorizer.Initializable.PermissionResolverAware.RolePermissionResolverAware {... }Copy the code

It turns out that AuthorizingRealm is not only the implementation class of Authorizer, but also the implementation class of AuthenticatingRealm we used above. Since AuthorizingRealm is the implementation class of both of these classes, MyRealm > AuthorizingRealm > AuthorizingRealm > MyRealm > AuthorizingRealm > MyRealm > AuthorizingRealm > MyRealm > AuthorizingRealm > MyRealm > AuthorizingRealm > MyRealm > AuthorizingRealm

public class MyRealm extends AuthorizingRealm {
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = token.getPrincipal().toString();
        if (!"sang".equals(username)) {
            throw new UnknownAccountException("User does not exist");
        }
        String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7e e470d9e58";
        return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName());
    }

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Set<String> roles = new HashSet<String>();
        if ("sang".equals(principals.getPrimaryPrincipal().toString())) {
            roles.add("Ordinary user");
        }
        return newSimpleAuthorizationInfo(roles); }}Copy the code

After inheriting AuthorizingRealm, we need to implement the doGetAuthorizationInfo method. In this method, we configure the user’s permissions. For convenience, I directly added the permission of ordinary user. In fact, I should query the permission in the database according to the user name. The query method is not described here.

Through source tracing, we see that the final authorization comes to the AuthorizingRealm class in two methods:

public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
    AuthorizationInfo info = getAuthorizationInfo(principal);
    return hasRole(roleIdentifier, info);
}

protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
    returninfo ! =null&& info.getRoles() ! =null && info.getRoles().contains(roleIdentifier);
}
Copy the code

The logic of these two methods is simple. The getAuthorizationInfo method called in the first method will eventually call our custom doGetAuthorizationInfo method. The second hasRole method receives two parameters, the first is the role requested by the user. The second is the set of roles the user has. A simple contains function determines whether the user has a role.

The doGetAuthorizationInfo method can be modified as follows:

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    Set<String> roles = new HashSet<String>();
    Set<String> permiss = new HashSet<String>();
    if ("sang".equals(principals.getPrimaryPrincipal().toString())) {
        roles.add("Ordinary user");
        permiss.add("book:update");
    }
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
    info.setStringPermissions(permiss);
    return info;
}
Copy the code

Of course, normally, permissions should also come from a query in the database, so LET me simplify this.

So how is the character verified? Tracing the source code leads us to two methods of the AuthorizingRealm class:

public boolean isPermitted(PrincipalCollection principals, Permission permission) {
    AuthorizationInfo info = getAuthorizationInfo(principals);
    return isPermitted(permission, info);
}

//visibility changed from private to protected per SHIRO-332
protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
    Collection<Permission> perms = getPermissions(info);
    if(perms ! =null && !perms.isEmpty()) {
        for (Permission perm : perms) {
            if (perm.implies(permission)) {
                return true; }}}return false;
}
Copy the code

The first isPermitted method called the getAuthorizationInfo method. The getAuthorizationInfo method called the doGetAuthorizationInfo method. The first parameter in the second isPermitted method is the permission the user wants to request.

This section case download: github.com/lenve/shiro…

9. Shiro integrates Spring

9.1 Spring&SpringMVC Environment construction

Spring and SpringMVC environment construction, generally speaking, or relatively easy, because this is not the focus of this article, so I will not do a detailed introduction here, partners can download the source code at the end of the article to view the Spring+SpringMVC environment construction. At the same time, since the integration of MyBatis is much easier, I will not introduce MyBatis in order to reduce the complexity of the project.

For project dependencies, in addition to Spring, SpringMVC, shiro-related dependencies, you also need to add Shiro and Spring integrated JAR, as follows:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>RELEASE</version>
</dependency>
Copy the code

9.2 integrated Shiro

After setting up the Spring+SpringMVC environment and integrating Shiro, we mainly configure two places:

  1. Configure proxy filters in web. XML as follows:
<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/ *</url-pattern>
</filter-mapping>
Copy the code

After that, when DelegatingFilterProxy intercepts all requests, it delegates to the shiroFilter, which is an instance of what we configured in the Spring container in step 2.

  1. Configuring the Spring container

There are at least two beans in the Spring container that need to be configured. One is the shiroFilter in step 1, and the other is the SecurityManager.

<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
</bean>
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.jsp"></property>
    <property name="successUrl" value="/success.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /**=authc
        </value>
    </property>
</bean>
Copy the code

This is a very simple configuration that we will continue to refine in future articles, and I have a few things to say about this configuration:

  1. First we need to configure a securityManager, and that’s where our realm will be configured.

  2. You also need to configure a bean named shiroFilter that matches the name of the proxy filter in web.xml.

  3. In shiroFilter, loginUrl indicates the login page address.

  4. SuccessUrl Indicates the successful login address.

  5. UnauthorizedUrl Indicates the address of an authorization failure.

  6. FilterChainDefinitions /**=authc means that all pages need to be authenticated (logged in) to be accessed.

  7. Authc is actually a filter, which we’ll talk about in more detail later.

  8. The matching characters follow an Ant style path expression, which can be multiple, matching from top to bottom and then no longer matching. For example:

/a/b/*=anon
/a/**=authc
Copy the code

If MY path is/A /b/c then it will match the first filter, anon, but not authc, so the order here is important.

OK, after these configurations are written, create the corresponding JSP file in the webPAP directory as follows:

At this point, we launch the project to visit the browser, and we end up back at the login.jsp page no matter what address we visit, because all pages (even nonexistent addresses) require authentication to be accessible.

Examples in this section: github.com/lenve/shiro…

10. Shiro handles login in three ways

10.1 Preparations

Obviously, no matter what kind of login is required, the database is the same as our previous database. I won’t go into details here (you can download the database script at the end of this article), but I need to configure JdbcRealm first. In applicationContext.xml, I need to configure the data source as follows:

<context:property-placeholder location="classpath:db.properties"/>
<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
    <property name="username" value="${db.username}"/>
    <property name="password" value="${db.password}"/>
    <property name="url" value="${db.url}"/>
</bean>
Copy the code

Now that you have the data source, configure JdbcRealm as follows:

<bean class="org.apache.shiro.realm.jdbc.JdbcRealm" id="jdbcRealm">
    <property name="dataSource" ref="dataSource"/>
    <property name="credentialsMatcher">
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashAlgorithmName" value="sha-512"/>
            <property name="hashIterations" value="1024"/>
        </bean>
    </property>
    <property name="saltStyle" value="COLUMN"/>
    <property name="authenticationQuery" value="select password, username from users where username = ?"/>
</bean>
Copy the code

These attributes in JdbcRealm are basically the same as those in the seventh article of this series. First, we configured the password comparator as HashedCredentialsMatcher, the corresponding algorithm as SHA512, and the password encryption iteration as 1024. Then we configured the salt of the password from the column of the data table. The username column is our salt. These configurations are the same as those in the previous article.

10.2 Customizing Login Logic

To customize the login logic, let’s start with a simple transformation of the login.jsp page:

<form action="/login" method="post">
    <table>
        <tr>
            <td>User name:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>Password:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2"><input type="submit" value="Login"></td>
        </tr>
    </table>
</form>
Copy the code

Then create our logon handler Controller as follows:

@PostMapping("/login")
public String login(String username, String password) {
    Subject currentUser = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
        currentUser.login(token);
        return "success";
    } catch (AuthenticationException e) {
    }
    return "login";
}
Copy the code

If the login is successful, we will go to the SUCCESS page, and if the login fails, we will return to the login page. After these two steps, we also need to modify the filterChainDefinitions property in the shiroFilter to make the /login interface accessible anonymously as follows:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.jsp"></property>
    <property name="successUrl" value="/success.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /login=anon
            /**=authc   Copy the code

With that done, you can go to the login.jsp page to test the login.

Shiro also provides us with two login methods that do not require us to write our own logins. Please continue to read.

10.3 HTTP-based Authentication

Shiro also provides authentication based on HTTP protocol, of course, this authentication also needs the assistance of database, data configuration is the same as above, we only need to modify one configuration, as follows:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="filterChainDefinitions">
        <value>
            /**=authcBasic
        </value>
    </property>
</bean>
Copy the code

This means that all pages are authenticated over HTTP. At this time, we open any page, and the authentication method is as follows:

10.4 Login through Forms

Form logins are similar to HTTP-based logins. They are logins that do not need to write their own logins.

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login"/>
    <property name="successUrl" value="/success.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /**=authc
        </value>
    </property>
</bean>
Copy the code

Configure the login page and the redirect page after a successful login, and ensure that all pages can be accessed only after you have logged in.

Configure the login page request as follows:

@RequestMapping("/login")
public String login(HttpServletRequest req, Model model) {
    String shiroLoginFailure = (String) req.getAttribute("shiroLoginFailure");
    if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
        model.addAttribute("error"."Account does not exist!");
    }
    if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
        model.addAttribute("error"."Incorrect password!");
    }
    return "login";
}
Copy the code

If the login fails, there is a shiroLoginFailure property in the request that stores the name of the exception class for the failed login. By identifying this class name, we can know what caused the login failure.

OK, with these two steps configured, you can go to the login page to test.

10.5 Logging Out

Logging out of the system is simple. It consists of a filter and can be configured as follows:

<property name="filterChainDefinitions">
    <value>
        /logout=logout
        /**=authc
    </value>
</property>
Copy the code

You can logout by requesting access /logout through get.

There are three examples in this section. The download addresses are as follows:

  • Github.com/lenve/shiro…
  • Github.com/lenve/shiro…
  • Github.com/lenve/shiro…

11. Authorization issues in Shiro

11.1 Configuring A Role

This article builds on the previous example, so I’ll still use JdbcRealm for the Realm, so I don’t need to configure the authorization. But the database script here has been updated, partners need to download and re-execute (github.com/lenve/shiro…

There are two users in the database: Sang has the role of admin, and has two permissions of book:* and author: Create; Lisi has the role of user, and has two permissions of User :info and user:delete. Modify shiroFilter as follows:

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login"/>
    <property name="successUrl" value="/success.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="filterChainDefinitions">
        <value>
            /admin.jsp=authc,roles[admin]
            /user.jsp=authc,roles[user]
            /logout=logout
            /**=authc
        </value>
    </property>
</bean>
Copy the code

About the configuration here, I say the following:

  1. UnauthorizedUrl represents the page to be displayed when authorization fails
  2. In filterChainDefinitions, the admin. JSP page can be accessed only after login, and the user must have the admin role. The user.jsp page can also be accessed only after login, and the user must have the user role

11.2 test

In the test, we log in with sang/123 and lisi/123 respectively. After successful login, we can visit user.jsp and admin.jsp respectively to see the effect.

11.3 Configuring Rights

JdbcRealm = jdbcRealm = jdbcRealm = jdbcRealm = jdbcRealm

<property name="permissionsLookupEnabled" value="true"/>
Copy the code

Then configure shiroFilter:

<property name="filterChainDefinitions">
    <value>
        /admin.jsp=authc,roles[admin]
        /user.jsp=authc,roles[user]
        /userinfo.jsp=authc,perms[user:info]
        /bookinfo.jsp=authc,perms[book:info]
        /logout=logout
        /**=authc
    </value>
</property>
Copy the code

It is assumed that userinfo.jsp requires user:info and bookinfo.jsp requires book:info.

Log in with sang/123 and lisi/123 respectively. Then log in to bookinfo.jsp and userinfo.jsp respectively to see the different effects.

This section case download: github.com/lenve/shiro…

12. JSP tags in Shiro

12.1 origin

In the last article, we wrote a lot of hyperlinks in success.jsp like the following:

<h1>Login successful!</h1>
<h3><a href="/logout">The cancellation</a></h3>
<h3><a href="/admin.jsp">admin.jsp</a></h3>
<h3><a href="/user.jsp">user.jsp</a></h3>
<h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3>
<h3><a href="/userinfo.jsp">userinfo.jsp</a></h3>
Copy the code

But for the different identity of the user, not every link is valid, invalid click links will enter the unauthorized pages, so that the user experience is not good, it is better to put the inaccessible links automatically hidden, at the same time, I also hope to be able to convenient for the current logged in user information, etc., considering these requirements, Let’s talk about JSP tags in Shiro.

12.2 Label Introduction

There are not many tags in Shiro, including the following:

  1. shiro:guest

Shiro: Guest tag displays the contents of the guest tag only if you are not currently logged in, as follows:

<shiro:guest> Welcome! </shiro:guest>Copy the code
  1. shiro:user

Shiro :user displays the contents of the label after the user logs in, whether through normal login or Remember Me login, as follows:

<shiro:user> Welcome to <shiro: Principal />! </shiro:user>Copy the code
  1. shiro:principal

Shiro: Principal is used to obtain information about the current login user. The following output is displayed:

4.shiro:authenticated

Compared to Shiro: User, Shiro :authenticated has a smaller scope. Shiro :authenticated is only shown when the user is authenticated and not by Remember Me:

<shiro: Authenticated > <shiro: Principal /> </shiro:authenticated>Copy the code
  1. shiro:notAuthenticated

Shiro :notAuthenticated also displays the content under the condition that the user is notAuthenticated. Different from shiro:guest, shiro:guest does not display the content when authenticated through Remember Me. Shiro :notAuthenticated will display the content (because it is not a tourist at this time, but it is really unauthenticated), as follows:

<shiro:notAuthenticated> user notAuthenticated </shiro:notAuthenticated>Copy the code
  1. shiro:lacksRole

If the user does not have a role, the following information is displayed:

<shiro:lacksRole name="admin"> User does not have the admin role </shiro:lacksRole>Copy the code
  1. shiro:lacksPermission

Display content when the user does not have a certain permission:

<shiro:lacksPermission name="book:info"> User does not have the book:info permission </shiro:lacksPermission>Copy the code
  1. shiro:hasRole

What is displayed when the user has a role:

<shiro:hasRole name="admin">
    <h3><a href="/admin.jsp">admin.jsp</a></h3>
</shiro:hasRole>
Copy the code
  1. shiro:hasAnyRoles

What is displayed when the user has one of multiple roles:

<shiro:hasAnyRoles name="user,aaa">
    <h3><a href="/user.jsp">user.jsp</a></h3>
</shiro:hasAnyRoles>
Copy the code
  1. shiro:hasPermission

What is displayed when a user has a permission:

<shiro:hasPermission name="book:info">
    <h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3>
</shiro:hasPermission>
Copy the code

This section case download: github.com/lenve/shiro…

13. Caching in Shiro

13.1 Adding a Dependency

To use the cache, you first need to add dependencies as follows:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>
Copy the code

13.2 Adding a Configuration File

The ehcache configuration file mainly refers to the official configuration. Create the ehcache. XML file in the resources directory as follows:

<ehcache>
    <diskStore path="java.io.tmpdir/shiro-spring-sample"/>
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />
    <cache name="shiro-activeSessionCache"
           maxElementsInMemory="10000"
           eternal="true"
           overflowToDisk="true"
           diskPersistent="true"
           diskExpiryThreadIntervalSeconds="600"/>
    <cache name="org.apache.shiro.realm.SimpleAccountRealm.authorization"
           maxElementsInMemory="100"
           eternal="false"
           timeToLiveSeconds="600"
           overflowToDisk="false"/>
</ehcache>
Copy the code

These are all common configurations in ehCache, meaning I will not explain, download source notes at the end of the article.

13.3 Cache Configuration

Next, we simply configure the cache in applicationContext as follows:

<bean class="org.apache.shiro.cache.ehcache.EhCacheManager" id="cacheManager">
    <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
    <property name="realm" ref="jdbcRealm"/>
    <property name="cacheManager" ref="cacheManager"/>
</bean>
Copy the code

First configuration EhCacheManager class, specify the cache location, and then introduced in DefaultWebSecurityManager cacheManager can, so, after our cache is used.

13.4 test

Since I’m using JdbcRealm, if I’m using a custom Realm, I can log to see if caching is in use. After JdbcRealm is in use, I can check to see if caching is in use by breaking points. For example, I execute the following code:

subject.checkRole("admin");
subject.checkPermission("book:info");
Copy the code

The breakpoint trace ends up in AuthorizingRealm’s getAuthorizationInfo method, which first checks for data in the cache. If there is data in the cache, The doGetAuthorizationInfo method is not executed (database operations are performed in doGetAuthorizationInfo), and if there is no data in the cache, the doGetAuthorizationInfo method is executed, And in the successful execution of the data saved to the cache (provided that the cache is configured, cache is not null), at this point we through the breakpoint, found to execute the cache without querying the database data, part of the source code is as follows:

protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
    AuthorizationInfo info = null;
    Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
    if(cache ! =null) {
        Object key = getAuthorizationCacheKey(principals);
        info = cache.get(key);
    }
    if (info == null) {
        info = doGetAuthorizationInfo(principals);
        if(info ! =null&& cache ! =null) { Object key = getAuthorizationCacheKey(principals); cache.put(key, info); }}return info;
}
Copy the code

OK, overall the cache configuration in Shiro is pretty simple.

That’s all.

This section can be downloaded from github.com/lenve/shiro…

To be continued…