Basic operation of shiro based on springboot

Posted by marian on Thu, 03 Feb 2022 14:58:35 +0100

Shiro execution logic

  • Application Code: written application
  • Subject: subject, which represents the current "user". This user is not necessarily a specific person. Anything interacting with the current application is a subject, such as web crawler, robot, etc; An abstract concept; All subjects are bound to SecurityManager, and all interactions with subjects will be delegated to SecurityManager; You can think of subject as a facade; The SecurityManager is the actual executor;
  • SecurityManager: Security Manager; That is, all security related operations will interact with the SecurityManager; And it manages all subjects; It can be seen that it is the core of Shiro. It is responsible for interacting with other components introduced later. If you have studied spring MVC, you can regard it as the front-end controller of dispatcher servlet;
  • Realm: in realm, Shiro obtains security data (such as user, role and permission) from realm, which means that if SecurityManager wants to verify user identity, it needs to obtain corresponding users from realm for comparison to determine whether the user identity is legal; You also need to get the corresponding role / authority of the user from realm to verify whether the user can operate; You can think of realm as a DataSource, that is, a secure data source.

The simplest Shiro application

  1. The application code is authenticated and authorized through the Subject, which is entrusted to the SecurityManager;
  2. We need to inject real into Shiro's SecurityManager so that the SecurityManager can obtain legal users and their permissions for judgment.

Shiro's organizational structure

As can be seen from the figure, Security Manager is the most complex system

Security Manager internal

  • Authenticator: authenticator, which is responsible for principal authentication. This is an extension point. If users think Shiro's default is not good, they can customize the implementation; It needs Authentication Strategy, that is, when the user authentication is passed;
  • Authorizer: authorizer or access controller, which is used to determine whether the subject has permission to perform corresponding operations; That is, it controls which functions users can access in the application;
  • SessionManager: if you have written servlets, you should know the concept of session. Session needs someone to manage its life cycle. This component is SessionManager; Shiro can be used not only in the Web environment, but also in ordinary JavaSE environment, EJB and other environments; Therefore, Shiro abstracts a session of its own to manage the data interaction between the subject and the application; In this case, for example, when we used it in the Web environment, it was a Web server at the beginning; Then I went to an EJB server; At this time, if you want to put the session data of the two servers in one place, you can realize your own distributed session (such as putting the data on the Memcached server);
  • SessionDAO: DAO has been used by everyone. Data access objects and CRUD for sessions. For example, if we want to save sessions to the database, we can implement our own SessionDAO and write to the database through JDBC; For example, if you want to put a Session into Memcached, you can implement your own Memcached SessionDAO; In addition, in SessionDAO, Cache can be used for caching to improve performance;
  • CacheManager: cache controller to manage the cache of users, roles, permissions, etc; Because these data are rarely changed, putting them in the cache can improve the performance of access
  • Cryptography: password module. Shiro provides some common encryption components, such as password encryption / decryption.

Implementation of domain Realm

There can be one or more realms, which can be considered as the data source of security entity, that is, the data source used to obtain security entity; It can be implemented by JDBC, LDAP or memory; Provided by the user; Note: Shiro doesn't know where your users / permissions are stored and in what format; Therefore, we generally need to implement our own Realm in applications;

Shiro authentication

Authentication, that is, who can prove that he is himself in the application. Generally, some identification information such as their ID is provided to indicate that he is himself, such as ID card and user name / password.

For authentication in shiro, you need to provide principals and credentials

Identity tells shiro what user you are, and the certificate is used to prove your identity.

The most common principals and credentials are user names and passwords

principals = user name / email / mobile number, etc

Credentials = password / verification code / digital credentials, etc

Next, use the springboot environment to demonstrate how Shiro authenticates

demonstration

Prepare environment dependencies

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

Prepare user identity

Shiro The INI configuration file specifies two subject s, one with a user name of wang and a password of 123, and the other with a password of 123

[users]
wang = 123
zhang = 123

Test code

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

    @Test
    public void MainTest(){
//        Initialize the SecurityManager factory, where the ini file is used as the realm
        IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//        Use the factory to generate a SecurityManager instance
        SecurityManager securityManager = factory.getInstance();
//        Bind the sm instance to the tool class provided by shiro. The following operations will take advantage of this tool class
        SecurityUtils.setSecurityManager(securityManager);
//        Obtain the principal, where the principal is two users in the ini file
        Subject subject = SecurityUtils.getSubject();
//        Use the class provided by shiro to create a virtual user to log in
        UsernamePasswordToken token = new UsernamePasswordToken("wang", "123");

        try {
//		  Authenticate identity
            subject.login(token);
        } catch (AuthenticationException e) {
            System.out.println("Wrong user name and password,Error message:"+e.getMessage());
        }
//        Judge whether the user has passed the authentication. Return true if it has passed the authentication and false if it has not
        boolean authenticated = subject.isAuthenticated();
         if(authenticated) System.out.println("Login successful");
        else System.out.println("Login failed");
//        Log out
        subject.logout();
    }
}

Explanation of line 29 error message

If authentication fails, please capture AuthenticationException or its subclasses, such as DisabledAccountException (disabled account), LockedAccountException (locked account), UnknownAccountException (wrong account), ExcessiveAttemptsException (too many login failures), IncorrectCredentialsException (wrong credentials) ExpiredCredentialsException (expired voucher), etc. Please check its inheritance relationship for details; For the error message display of the page, it is best to use "user name / password error" instead of "user name error" / "password error" to prevent some malicious users from illegally scanning the account library;

The steps of authentication can be summarized from the above code:

  1. Collect user identity / credentials, such as user name / password;
  2. Call subject Login. If it fails, you will get the corresponding AuthenticationException exception, and the user will be prompted with error information according to the exception; Otherwise, login succeeds;
  3. Finally, call Subject.. Logout to exit.

Several problems in the above test:

  1. The user name / password is hard coded in the ini configuration file, which needs to be changed into database storage in the future, and the password needs to be encrypted;
  2. The user identity Token may not only be the user name / password, but also others. For example, the user name / email / mobile phone number is allowed to log in at the same time.

It can be seen from the code that these operations seem to be completed by the subject, but in fact, all of them are entrusted by the subject to the securitymanager, and the securitymanager will entrust its own sub module Authenticator to complete the authentication operation. The Authenticator will give the obtained token (user information and credentials) to realm. If realm does not throw an exception, Then the authentication is successful.

It can be seen that the Realm is the final station for authentication. Therefore, we can configure multiple realms to verify the token handed in by the Authenticator for many times

Realm configuration

Realm is a secure data source. We just used the ini file as the data source in the demonstration, so the realm we used is in shiro

org.apache.shiro.realm.text.IniRealm

shiro defines the Realm interface, which is the parent interface of all realms. The code is as follows

String getName(); //Returns a unique Realm name
boolean supports(AuthenticationToken token); //Judge whether this Realm supports this Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
 throws AuthenticationException;  //Obtain authentication information according to Token

We can customize the Realm, and then specify the Realm implementation of securityManager through the ini configuration file, so as to realize multi Realm verification

Create a Shiro multi Realm INI file. Suppose we have customized two realms

Then you can configure multiple realms through the following configuration file

\#Declare a realm
myRealm1 = com.whr.realm.MyRealm1
myRealm2 = com.whr.realm.MyRealm2
\#The $symbol refers to the object defined above
\#Specify the realms implementation of securityManager. This line can be deleted. After deletion, it will be loaded in the order declared above
securityManager.realms=$myRealm1,$myRealm2

The securityManager authenticates in the order specified by realms. Here, we specify the order of realms by displaying the specified order. If "securityManager.realms=$myRealm1,$myRealm2" is deleted, the securityManager will be used in the order declared by realms (that is, it will be found automatically without setting the realms attribute). When we display the specified realms, other unspecified realms will be ignored, If "securityManager.realms=$myRealm1", myRealm2 will not be automatically set.

Real provided by Shiro

Generally, you can inherit the authorizing realm in the future; It inherits AuthenticatingRealm (i.e. authentication) and indirectly cacheingrealm (with cache implementation).

Shiro authorization

Why authorization is required? Because in the page, we don't want people without permission to see some resources. For example, in a background management system login window page, we will have several roles, such as administrator, user, etc. then the pages that administrators and users can see and the operations they perform must be different. At this time, we need to authorize different roles.

Several key objects to understand in authorization:

  • The Subject is the user accessing the application. In Shiro, the Subject is used to represent the user. Users are only allowed to access the corresponding resources after authorization.

  • Resource s are the URL s that users can access in the application, such as accessing JSP pages, viewing / editing some data, accessing a business method, printing text, etc. Users can only access after authorization.

  • Permission is the atomic authorization unit in the security policy. Through permission, we can indicate whether the user has the right to operate a resource in the application. That is, permission indicates whether a user can access a resource in the application, such as accessing the user list page to view / add / modify / delete user data (i.e. CRUD (add query, modify and delete) permission control) and printing documents. As can be seen from the above, permission represents whether the user has the right to operate a resource, that is, whether the operation reflected on a resource is allowed or not, and who will perform the operation. Therefore, it is necessary to give permissions to users later, that is, to define which user is allowed to do what operations (permissions) on a resource. Shiro will not do this, but will be provided by the implementer.

  • Role. Roles represent a set of operations and can be understood as a set of permissions. Generally, we will give users roles rather than permissions, that is, users can have a set of permissions, which is more convenient when granting permissions. Typical roles include project manager, technical director, CTO and development engineer. Different roles have different permissions.

Authorization method

Shiro supports authorization in three ways:

Programming: complete by writing if/else authorization code block:

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
    //Have authority
} else {
    //No permission
}

Annotation: This is accomplished by placing corresponding annotations on the executed Java method:

@RequiresRoles("admin")
public void hello() {
    //Have authority
}

If there is no permission, the corresponding exception will be thrown;

JSP/GSP tag: it is completed through the corresponding tag on the JSP/GSP page:

<shiro:hasRole name="admin">
<!— Have authority to ->
</shiro:hasRole>

demonstration

Role based presentation control

[users]
wang = 123 ,admin,user
zhang = 123 ,user

Two roles are set for wang user, admin and user. zhang user only has user role

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

    /*
    * Use static code blocks to complete the registration of securityManager, but in most cases, you don't need to do so. This is just a test
    * */
    static {
        SecurityManager securityManager = new IniSecurityManagerFactory("classpath:shiro.ini").
                getInstance();
        SecurityUtils.setSecurityManager(securityManager);
    }
    
    @Test
    public void MainTest(){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken zhang = new UsernamePasswordToken("zhang", "123");
        subject.login(zhang);
        if(subject.hasRole("admin")){
            System.out.println("have permission");
        }else{
            System.out.println("don't have permission");
        }
//        Log out
        subject.logout();
    }


}

The above example only demonstrates testing whether this user has a certain role. Next, give permission to this role

[users]
zhang=123,admin,user
wang=123,user
[roles]
admin = admin:create,admin:delete,admin:update
user = user:read
@Test
    public void MainTest(){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken zhang = new UsernamePasswordToken("zhang", "123");
        subject.login(zhang);
        if(subject.hasRole("user")){
            System.out.println("have role");
//            Judge whether this user has delete permission. If not, an error will be reported
            try{
                subject.checkPermission("admin:delete");
            }catch(UnauthorizedException e){
                System.out.println(e.getMessage());
            }
        }else{
            System.out.println("don't have role");
        }
//        Log out
        subject.logout();
    }
//Error reporting information
Subject does not have permission [admin:delete]

Springboot integrated Shiro

All page structures before starting

In POM Add the following three dependencies to XML

		<!--shiro Core package-->
		<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.2</version>
        </dependency>
		<!--shiro And spring Integrated jar package-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.1</version>
        </dependency>
		<!--thymeleaf Dependent package-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

Configure in yml file

spring:
  thymeleaf:
    #Configure the template path. The default is templates. You can not configure it
    prefix: classpath:/templates/
    suffix: .html
    cache: false #Turn off page caching

Customize a MyShiroConfig

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author
 * @description
 */
@Configuration
public class MyShiroConfig {
//        Use the securityManager object to register shiroFilter
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean
            (@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//        Associate SecurityManager
        shiroFilter.setSecurityManager(defaultWebSecurityManager);
        return shiroFilter;
    }
//        Register the SecurityManager object, where the underlying realm uses its own implementation
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//        Associated realm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

//		  Register the realm, which is user-defined
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }
}

Customize a Realm

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
 * @author
 * @description
 */
public class UserRealm extends AuthorizingRealm {

//    Authorize users
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("implement doGetAuthorizationInfo-PrincipalCollection");
        return null;
    }

//    The token of the authenticated user
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("implement doGetAuthorizationInfo-AuthenticationToken");
        return null;
    }
}

The above code customizes the UserRealm, which describes the access object when the SecurityManager needs the realm, and then defines a filter that can intercept the user's request and jump to the page

After getting the data from the front end in the front-end controller, it is encapsulated into a token and delivered to the SecurityManager object. sm then goes to the real to verify the user's identity and authority.

For example, we have the following Controller

 @RequestMapping("/userLogin")
    public String login(String username, String password, Model model){
//        Get the currently logged in user
        Subject subject = SecurityUtils.getSubject();
//        Encapsulate user login data
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try{
//            The login here will deliver the token to the securityManager, sm and then go to UserRealm to authenticate the user
            subject.login(token);
//            Login successful jump
            return "index";
        }catch (UnknownAccountException e){
            //user name does not exist
            model.addAttribute("msg","user name does not exist");
            return "login";
        }catch (IncorrectCredentialsException e){
            //Incorrect password
            model.addAttribute("msg","Password error");
            return "login";
        }
    }

When a user initiates a / userLogin request, the front end encapsulates the user name and password into a token, and calls the login method of the subject to give the token to the SM object, which then accesses the UserRealm to verify the user's identity

Intercept user requests

If you want to intercept user requests, you need to modify shiroFilter in MyShiroConfig class for configuration

@Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean
            (@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(defaultWebSecurityManager);
//      Add shiro's built-in filter to intercept requests
        /*
            anno:Access to the default without authentication
            authc: Authentication is required to access
            user: I have to remember to visit
            perms: You must have permission on a resource to access it
            role:  Access is only possible with the permission of a role
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
/*       
        After configuration, shiro will intercept and authenticate the two requests sent by the user
        filterMap.put("/user/add","authc");
       filterMap.put("/user/delete","authc");
 */
        filterMap.put("/user/*","authc");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
//        If you don't have permission, jump to the login page
        shiroFilter.setLoginUrl("/login");
        return shiroFilter;
    }

At this time, when the user wants to access these two requests, he will jump to the login page, and the login page will initiate the / userLogin request

Authenticated user

This is written in the second overloaded method in UserRealm

//    The token delivered by the authenticated user
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("implement doGetAuthorizationInfo-AuthenticationToken");
//        Assumed user name and password. In the actual development, get from the database
        String username = "root";
        String password = "123456";
//        Since the UsernamePasswordToken is passed in, it needs to be transformed downward
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        if (!token.getUsername().equals(username)) {
//            If the user name does not match the assumed user name, null is returned and an UnKnownAccountException is thrown
            return null;
        }
//        The user name is judged by ourselves, while the password judgment needs to be done by shiro, and shiro will automatically call the password in the token
        return new SimpleAuthenticationInfo("",password,"");
    }

Password encryption

Before password encryption, you need to test the results of the database or assumed password

shiro has two encryption methods

  • SimpleCredentialsMatcher: SimpleCredentialsMatcher directly performs an equality check on the stored user credentials and the user credentials submitted from the AuthenticationToken.

  • Hashedcredentials matcher: instead of storing credentials in their original form and comparing the original data, a safer way to store end-user credentials (such as passwords) is to hash before storing data.

shiro's encryption algorithm

import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {


    @Test
    public void MainTest(){
        Md5Hash md5Hash = new Md5Hash("123456");
        System.out.println("md5Hash.getAlgorithmName()="+md5Hash.getAlgorithmName());//The default is MD5
        System.out.println("md5Hash.getSalt()="+md5Hash.getSalt()); //The default is null
        System.out.println("md5Hash.getIterations()="+md5Hash.getIterations());//The default is 1
//        Encryption results without salt and with hash times of 1
        System.out.println(md5Hash);
//        Add salt. The salt value is generally the user name and the hash number is 1
        md5Hash = new Md5Hash("123456","root");
        System.out.println(md5Hash);
//        Generally, the species are developed, salt is added, and the encryption times are 3 times
        md5Hash = new Md5Hash("123456","root",3);
        System.out.println(md5Hash);

        //4. Use SimpleHash to set MD5 (the above three can be set through this. Here is an example of the number of times of adding salt and hashing)
        //The first parameter is the algorithm name, md5 is specified here, the second is the password to be encrypted, the third parameter is adding salt, and the fourth parameter is the number of hashes
        SimpleHash hash = new SimpleHash("md5", "123456", "root",3);
        System.out.println(hash.toString());
    }


}

to grant authorization

Configure the request address to be authorized by the filter in MyShiroConfig

 @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean
            (@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(defaultWebSecurityManager);
//      Add shiro's built-in filter to intercept requests
        /*
            anno:Access to the default without authentication
            authc: Authentication is required to access
            user: I have to remember to visit
            perms: You must have permission on a resource to access it
            role:  Only with the permission of a role can you access
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
/*
        After configuration, shiro will intercept and authenticate the two requests sent by the user
        filterMap.put("/user/add","authc");
        filterMap.put("/user/delete","authc");
 */
//        Configure permission. The configuration permission must take effect before authentication
        filterMap.put("/user/add","perms[user:add]");
//        Set the delete request to be accessible only with permission. At this time, it will jump to realm
        filterMap.put("/user/delete","perms[user:delete]");
        filterMap.put("/user/*","authc");

        shiroFilter.setFilterChainDefinitionMap(filterMap);
//        If you don't have permission, jump to the login page
        shiroFilter.setLoginUrl("/login");
//        If there is no authorization, jump to the unauthorized page
        shiroFilter.setUnauthorizedUrl("/unauth");
        return shiroFilter;
    }

Modify UserRealm as follows

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;

/**
 * @author
 * @description
 */
public class UserRealm extends AuthorizingRealm {

//    Authorize the user. Every time the filter intercepts perms[user:add], it will jump here for authorization authentication
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("implement doGetAuthorizationInfo-PrincipalCollection");
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

//        If this user has a role, just add role
//        simpleAuthorizationInfo.addRole("role");
        Subject subject = SecurityUtils.getSubject();
//        Through the following two methods, you can get the perms from the authenticated user method
        String perms = (String) subject.getPrincipal();
//        String perms = principalCollection.getPrimaryPrincipal();
//        Give this permission to this user
        simpleAuthorizationInfo.addStringPermission(perms);
        return simpleAuthorizationInfo;
    }

//    The token delivered by the authenticated user
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("implement doGetAuthorizationInfo-AuthenticationToken");
//        Assumed user name and password. In the actual development, get from the database
        String username = "root";
        String password = "123456";
//        It is assumed that the user has only add permission and no delete permission
        String perms = "user:add";
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        if (!token.getUsername().equals(username)) {
            return null;
        }
//        The user's permission perms is transferred to the doGetAuthorizationInfo method through this return value, and the object is transferred in the actual development
        return new SimpleAuthenticationInfo(perms,password,"");
    }
}

At this point, the basic operation of springboot has been demonstrated. In the jsp page, shiro can use the tag to carry out some tests, and in thymeleaf, you can also use the tag, but you need to load a new jar package, which is the jar package integrated by thymeleaf and shiro. I haven't studied it specifically, but I'll talk about it later.

Topics: Java Spring Boot Back-end