Will you not know Shiro in 2021?-3. Analyze the source code of identity authentication to implement a custom Realm

Preface

We already know that no matter whether we are authentication or authorization, the data acquisition comes from Realm. Realm is equivalent to our datasource. In the previous article, we used IniRealm to load our configuration file shiro.ini. At the same time, we It is also said that ini is only a temporary solution. In actual development, it is impossible to put user information and permission information in the ini file. They are all derived from the database. Then the IniRealm provided by the system cannot meet our needs. You need to customize the Realm to realize the real scene. In fact, the ini file is only a strategy for apache to provide us with learning and use. Let's see how to define a Realm by ourselves.

1. The default acquisition of user name and password

The realization of authentication is on one line in the code, as follows:

subject.login(authenticationToken);

The previous code is prepared for this line of code. Didn’t it say that Shiro’s identity authentication is achieved through the Authenticator? Here, it’s just that the subject calls the login method? Doesn't this match the Shiro architecture mentioned earlier? Let's take a look together below.

1. Verification of user name

We debug run the following code:

public class TestAuthenticator {
    
    
    public static void main(String[] args) {
    
    
        //第一步,获取Security Manager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //第二步,使用Security Manager加载配置文件,注意前面已经说过Realm是身份认证与权限的数据获取的地方,所以调用setRealm
        defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        //第三步,使用SecurityUtils获取Subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        //第四步,获取用户登录Token
        UsernamePasswordToken authenticationToken = new UsernamePasswordToken("zhaoyun","daye");
        try {
    
    
            System.out.println(subject.isAuthenticated());
            //第五步,登录
            subject.login(authenticationToken);
            System.out.println(subject.isAuthenticated());
        } catch (UnknownAccountException e) {
    
    
            e.printStackTrace();
        }catch(IncorrectCredentialsException e){
    
    
            e.printStackTrace();
        }catch(Exception e){
    
    
            e.printStackTrace();
        }

    }
}

We know that subject.login() is the core content, we run dubug to enter, and see how the bottom layer of the method implements identity authentication.
After clicking in, we enter the login method in DelegatingSubject, as shown in the following figure: What is
Insert picture description here
entered is not the login method of subject, here DelegatingSubject is actually the implementation class of Subject, but in this method we found that there is no judgment on user information, and Yes, we continue to call with the token, so we continue to click in and take a look. As shown in the figure below, we have entered the login method in the security manager of DefaultSecuityManager, indicating that the underlying subject of the subject is still the authentication method in the security manager. .
Insert picture description here
But obviously, this is not the end, here we continue to pass the Token, we have to continue to follow up, and then we found that we have entered the AuthentticatingSecurityManager, the security manager as shown in the figure below:
Insert picture description here
But even if so many layers have been encapsulated, we found this The token is still not processed in one step, and the call is still continuing. We need to continue to track the call, and then enter the AbstractAuthenticator authenticator as shown below:
Insert picture description here

But this authenticator is not the end, continue to follow up and enter the ModularRealmAuthenticator class, as shown in the figure below:
Insert picture description here
Then we can find that in the last line of code of the method, the method continues to be called but still not processed. After continuing to follow up, we find that it enters When it comes to another method in this class, the method checks whether it supports tokens, etc., and starts to verify the tokens, indicating that we are not far from the destination. Continue to follow up the code and find that we have entered the AutenticatingRealm class. As shown in the figure below:
Insert picture description here
First go to the cache to obtain whether the user represented by the Token has been authenticated. Obviously, there is no data in the cache, so we enter the third line. We need to continue to follow up. Click to enter and enter the doGetAuthenticationInfo of the SimpleAccountRealm class. As shown in the figure below:
Insert picture description here
From the code, we can clearly see that the Token did not continue to be passed down. Obviously, this is the end point of the code call we are looking for. The doGetAuthenticationInfo method of SimpleAccountRealm is the real realization of authentication here. Locally , we can find that Shiro has a deep encapsulation of certification, and it is really difficult to find. Let's take a look at the content of this method, as follows:

   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    
       UsernamePasswordToken upToken = (UsernamePasswordToken)token;
       SimpleAccount account = this.getUser(upToken.getUsername());
       if (account != null) {
    
    
           if (account.isLocked()) {
    
    
               throw new LockedAccountException("Account [" + account + "] is locked.");
           }

           if (account.isCredentialsExpired()) {
    
    
               String msg = "The credentials for account [" + account + "] are expired";
               throw new ExpiredCredentialsException(msg);
           }
       }

       return account;
   }

The first line of code forcibly converts the token to the usernamePasswordToken type. In fact, we use this type.
The second line of code gets the username in the token and passes it to the getUser object of this class and returns a SimpleAccount object. We already know that token.getUserName must be the username we passed in, so what is the purpose of calling getUser with this username? What to use? Let's enter this method and take a look: After
Insert picture description here
entering this method, we find that what this method gets is the information of the ini file we configured, then we know that this method is a method to verify the existence of our user name. We return to the previous level of code. Here is a SimpleAccount object. Obviously, as long as the received object is not null, it means that the user name verification is successful. The following method is to check whether the user is locked and whether the user credentials have expired. Verification. At this point, we found that the user name is correct after the verification, but why the password is not verified?

2. Password verification

In the above, we have found how the user name is verified, but the password is not seen. Now we continue the code just now and go down to find the verification place of the password. F8 executes this method and returns to find that the method finally returns a The SimpleAccount object, since the user name has been verified, the SimpleAccount must be returned for password verification. Follow the code to return the SimpleAccount object here, as shown in the figure below: After the
Insert picture description here
call is returned here, we can find that the user name is put in first After the cache, we will enter the red part of the picture in a hurry. According to the name, we can see that this part is an assertion of whether the credential information matches the user's token and the user found in the ini file, so here it must be The place where the password verification is implemented, let's click in to see the implementation of this part, as shown in the figure below:
Insert picture description here
Obviously the marked line is to determine whether the password matches, and the following IncorrectCredentialsException is also thrown in the unmatched scenario, which is also We said that the password does not match abnormally, and then enter the next layer call from the position marked in the figure, as shown in the following figure:
Insert picture description here
We can find that here are two Object objects generated based on the password in the token and info. These two Object objects are actually It is the password, and then use equals to compare the two objects. If the match is successful, the verification is passed. Otherwise, the verification fails and returns false. Return to the previous level, and an invalid identity credential exception will be thrown. Here we have the user name and The passwords are all verified.

3. Summarize username and password verification

The authentication process of user name and password has been mentioned above, so I won’t repeat it here. Through the above process, we can understand that the user name is verified by the doGetAuthenticationInfo method in SimpleAccountRealm , and the password verification is in AuthenticatingRealm. It is implemented by assertCredentialsMatch in . Observing the verification of the password, we can find that we don’t need to do anything here. We only need to return the user information (including password information) when the user is authenticated. The program will hold the user login information. , Check with the information in the configuration file, if the match is verified, even if we use the database to get the user information, we still return a SimpleAccout information, it is still automatically judged here, so we can get the password No human intervention is required for the verification of the password. Shiro will automatically help us implement the password authentication. If you are familiar with the above call, we will find that the real implementation class of user authentication SimpleAccountRealm is the real implementation class of AuthenticatingRealm. Subclass.

2. Custom Realm

According to the verification process above, we can find that if we want to implement our Realm for so long, we need to inherit AuthenticatingRealm, just like SimpleAccountRealm to implement the doGetAuthenticationInfo method.

1. Custom Realm----FirstRealm

Let's go to top one of our own Realm called FirstRealm.

public class FirstRealm extends AuthenticatingRealm {
    
    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        
        return null;
    }
}

We have defined our own Realm, but now there is nothing in the method, and the returned information is empty, so when the comparison is performed, the unknown user name, which is what we call UnknownAccountException, will be reported. The database obtains the password, and still uses the hard-coded method for comparison. I believe that the basic students will be able to obtain the data from the database. I will not write it here. We refer to the implementation class of SimpleAccountRealm to complete the method, as follows:

public class FirstRealm extends AuthenticatingRealm {
    
    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken;
        String userName = upToken.getUsername();
        //假设这就是从数据库获取的用户信息
        SimpleAccount simpleAccount = getAccountInfo();
        if(simpleAccount.getPrincipals().asList().contains(userName)) {
    
    
            System.out.println("用户名验证通过");
            return simpleAccount;
        }else{
    
    
            return null;
//            throw new UnknownAccountException();
        }
    }

    private SimpleAccount getAccountInfo(){
    
    
        return new SimpleAccount("zhaoyun","1111",this.getName());
//        return  new SimpleAuthenticationInfo("zhaoyun","1111",this.getName());
    }
}

From the code above, we can see that there are two places of code that I commented out. Note that the first place is in the retun null place. Here we can choose to manually throw an exception for an unknown user, or throw a null directly. , The upper-level call will help us throw an unknown user exception, so it doesn’t matter if we don’t throw an exception here. The second code is where we pretend to get database data. The abbreviation here writes two different returns because we use Both of these methods are possible. If you are interested, you can try another one. Of course, the return value should also be changed.

2. Use a custom Realm

When we wrote the code for identity authentication, we injected IniRealm at the time, so we can replace it with Realm that we implemented, as follows:

public class TestAuthenticator {
    
    
    public static void main(String[] args) {
    
    
        //第一步,获取Security Manager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //第二步,使用Security Manager加载配置文件,注意前面已经说过Realm是身份认证与权限的数据获取的地方,所以调用setRealm
        defaultSecurityManager.setRealm(new FirstRealm());
        //第三步,使用SecurityUtils获取Subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        //第四步,获取用户登录Token
        UsernamePasswordToken authenticationToken = new UsernamePasswordToken("zhaoyun","daye");
        try {
    
    
            System.out.println(subject.isAuthenticated());
            //第五步,登录
            subject.login(authenticationToken);
            System.out.println(subject.isAuthenticated());
        } catch (UnknownAccountException e) {
    
    
            e.printStackTrace();
        }catch(IncorrectCredentialsException e){
    
    
            e.printStackTrace();
        }catch(Exception e){
    
    
            e.printStackTrace();
        }

    }
}

Then we run the petitioning code and feel whether the Realm defined by ourselves is working well. The running results are as follows:

false
用户名验证通过
org.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - zhaoyun, rememberMe=false] did not match the expected credentials.
	at org.apache.shiro.realm.AuthenticatingRealm.assertCredentialsMatch(AuthenticatingRealm.java:603)
	at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:581)
	at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)
	at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:273)
	at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
	at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
	at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:275)
	at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:260)
	at org.example.TestAuthenticator.main(TestAuthenticator.java:33)

Process finished with exit code 0

We found that our user name verification has passed. From the error report, we can know that the password is wrong, and then found that our login password is inconsistent with the obtained password. Change the passwords to daye, and we try again, as follows:

false
用户名验证通过
true

Process finished with exit code 0

Then we found that the identity authentication was successful.

Three. Summary

In the first part, we introduced the low-level implementation of the login method in Subject, and figured out how the method achieves identity verification. We found that the bottom of the method is indeed to authenticate the incoming login information through the authenticator, and the authenticator again You need to rely on the identity information in Realm to determine whether the user information is legal. According to the source code, we found the doGetAuthenticationInfo method in SimpleAccountRealm step by step, and found that the user name authentication is done here, and the password authentication is in the assertCredentialsMatch in AuthenticatingRealm. This is done here, and the password authentication does not require us to participate, Shiro will do it for us, so we only need to complete the user authentication and return a SimpleAccount object with user information and password information. According to this idea, we designed our own FirstRealm. We also inherited AuthenticatingRealm like SimpleAccountRealm, and then implemented the doGetAuthenticatonInfo method, where we implemented the user authentication, so that we completed the implementation of the custom Realm. We are in development. This is also the way to implement the Realm defined by yourself, but the injection method of Realm will be different, and follow-up articles will continue to introduce it.

Guess you like

Origin blog.csdn.net/m0_46897923/article/details/114804895