springboot integrated shiro (full version)

The source code address is added at the end of the article. Friends who report errors can download it
- updated on June 15, 2020

At the request of netizens, the code of this pom file has been pasted completely,
The springboot version uses the latest 2.3.4.RELEASE,
shiro version uses the latest 1.6.0
The pro test is valid and will be updated continuously
- updated on September 20, 2020

Added some common exceptions to shiro
- updated on September 20, 2020

##1. What is Shiro?

Shiro is an open source project under Apache. Shiro is a lightweight framework, which is much simpler and less complex than spring security. The following is the record of my own study.
The official structure is as follows:

##2. Main functions

shiro mainly has three functional modules:

1. Subject: subject, which generally refers to the user.
2. SecurityManager: Security Manager, which manages all subjects and can cooperate with internal security components. (similar to the dispatcher servlet in spring MVC)
3. Realms: used to verify permission information, which generally needs to be implemented by yourself.

3. Subdivision function

1. Authentication: identity authentication / login (account password verification).
2. Authorization: authorization, that is, role or permission verification.
3. Session Manager: session management, session related management after user login.
4. Cryptography: encryption, password encryption, etc.
5. Web Support: web support, integrated web environment.
6. Caching: caching user information, roles, permissions, etc. into a cache such as redis.
7. Concurrency: multithread concurrency verification. When another thread is started in one thread, the permissions can be automatically propagated in the past.
8. Testing: test support;
9. Run As: allow one user to access as another user (if they allow it).
10. Remember Me: after logging in, you don't have to log in next time.

(for more information on what shiro is, please go to the search engine. This article mainly records the integration of springboot and shiro.)
First, create the springboot project, which is not described here.

Upper Code:

Directory structure:

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.wsl</groupId>
    <artifactId>spring-shiro-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-shiro-demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring.shiro.version>1.6.0</spring.shiro.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${spring.shiro.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--Page template dependency-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--Hot deployment dependency-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>    
        </plugins>
    </build>
</project>

User.java (user entity class):

package com.wsl.bean;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

@Data
@AllArgsConstructor
public class User {
    private String id;
    private String userName;
    private String password;
    /**
     * Role set corresponding to user
     */
    private Set<Role> roles;
}

Role.java (entity class corresponding to role):

package com.wsl.bean;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

@Data
@AllArgsConstructor
public class Role {

    private String id;
    private String roleName;
    /**
     * Permission set corresponding to role
     */
    private Set<Permissions> permissions;
}

Permissions.java (entity class corresponding to permission):

package com.wsl.bean;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Permissions {
    private String id;
    private String permissionsName;
}

LoginServiceImpl.java:

package com.wsl.service.impl;

import com.wsl.bean.Permissions;
import com.wsl.bean.Role;
import com.wsl.bean.User;
import com.wsl.service.LoginService;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@Service
public class LoginServiceImpl implements LoginService {

    @Override
    public User getUserByName(String getMapByName) {
        return getMapByName(getMapByName);
    }

    /**
     * Simulate database query
     *
     * @param userName user name
     * @return User
     */
    private User getMapByName(String userName) {
        Permissions permissions1 = new Permissions("1", "query");
        Permissions permissions2 = new Permissions("2", "add");
        Set<Permissions> permissionsSet = new HashSet<>();
        permissionsSet.add(permissions1);
        permissionsSet.add(permissions2);
        Role role = new Role("1", "admin", permissionsSet);
        Set<Role> roleSet = new HashSet<>();
        roleSet.add(role);
        User user = new User("1", "wsl", "123456", roleSet);
        Map<String, User> map = new HashMap<>();
        map.put(user.getUserName(), user);
        
        Set<Permissions> permissionsSet1 = new HashSet<>();
        permissionsSet1.add(permissions1);
        Role role1 = new Role("2", "user", permissionsSet1);
        Set<Role> roleSet1 = new HashSet<>();
        roleSet1.add(role1);
        User user1 = new User("2", "zhangsan", "123456", roleSet1);
        map.put(user1.getUserName(), user1);
        return map.get(userName);
    }
}

Custom Realm is used to query the user's role and permission information and save it to the permission Manager:

CustomRealm.java

package com.wsl.shiro;

import com.wsl.bean.Permissions;
import com.wsl.bean.Role;
import com.wsl.bean.User;
import com.wsl.service.LoginService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private LoginService loginService;

    /**
     * @MethodName doGetAuthorizationInfo
     * @Description Permission configuration class
     * @Param [principalCollection]
     * @Return AuthorizationInfo
     * @Author WangShiLin
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //Get login user name
        String name = (String) principalCollection.getPrimaryPrincipal();
        //Query user name
        User user = loginService.getUserByName(name);
        //Add roles and permissions
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role role : user.getRoles()) {
            //Add role
            simpleAuthorizationInfo.addRole(role.getRoleName());
            //add permission
            for (Permissions permissions : role.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
            }
        }
        return simpleAuthorizationInfo;
    }

    /**
     * @MethodName doGetAuthenticationInfo
     * @Description Authentication configuration class
     * @Param [authenticationToken]
     * @Return AuthenticationInfo
     * @Author WangShiLin
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
            return null;
        }
        //Get user information
        String name = authenticationToken.getPrincipal().toString();
        User user = loginService.getUserByName(name);
        if (user == null) {
            //The corresponding exception will be reported after returning here
            return null;
        } else {
            //The information of authenticationToken and simpleAuthenticationInfo is verified here
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(), getName());
            return simpleAuthenticationInfo;
        }
    }
}

ShiroConfig.java, inject CustomRealm and SecurityManager into the spring container:

package com.wsl.config;

import com.wsl.shiro.CustomRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class shiroConfig {
    
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    //Add your own authentication method to the container
    @Bean
    public CustomRealm myShiroRealm() {
        CustomRealm customRealm = new CustomRealm();
        return customRealm;
    }

    //Permission management and configuration are mainly the management and authentication of Realm
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

    //Filter factory, set the corresponding filter conditions and jump conditions
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //Logout
        map.put("/logout", "logout");
        //Authenticate all users
        map.put("/**", "authc");
        //Sign in
        shiroFilterFactoryBean.setLoginUrl("/login");
        //home page
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //Error page, authentication failed, jump
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

  
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

LoginController.java: we write a simple login method, an index page query method, an add method and an admin method, which correspond to different roles or permissions

package com.wsl.controller;

import com.wsl.bean.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class LoginController {

    @GetMapping("/login")
    public String login(User user) {
        if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
            return "Please enter user name and password!";
        }
        //User authentication information
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
                user.getUserName(),
                user.getPassword()
        );
        try {
            //For verification, you can catch exceptions and return the corresponding information
            subject.login(usernamePasswordToken);
//            subject.checkRole("admin");
//            subject.checkPermissions("query", "add");
        } catch (UnknownAccountException e) {
            log.error("User name does not exist!", e);
            return "User name does not exist!";
        } catch (AuthenticationException e) {
            log.error("Wrong account or password!", e);
            return "Wrong account or password!";
        } catch (AuthorizationException e) {
            log.error("No permission!", e);
            return "No permission";
        }
        return "login success";
    }

    @RequiresRoles("admin")
    @GetMapping("/admin")
    public String admin() {
        return "admin success!";
    }

    @RequiresPermissions("query")
    @GetMapping("/index")
    public String index() {
        return "index success!";
    }

    @RequiresPermissions("add")
    @GetMapping("/add")
    public String add() {
        return "add success!";
    }
}

If the annotation verifies the role and permission, it cannot catch the exception, so it cannot correctly return the error message to the front end. Therefore, I added a class to intercept the exception. The specific code is as follows
MyExceptionHandler.java
package com.wsl.filter;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
@Slf4j
public class MyExceptionHandler {

    @ExceptionHandler
    @ResponseBody
    public String ErrorHandler(AuthorizationException e) {
        log.error("Permission verification failed!", e);
        return "Permission verification failed!";
    }
}

Open web page http://localhost:8080/login?userName=wsl&password=123456

Then enter the index address: http://localhost:8080/index

Change zhangsan account and log in before accessing index
http://localhost:8080/login?userName=zhangsan&password=123456
http://localhost:8080/index

Recently, many people have reported project errors. I put the source code on the code cloud and you can download it yourself:
Source address

Finally, add some common shiro exceptions:

1. AuthenticationException   Authentication exception

Shiro needs to throw an exception if authentication fails during login authentication. AuthenticationException contains the following subclasses:

1.1. Credentialsexception voucher exception

IncorrectCredentialsException               Incorrect voucher
ExpiredCredentialsException                 Voucher expiration

1.2. AccountException account exception

ConcurrentAccessException: concurrent access exception (thrown when multiple users log in at the same time)
UnknownAccountException: unknown account
ExcessiveAttemptsException: the number of authentication times exceeds the limit
DisabledAccountException: Disabled account
LockedAccountException: the account is locked
Unsupported tokenexception: Unsupported Token used

2. AuthorizationException: authorization exception

Shiro needs to throw an exception when authorization fails during login authentication. AuthorizationException contains the following subclasses:

2.1.  UnauthorizedException:

Thrown to indicate that the requested operation or access to the requested resource is not allowed.

2.2. UnanthenticatedException:

An exception is thrown when attempting to perform an authorization operation when successful authentication has not been completed.

Tags: Shiro Spring Spring Boot

Posted on Thu, 23 Sep 2021 01:45:36 -0400 by mgilbert