Integration of Shiro

Set up the environment

Building environments before integration is inevitable

Rely on

<dependencies>
    <! - mysql driver - >
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <! --MyBatis starter-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>
    <! -- Shiro-->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.7.0</version>
    </dependency>
    <! -- thymeleaf-spring-->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
    </dependency>
    <! -- thymeleaf-Java8-->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-java8time</artifactId>
    </dependency>
    <! -- web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
Copy the code

The database

You can write a simple database that holds the user login information.

Let's not write about roles and permissions

SpringBoot configuration file

In this configuration file, configure the connection information of the database and some necessary configurations of MyBatis

# Database connection information
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/my_demo? useUnicode=true&useSSL=true&characterEncoding=utf-8
    username: root
    password: * * * * * * * * * * *
    driver-class-name: com.mysql.cj.jdbc.Driver
  # Turn off thymeleaf cache
  thymeleaf:
    cache: false
# MyBatis related
mybatis:
  # mapper file path
  mapper-locations: classpath:mybatis/mapper/*.xml
  # alias
  type-aliases-package: com.molu.pojo
Copy the code

Controller

Write a Controller Controller view jump

@Controller
public class MyController {
    / / home page
    @RequestMapping({"/","index","index.html"})
    public String toIndex(Model model){
        model.addAttribute("msg"."Hello Shiro");
        return "index";
    }
    / / test page
    @RequestMapping("/user/add")
    public String toAdd(a){
        return "/user/add";
    }
    @RequestMapping("/user/update")
    public String toUpdate(a){
        return "/user/update"; }}Copy the code

ShiroConfig

This profile associates Shiro’s three main classes

@Configuration
public class ShiroConfig {
    // Create a Realm object that is responsible for retrieving login information from the database, meaning that authentication and authorization are handled by it
    @Bean
    public UserRealm getUserRealm(a){
        return new UserRealm();
    }
    This object is the principal manager, which manages a number of principals and needs to inject our Realm into it for authentication and authorization
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getUserRealm")UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        / / into the Realm
        securityManager.setRealm(userRealm);
        return securityManager;
    }
    // Create a filter factory object
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // Set the managed objects
        bean.setSecurityManager(securityManager);
        // Add Shiro built-in filters
        LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
        // "/user/add path access must be authenticated"
        filterMap.put("/user/add"."authc");
        bean.setFilterChainDefinitionMap(filterMap);
        // Set the login page for unauthenticated login
        bean.setLoginUrl("/toLogin");
        returnbean; }}Copy the code

Realm

This class is responsible for our specific rules for authorization and certification.

Since authorization and authentication are not involved, they are left blank.

// Inherit this interface to override authentication and authorization methods
public class UserRealm extends AuthorizingRealm {
    / / authorization
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    / / certification
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        return null; }}Copy the code

Here the skeleton is built, nothing too difficult.

The simple HTML involved in view jumps is unnecessary.

We start the project

test

The filter

As you can see from the GIF above, both resource paths are freely accessible, authenticated or not.

To block access to a specific path, you can configure the filter chain in ShiroConfig.

@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager securityManager){
    // Get the filter factory instance
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // Set Shiro's core security interface
    bean.setSecurityManager(securityManager);
    // Pass custom filtering rules through map
    LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
    // "/user/add path access must be authenticated"
    filterMap.put("/user/add"."authc");
    bean.setFilterChainDefinitionMap(filterMap);
    return bean;
}
Copy the code

If you are new to Shiro, you may be confused about the value passed into the map.

This “authC” is an instance of Shiro’s built-in filter

Commonly used Shiro filter instances

(1) Anon: anonymous filter, indicating that all resources that pass the URL configuration can be accessed

(2) AuthC: form-based filter, indicating that resources that pass the URL configuration need login authentication

(3) authcBasic: Basic authentication filter, indicating that resources that pass the URL configuration will prompt authentication

(4) PerMS: permission filter, which means that the access to resources that pass the URL configuration will check the corresponding permissions

(5) Port: port filter, which indicates the port number of the resource request that passes the URL configuration

(6) REST: Restful type filter: Restful check is performed on the resources that pass the URL configuration

(7) Roles: Role filter, which checks whether a url configured resource owns the role

(8) SSL: indicates an SSL filter, which indicates that the url configured resources can be accessed only through HTTPS

(9) User: user filter, indicating that login authentication/remember my way can be used to access resources that pass the URL configuration

(10) Logout: Exits the interceptor, indicating that the url configured resource can be jumped after the logout method is executed


As shown in the code above, we added an instance of “authc” filtering for the /user/add request, which means we can no longer access the path as a visitor (anonymous).

If you specify the same URL in different filter instances, Shiro will no longer match the same URL down through the filter instance.

At first glance, it sounds fine and reasonable, but some friends don’t pay attention to it and waste a lot of time.

Give me an example

filterMap.put("/user/**"."authc");
filterMap.put("/user/add"."anon");
Copy the code

As shown in the code above, I want to configure the interceptor instance “authc” for all paths starting with /user, but I want to put the “/user/add” path out again.

Let’s see if the “/user/add” path is accessible

No, although we have configured “anon” for it, it actually requires authentication to access it.

/user/** includes /user/add.

Now that it has been matched by the filter instance “authc” the “anon” instance is automatically invalidated.

So if you want to write wildcards in a path, it is recommended to write them in the background to avoid unnecessary trouble

As some eagle-eyed friends may have noticed, when we access a path that requires authentication as a visitor (anonymous), it redirects us to login.jsp and then 404.

This is setLoginUrl (); Method, which Shiro automatically calls when we are unauthenticated and access a path that requires authentication to access.

If we don’t set setLoginUrl(); By default, it automatically looks for the “/login.jsp” page or “/login” mapping in the root directory of the Web project.

We can also set it to a value that does not follow the default mapping.

/ / for setLoginUri (); Method to jump to this mapping when accessing an unauthorized path
bean.setLoginUrl("/toLogin");
Copy the code

Unlike SpringSecurity, Shiro does not write you a login page. The configured login page mapping must have the specified HTML file

/ / map
@RequestMapping("/toLogin")
public String toLogin(a){
    return "login";
}
Copy the code

Accessing the update path that requires authentication at this point will lead you to the crude login page you just wrote.

certification

With the login page, you now need to perform some simple verification of the user login information from the database to complete the login authentication operation.

Fetching data should not need to be described too much, if the database is not able to forge some login information in memory.

Authentication and authorization are taken care of by the Realm class. If we access a path resource without authentication, we enter doGetAuthenticationInfo(). methods

The corresponding authentication logic is written in this method

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        // Call the Service interface implementation class method to get the user login information in the database
        ShiroUser user = userService.queryUser(userToken.getUsername()); // The implementation class method is to query the user by username
        // If the user name passed in by the front-end login page does not match the user name in the database, the user information cannot be obtained
        if (user==null) {// return null, which throws an exception
            return null;
        }
        // We pass in three parameters to create the instance
        // The second parameter is password. We can pass in the password to find the specified user from the database
        // It compares the password passed in by the front end during authentication and throws an exception if it is inconsistent
        return new SimpleAuthenticationInfo("",user.getPassword(),""); }}Copy the code

After data acquisition and verification, we also need to receive form data from the front end.

// The path must be the same as the form submission path
@PostMapping("/login")
public String login(String username, String password, Model model){
    // Get the current user
    Subject subject = SecurityUtils.getSubject();
    // Encapsulates user login information into token instances
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
        // Perform the login operation
        subject.login(token);
        // Forward to the home page
        return "/index";
    // Exception correlation
    }catch (UnknownAccountException e){
        // If the user name does not match the data in the database, carry MSG
        model.addAttribute("msg"."Abnormal user name");
        // Forward to the login page
        return "/login";
     / / similar
    }catch (IncorrectCredentialsException e){
        model.addAttribute("msg"."Incorrect password");
        return "/login"; }}Copy the code

Shiro is smart enough to throw an exception exactly if you write the logic right in your Realm class.

We use the root user in the database to complete the three operations of incorrect password, incorrect user name and successful login.

No problem, the most basic user authentication will do.

After authentication, you can access the resource path whose filter instance is “AuthC”.

authorization

Logged-in users can access different paths, which can also be done by configuring the Realm class.

Some examples of Shiro’s built-in filters were mentioned above. The perms-related one is “perms”, which checks the permissions the current user is carrying

As with “authC”, we can configure an instance of a “perms” filter for some paths so that access to the resource path requires associated permissions.

Write an admin.html and configure the filter instance "perms" for the mapping of the resource path

// The user must have the "user:admin" permission to access the resource path
filterMap.put("/user/admin"."perms[user:admin]");
Copy the code

Well, after writing this line of code, an authenticated user cannot access the resource if he or she does not carry the permission.

A 401 error will be reported when accessing, indicating no permission.

As well as setting up unauthenticated path jumps, we can also set up unauthorized path jumps through the ShiroFilterFactoryBean instance.

// Set unauthorized path jump
bean.setUnauthorizedUrl("/unauth");
Copy the code

The same thing, the HTML file and the Controller.

We can also write a logout request on a page that is not authorized to jump

    // Unauthorized path mapping
    @RequestMapping("/unauth")
    public String Unauthorized(a){
        // Redirect to the HTML file
        return "/unauth";
    }
    // This path is mapped to the corresponding path in the unauth HTML file
    @RequestMapping("/logout")
    public String logout(a){
        Subject subject = SecurityUtils.getSubject();
        // Call the logout method
        subject.logout();
        // Log out and forward to the home page
        return "index"; }}Copy the code

Grant permissions to users in doGetAuthorizationInfo(); Method.

Note that this method and doGetAuthenticationInfo(); It looks very similar. Pay attention to the distinction.

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    // Get the authorization instance, which is the implementation class of the AuthorizationInfo interface
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    // Invoke the authorization method from this instance to grant "user:admin" permission to all users who enter the method (doGetAuthorizationInfo)
    info.addStringPermission("user:admin");
    // return authorization instance
    return info;
}
Copy the code

After these lines of code are written, all authenticated users will have access to the admin resource path only and will enter this method.

They will all be granted “user:admin” privileges, in other words, all of them will have administrator privileges.

This is obviously not possible, we should only give administrator privileges to certain users (root).

Regardless of how to do this, we will create a new column perms in the database and add the perms value “user:admin” to the root user.

After adding the column, remember to update the entity class.

Our user information is placed in doGetAuthenticationInfo(); Method, while the authorization is in doGetAuthorizationInfo(); Method.

So, is there a good way to do that when you’re checking, doGetAuthorizationInfo(); How to get user data?

Of course there is. In the previous password verification, we wrote this line of code.

SimpleAuthenticationInfo is the implementation class of the AuthenticationInfo interface

Creating this instance requires passing in three (or four) parameters, and we left the other two empty except for password.

Let’s look at the first parameter, principal, which can be a username or a User object.

// Pass the user object as the first argument
return new SimpleAuthenticationInfo(user,user.getPassword(),"");
Copy the code

The user object encapsulates all the data corresponding to the username passed by token.

If we log in as root, the perms of root will also be encapsulated.

So all we need to do is go to doGetAuthorizationInfo(); Method takes the perms value encapsulated by the User object and grants it as permission.

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // Get the current user instance
        Subject subject = SecurityUtils.getSubject();
        // Call the method in the subject instance to get the user Object (strong, Object by default)
        ShiroUser currentUser = (ShiroUser) subject.getPrincipal();
        // Call the authorization method to get the perms value encapsulated in the User object
        info.addStringPermission(currentUser.getPerms());
        // return authorization instance
        return info;
    }
Copy the code

The database has only root userspermsColumns can be fetched"user:admin"In this way, only the root user can be granted the admin permission.

I have to say Shiro is very well designed

So that’s the end of it, there’s still a lot left to do

After all, SpringBoot is integrating Shiro, not Shiro, so I’ll write about it later


Relax your eyes

Original picture P station address

Painters home page