Shiro--SpringBoot--jwt -- use / usage / instance / example

Keywords: Shiro Spring Spring Boot

Original website:

Other web sites

Shiro instance series

Shiro -- instance -- springboot_ CSDN blog
Shiro -- instance -- springboot -- Shiro redis_ CSDN blog
Shiro -- instance -- springboot -- JWT_ CSDN blog (this article)

Reference website

shiro+jwt+springboot integration_ q89757316 blog - CSDN blog
Super detailed! 4 hours to develop a SpringBoot+vue front and rear end separation blog project!!
SpringBoot integrates JWT and Apache Shiro - Zhihu

brief introduction

explain

  1. This article practices the use of Shiro. Try to use the native Shiro configuration and minimize the custom configuration.
  2. Use jwt instead of the default authc as the authentication method, others remain unchanged.
  3. I passed my self-test and the code is available.
  4. This Shiro example is a series, and this article is one of them. The following articles are in this series
    1. Shiro -- instance -- SpringBoot (see: here)
    2. Shiro -- instance -- springboot -- Shiro redis (see: here)
    3. Shiro -- instance -- SpringBoot--jwt (this article)
    4. Shiro -- instance -- SpringBoot--jwt--redis (not started)

Use form

  1. Use jwt instead of the default session to manage permissions.
    1. Customize the jwt filter and register it with the Spring container under the name "authc"
  2. Use roles and resource permissions. (annotation)
  3. Use the Knife4j test (an upgraded version of Swagger).
  4. Use Shiro spring boot web starter: 1.7.0

Technology stack

  1. shiro-spring-boot-web-starter: 1.7.0
  2. spring-boot-starter-parent: 2.3.8.RELEASE
  3. mysql: 8.0
  4. Mybatis plus boot starter: 3.4.3.1 (persistence layer framework)
  5. lombok (simplified code)
  6. Knife4j spring boot starter: 3.0.3 (interface document (upgraded version of swagger))

Business scenario

This paper assumes a mall system.

The roles are as follows

  • admin: have all permissions
    • Use shiro wildcard: *:*
  • productManager: has all permissions for product
    • product:add,product:delete,product:edit,product:view
    • We assign these four permissions to the role of productManager separately. In the actual project, we can assign the permission of product: * to the role of productManager.
  • orderManager: has all the permissions of order
    • order:add,product:delete,product:edit,product:view
    • We assign these four permissions to the role of productManager separately. In the actual project, we can assign the permission of product: * to the role of productManager.

The user password and authority are as follows

user

password

role

Permissions owned

zhang3

12345

admin

All permissions.

li4

abcde

productManager

All permissions for the product.

In order to test the function, the Department grants the following special permissions:

  • Verify that shiro can control permissions through roles alone
    • Set role permissions for the edit order interface:
      • @RequiresRoles(value = {"admin,productManager"}, logical = Logical.OR)
  • Verify that shiro cannot add @ RequiresRoles and @ RequiresPermissions to an interface at the same time
    • Set role permissions and resource permissions for the interface viewing orders:
      • @RequiresPermissions("order:view")
        @RequiresRoles(value = {"admin,productManager"}, logical = Logical.OR)

code

Project structure

Table structure and data

        In order to simplify the code, users, roles and permissions are directly generated to the database with SQL. In actual development, they need to be added, deleted and modified through the Controller.

business.sql

DROP DATABASE IF EXISTS shiro;
CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro;

DROP TABLE IF EXISTS t_user;
DROP TABLE IF EXISTS t_role;
DROP TABLE IF EXISTS t_permission;
DROP TABLE IF EXISTS t_user_role_mid;
DROP TABLE IF EXISTS t_role_permission_mid;

create table t_user (
  id bigint AUTO_INCREMENT,
  user_name VARCHAR(100),
  password VARCHAR(100),
  salt VARCHAR(100),
  PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

create table t_role (
  id bigint AUTO_INCREMENT,
  name VARCHAR(100),
  description VARCHAR(100),
  PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

create table t_permission (
  id bigint AUTO_INCREMENT,
  name VARCHAR(100),
  description VARCHAR(100),
  PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

create table t_user_role_mid (
  id bigint AUTO_INCREMENT,
  user_id bigint,
  role_id bigint,
  PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

create table t_role_permission_mid (
  id bigint AUTO_INCREMENT,
  role_id bigint,
  permission_id bigint,
  PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

-- Password: 12345
INSERT INTO `t_user` VALUES (1,''zhang3'',''a7d59dfc5332749cb801f86a24f5f590'',''e5ykFiNwShfCXvBRPr3wXg=='');
-- password: abcde
INSERT INTO `t_user` VALUES (2,''li4'',''43e28304197b9216e45ab1ce8dac831b'',''jPz19y7arvYIGhuUjsb6sQ=='');
INSERT INTO `t_role` VALUES (1,''admin'',''Super administrator'');
INSERT INTO `t_role` VALUES (2,''productManager'',''Product administrator'');
INSERT INTO `t_role` VALUES (3,''orderManager'',''Order administrator'');
INSERT INTO `t_permission` VALUES (1,''*:*'',''All permissions'');
INSERT INTO `t_permission` VALUES (2,''product:add'',''Add products'');
INSERT INTO `t_permission` VALUES (3,''product:delete'',''Delete product'');
INSERT INTO `t_permission` VALUES (4,''product:edit'',''Edit product'');
INSERT INTO `t_permission` VALUES (5,''product:view'',''View products'');
INSERT INTO `t_permission` VALUES (6,''order:add'',''Add order'');
INSERT INTO `t_permission` VALUES (7,''order:delete'',''Delete order'');
INSERT INTO `t_permission` VALUES (8,''order:edit'',''Edit order'');
INSERT INTO `t_permission` VALUES (9,''order:view'',''View order'');
INSERT INTO `t_user_role_mid` VALUES (1,2,2);
INSERT INTO `t_user_role_mid` VALUES (2,1,1);
INSERT INTO `t_role_permission_mid` VALUES (1,1,1);
INSERT INTO `t_role_permission_mid` VALUES (2,2,2);
INSERT INTO `t_role_permission_mid` VALUES (3,2,3);
INSERT INTO `t_role_permission_mid` VALUES (4,2,4);
INSERT INTO `t_role_permission_mid` VALUES (5,2,5);
INSERT INTO `t_role_permission_mid` VALUES (6,3,6);
INSERT INTO `t_role_permission_mid` VALUES (7,3,7);
INSERT INTO `t_role_permission_mid` VALUES (8,3,8);
INSERT INTO `t_role_permission_mid` VALUES (9,3,9);

Configuration files and dependencies

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/shiro?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 222333

# Mybatis plus print SQL log
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

shiro:
  enabled: true       # Enable shiro. The default value is true
  web:
    enabled: true     # Enable shiro Web. The default value is true
#  loginUrl: /login    # The login address is "login.jsp" by default
#  successUrl: /index  # The address to jump after successful login. The default is "/"
#  unauthorizedUrl: /unauthorized          # Unauthorized default jump address
#  sessionManager:
#    sessionIdCookieEnabled: true          # Whether to allow session tracking through cookies. The default value is true.
#    sessionIdUrlRewritingEnabled: false   # Whether to put JSESSIONID into the url. The default is true.
#  annotations:
#    enabled: true    # Open shiro's annotation. For example: @ RequiresRole.
# There are three ways to open shiro annotation.
#    1. Introduce spring aop dependency: org.springframework.boot: spring boot starter AOP
#    2. Set shiro.annotations.enabled=true in application.yml.
#         Correspondence: ShiroAnnotationProcessorAutoConfiguration.class
#    3. Provide an AuthorizationAttributeSourceAdvisor bean
#  It is strongly recommended to use the first one, because only one of shiro's own AOPs and spring AOPs can be used. If shiro's AOPs are used, there will be many
#  The spring annotation is invalid. For example, @ Async, @ Cacheable

# Custom configuration
custom:
  jwt:
    secret: 7h4alejfloriaj5&asf!a4m   # Key. Write whatever you want
    expire: 1800    # Valid time of token, 30 minutes. Unit: seconds

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 https://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.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>shiro_3_jwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shiro_3_jwt</name>
    <description>shiro_3_jwt</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.7.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Configuration (config package)

Code execution process

login

JwtFilter#onAccessDenied  // Return true normally  
    PathMatchingFilter     // Go to the anon filter and let it go if it's inside  
      Own login interface

Interfaces requiring permissions

JwtFilter#onAccessDenied  // Execute executeLogin(servletRequest, servletResponse);  
   JwtFilter#createToken  
     AccountRealm#doGetAuthenticationInfo  
       AccountRealm#doGetAuthorizationInfo  
          Own interface

shiro general configuration

package com.example.demo.config.shiro;

import com.example.demo.common.constant.WhiteList;
import com.example.demo.config.shiro.filter.JwtFilter;
import com.example.demo.config.shiro.realm.AccountRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        chainDefinition.addPathDefinition("/login", "anon");

        WhiteList.ALL.forEach(str -> {
            chainDefinition.addPathDefinition(str, "anon");
        });

        // all other paths require a logged in user
        chainDefinition.addPathDefinition("/**", "authc");
        return chainDefinition;
    }

    // The name must be authc to replace shiro's default authc.
    @Bean("authc")
    public AuthenticatingFilter authenticatingFilter() {
        return new JwtFilter();
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // Close shiro's own session. In this way, shiro cannot be logged in through session. Later, you will log in with jwt credentials.
        // See: http://shiro.apache.org/session-management.html#SessionManagement-DisablingSubjectStateSessionStorage
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(getDatabaseRealm());
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    @Bean
    public AccountRealm getDatabaseRealm() {
        return new AccountRealm();
    }

    /**
     * setUsePrefix(true)It is used to solve a strange bug. It is as follows:
     *  When spring aop is introduced, add @ RequiresRole and so on to the methods of the class annotated by @ Controller
     *  shiro Annotation will cause the method to be unable to map the request and return 404. Adding this configuration can solve this bug.
     */
    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * Open shiro annotation. For example: @ RequiresRole
     * This method is not used here to open the annotation, but the spring aop dependency is introduced. See the annotation in application.yml for the reason
     */
    /*@Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
                new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }*/

    /**
     * This configuration method will not work in this project.
     */
    /* @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // Authentication failed. The address to jump to.
        // shiroFilterFactoryBean.setLoginUrl("/login");
        // // Link to jump after successful login
        // shiroFilterFactoryBean.setSuccessUrl("/index");
        // // Unauthorized interface;
        // shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/login", "anon");

        WhiteList.ALL.forEach(str -> {
            filterChainDefinitionMap.put(str, "anon");
        });

        // filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/**", "jwtAuthc");

        Map<String, Filter> customisedFilters = new LinkedHashMap<>();
        // Injection cannot be used to set the filter. If injection is used, this filter will have the highest priority (/ * * the highest priority, resulting in the invalidation of all previous requests).
        // springboot All classes that implement the javax.servlet.Filter interface will be scanned without adding @ Component.
        customisedFilters.put("jwtAuthc", new JwtFilter());

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        shiroFilterFactoryBean.setFilters(customisedFilters);

        return shiroFilterFactoryBean;
    }*/
}

Custom Realm

package com.example.demo.config.shiro.realm;

import com.example.demo.rbac.permission.service.PermissionService;
import com.example.demo.rbac.role.service.RoleService;
import com.example.demo.rbac.user.entity.User;
import com.example.demo.rbac.user.service.UserService;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.shiro.entity.JwtToken;
import com.example.demo.common.entity.AccountProfile;
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.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;

import java.util.Set;

public class AccountRealm extends AuthorizingRealm {
    @Lazy
    @Autowired
    private UserService userService;

    @Lazy
    @Autowired
    private RoleService roleService;

    @Lazy
    @Autowired
    private PermissionService permissionService;

    //Authentication scheme to enable realm to support jwt
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    // Login authentication
    // SimpleAuthenticationInfo here can return any value, which will not be used in password verification.
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) token;

        String userId = JwtUtil.getUserIdByToken((String) jwtToken.getPrincipal());
        if (userId == null) {
            throw new UnknownAccountException("token Null, please login again");
        }
        // Get password in database
        User user = userService.getById(userId);
        if (user == null) {
            throw new UnknownAccountException("token Null, please login again");
        }

        AccountProfile accountProfile = new AccountProfile();
        accountProfile.setId(userId);
        accountProfile.setUserName(user.getUserName());

        String salt = user.getSalt();
        // The account and password are stored in the authentication information. getName() is the inheritance method of the current real, and usually returns the current class name: accountreal
        // The salt is also put in and automatically verified by the HashedCredentialsMatcher configured in ShiroConfig
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                accountProfile, jwtToken.getCredentials(), ByteSource.Util.bytes(salt), getName());
        return authenticationInfo;
    }

    // Permission verification
    // Only the default filter in the org.apache.shiro.web.filter.authz package can be used here.
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // If you can enter here, it means that the account has passed the authentication
        AccountProfile profile = (AccountProfile) principalCollection.getPrimaryPrincipal();

        // Obtain roles and permissions through service
        Set<String> permissions = permissionService.getPermissionsByUserId(profile.getId());
        Set<String> roles = roleService.getRolesByUserId(profile.getId());

        // Authorized object
        SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
        // Put in the roles and permissions obtained through the service
        s.setStringPermissions(permissions);
        s.setRoles(roles);
        return s;
    }
}

Custom jwt filter

package com.example.demo.config.shiro.filter;

import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.shiro.entity.JwtToken;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

public class JwtFilter extends AuthenticatingFilter {
    /**
     * All requests will come here (anon or not).
     * Return true: indicates that you are allowed to go down. The PathMatchingFilter will be followed to see if the path corresponds to anon, etc
     * Return false: indicates that you are not allowed to go down.
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest,
                                     ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String token = request.getHeader(HttpHeaders.COOKIE);
        // Custom headers are also OK, but the browser will not save custom headers, which need to be saved by the front end itself
        // String token = request.getHeader("Authentication");

        if (!StringUtils.hasText(token)) {
            return true;
        } else {
            boolean verified = JwtUtil.verifyToken(token);
            if (!verified) {
                return true;
            }
        }

        // This login does not call the login interface, but the shiro level login.
        // Inside, the createToken method below will be called
        return executeLogin(servletRequest, servletResponse);
    }

    /**
     * The token here will be passed to the doGetAuthenticationInfo method of the AuthorizingRealm subclass (AccountRealm here) as a parameter
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest,
                                              ServletResponse servletResponse) {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String token = request.getHeader(HttpHeaders.COOKIE);
        // Custom headers are also OK, but the browser will not save custom headers, which need to be saved by the front end itself
        // String token = request.getHeader("Authentication");
        if (!StringUtils.hasText(token)) {
            return null;
        }
        return new JwtToken(token);
    }
}

Custom jwt token

package com.example.demo.config.shiro.entity;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * JwtToken Instead of the official UsernamePasswordToken, it is the carrier of Shiro's user name, password and other information,
 * The front and back ends are separated, and the server does not save the user state, so functions such as RememberMe are not required.
 */
public class JwtToken implements AuthenticationToken {

    private final String token;

    public JwtToken(String jwt) {
        this.token = jwt;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

Other configurations

Get the configuration in jwt the configuration file

Configuration class

package com.example.demo.config;

import com.example.demo.config.properties.JwtProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JwtConfig {
    @Bean
    @ConfigurationProperties(prefix = "custom.jwt")
    public JwtProperties jwtProperties() {
        return new JwtProperties();
    }
}

Attribute class

package com.example.demo.config.properties;

import lombok.Data;

@Data
public class JwtProperties {
    private String secret;
    private long expire;
}

Knife4j configuration (interface document)

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
 
@Configuration
@EnableSwagger2
public class Knife4jConfig {
    @Bean(value = "defaultApi2")
    public Docket defaultApi2() {
        Docket docket=new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder()
                        .title("My title")
                        .description("My description")
                        // .termsOfServiceUrl("http://www.xx.com/")
                        .contact(new Contact("daoren", "https://knife.blog.csdn.net", "xx@qq.com"))
                        .version("1.0")
                        .build())
                //Group name
                .groupName("all")
                .select()
                //Specify the controller scan path. It can not be specific to the controller. It will scan all the data in the specified path
                .apis(RequestHandlerSelectors.basePackage("com.example.demo"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
}

Permissions (rbac package)

user

service

package com.example.demo.rbac.user.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.rbac.user.entity.User;

public interface UserService extends IService<User> {
    User getUserByUserName(String userName);
}
package com.example.demo.rbac.user.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.rbac.user.entity.User;
import com.example.demo.rbac.user.mapper.UserMapper;
import com.example.demo.rbac.user.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Override
    public User getUserByUserName(String userName) {
        return lambdaQuery().eq(User::getUserName, userName).one();
    }
}

mapper

package com.example.demo.rbac.user.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.rbac.user.entity.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper extends BaseMapper<User> {
}

entity

package com.example.demo.rbac.user.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_user")
public class User {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String userName;

    private String password;

    private String salt;

}

role

service

package com.example.demo.rbac.role.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.rbac.role.entity.Role;

import java.util.Set;

public interface RoleService extends IService<Role> {
    Set<String> getRolesByUserId(String userId);
}
package com.example.demo.rbac.role.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.rbac.role.entity.Role;
import com.example.demo.rbac.role.mapper.RoleMapper;
import com.example.demo.rbac.role.service.RoleService;
import org.springframework.stereotype.Service;

import java.util.Set;

@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {

    @Override
    public Set<String> getRolesByUserId(String userId) {
        Long id = Long.parseLong(userId);
        return this.getBaseMapper().getRolesByUserId(id);
    }
}

mapper 

package com.example.demo.rbac.role.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.rbac.role.entity.Role;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.Set;

@Repository
public interface RoleMapper extends BaseMapper<Role> {
    @Select("SELECT " +
            "    t_role.`name`  " +
            "FROM " +
            "    t_user, " +
            "    t_user_role_mid, " +
            "    t_role " +
            "WHERE " +
            "    t_user.`id` = #{userId}  " +
            "    AND t_user.id = t_user_role_mid.user_id  " +
            "    AND t_user_role_mid.role_id = t_role.id")
    Set<String> getRolesByUserId(@Param("userId")Long userId);
}

entity

package com.example.demo.rbac.role.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_role")
public class Role {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String name;

    private String description;

}

permission

service 

package com.example.demo.rbac.permission.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.rbac.permission.entity.Permission;

import java.util.Set;

public interface PermissionService extends IService<Permission> {
    Set<String> getPermissionsByUserId(String userName);
}
package com.example.demo.rbac.permission.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.rbac.permission.entity.Permission;
import com.example.demo.rbac.permission.mapper.PermissionMapper;
import com.example.demo.rbac.permission.service.PermissionService;
import org.springframework.stereotype.Service;

import java.util.Set;

@Service
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {

    @Override
    public Set<String> getPermissionsByUserId(String userId) {
        Long id = Long.parseLong(userId);
        return this.getBaseMapper().getPermissionsByUserId(id);
    }
}

mapper 

package com.example.demo.rbac.permission.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.rbac.permission.entity.Permission;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.Set;

@Repository
public interface PermissionMapper extends BaseMapper<Permission> {
    @Select("SELECT " +
            "    t_permission.`name`  " +
            "FROM " +
            "    t_user, " +
            "    t_user_role_mid, " +
            "    t_role, " +
            "    t_role_permission_mid, " +
            "    t_permission " +
            "WHERE " +
            "    t_user.`id` = #{userId}  " +
            "    AND t_user.id = t_user_role_mid.user_id  " +
            "    AND t_user_role_mid.role_id = t_role.id " +
            "    AND t_role.id = t_role_permission_mid.role_id " +
            "    AND t_role_permission_mid.permission_id = t_permission.id")
    Set<String> getPermissionsByUserId(@Param("userId") Long userId);
}

entity

package com.example.demo.rbac.permission.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_permission")
public class Permission {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String name;

    private String description;

}

Business (business package)

login

controller

package com.example.demo.business.login.controller;

import com.example.demo.business.login.entity.LoginRequest;
import com.example.demo.business.login.entity.LoginVO;
import com.example.demo.rbac.user.entity.User;
import com.example.demo.rbac.user.service.UserService;
import com.example.demo.common.constant.AuthConstant;
import com.example.demo.common.exception.BusinessException;
import com.example.demo.common.util.auth.JwtUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;

@Api(tags = "Sign in")
@RestController
public class LoginController {
    @Autowired
    private UserService userService;

    @ApiOperation("Sign in")
    @PostMapping("login")
    public LoginVO login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
        String userName = loginRequest.getUserName();
        String password = loginRequest.getPassword();

        User user = userService.getUserByUserName(userName);
        if (user == null) {
            throw new BusinessException("user does not exist");
        }

        String calculatedPassword = new SimpleHash(AuthConstant.ALGORITHM_TYPE,
                password, user.getSalt(), AuthConstant.HASH_ITERATIONS).toString();

        if (!user.getPassword().equals(calculatedPassword)) {
            throw new BusinessException("Username or password incorrect ");
        }

        String token = JwtUtil.createToken(user.getId().toString());

        response.setHeader(HttpHeaders.SET_COOKIE, token);
        // Custom headers are also OK, but the browser will not save custom headers, which need to be saved by the front end itself
        // response.setHeader("Authentication", token);

        return fillResult(user);
    }

    private LoginVO fillResult(User user) {
        LoginVO loginVO = new LoginVO();
        loginVO.setUserId(user.getId());
        loginVO.setUserName(user.getUserName());
        return loginVO;
    }
}

entity

package com.example.demo.business.login.entity;

import lombok.Data;

@Data
public class LoginRequest {
    private String userName;
    private String password;
}
package com.example.demo.business.login.entity;

import lombok.Data;

@Data
public class LoginVO {
    private Long userId;
    private String userName;
}

Login (logout)

package com.example.demo.business.logout;

import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "Logout")
@RestController
public class LogoutController {

    @ApiOperation("Logout")
    @PostMapping("logout")
    public Result<Object> logout() {
        SecurityUtils.getSubject().logout();
        return new Result();
    }
}

product

controller

package com.example.demo.business.product;

import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "product")
@RestController
@RequestMapping("product")
public class ProductController {
    @RequiresPermissions("product:add")
    @ApiOperation(value="Add products")
    @PostMapping("add")
    public Result add() {
        return new Result<>().message("product:add success");
    }

    @RequiresPermissions("product:delete")
    @ApiOperation(value="Delete product")
    @PostMapping("delete")
    public Result delete() {
        return new Result<>().message("product:delete success");
    }

    @RequiresPermissions("product:edit")
    @ApiOperation(value="Edit product")
    @PostMapping("edit")
    public Result edit() {
        return new Result<>().message("product:edit success");
    }

    @RequiresPermissions("product:view")
    @ApiOperation(value="View products")
    @GetMapping("view")
    public Result view() {
        return new Result<>().message("product:view success");
    }
}

order

package com.example.demo.business.order;

import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "order")
@RestController
@RequestMapping("order")
public class OrderController {
    @RequiresPermissions("order:add")
    @ApiOperation(value="Add order")
    @PostMapping("add")
    public Result add() {
        return new Result<>().message("order:add success");
    }

    @RequiresRoles(value = {"admin", "orderManager"}, logical = Logical.OR)
    @ApiOperation(value="Delete order")
    @PostMapping("delete")
    public Result delete() {
        return new Result<>().message("order:delete success");
    }

    // Administrator or order administrator has permission
    @RequiresRoles(value = {"admin", "productManager"}, logical = Logical.OR)
    @ApiOperation(value="Edit order")
    @PostMapping("edit")
    public Result edit() {
        return new Result<>().message("order:edit success");
    }

    // At this time, the conditions of both annotations must be met before access is allowed
    @RequiresPermissions("order:view")
    @RequiresRoles(value = {"admin", "productManager"}, logical = Logical.OR)
    @ApiOperation(value="View order")
    @GetMapping("view")
    public Result view() {
        return new Result<>().message("order:view success");
    }
}

Common (common package)

Startup class

Of course, it's not in the common package, but it's also a public thing. Let's put it here.  

package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.demo.**.mapper")
public class ShiroApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class, args);
    }

}

Global exception handling

package com.example.demo.common.advice;

import com.example.demo.common.entity.Result;
import com.example.demo.common.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE - 1)
@RestControllerAdvice
public class GlobalExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public Result<Object> handleException(Exception e) throws Exception {
        log.error(e.getMessage(), e);

        // If a custom exception has @ ResponseStatus annotation, it will continue to be thrown
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
            throw e;
        }

        // This should be written in the actual project to prevent users from seeing detailed exception information
        // return new Result().failure().message.message("operation failed");
        return new Result<>().failure().message(e.getMessage());
    }

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthenticatedException.class)
    public Result<Object> handleUnauthenticatedException(Exception e) {
        log.error(e.getMessage(), e);
        return new Result<>().failure().message(e.getMessage());
    }

    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(UnauthorizedException.class)
    public Result<Object> handleUnauthorizedException(Exception e) {
        log.error(e.getMessage(), e);
        return new Result<>().failure().message(e.getMessage());
    }

    @ExceptionHandler(BusinessException.class)
    public Result<Object> handleBusinessException(Exception e) throws Exception {
        log.error(e.getMessage(), e);

        // If a custom exception has @ ResponseStatus annotation, it will continue to be thrown
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
            throw e;
        }

        // This should be written in the actual project to prevent users from seeing detailed exception information
        // Return new result < > (). Failure(). Message ("operation failed");
        return new Result<>().failure().message(e.getMessage());
    }
}

Global response processing

package com.example.demo.common.advice;

import com.example.demo.common.entity.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.Arrays;
import java.util.List;

@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE)
@ControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    private List<String> KNIFE4J_URI = Arrays.asList(
            "/doc.html",
            "/swagger-resources",
            "/swagger-resources/configuration",
            "/v3/api-docs",
            "/v2/api-docs");
    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        // If the type returned by the interface itself is ResultWrapper, no operation is required and false is returned
        // return !returnType.getParameterType().equals(ResultWrapper.class);
        return true;
    }

    @Override
    @ResponseBody
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String) {
            // If the return value is of String type, it needs to be wrapped as String type. Otherwise, an error will be reported
            try {
                ObjectMapper objectMapper = new ObjectMapper();
                Result<Object> result = new Result<>().data(body);
                return objectMapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("serialize String error");
            }
        } else if (body instanceof Result) {
            return body;
        } else if (isKnife4jUrl(request.getURI().getPath())) {
            // If it is an interface document uri, skip it directly
            return body;
        }
        return new Result<>().data(body);
    }

    private boolean isKnife4jUrl(String uri) {
        AntPathMatcher pathMatcher = new AntPathMatcher();
        for (String s : KNIFE4J_URI) {
            if (pathMatcher.match(s, uri)) {
                return true;
            }
        }
        return false;
    }
}

constant

White list  

package com.example.demo.common.constant;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public interface WhiteList {
    List<String> KNIFE4J = Arrays.asList(
            "/doc.html",
            "/swagger-resources",
            "/swagger-resources/configuration",
            "/v3/api-docs",
            "/v2/api-docs",
            "/webjars/**");

    List<String> ALL = new ArrayList<>(KNIFE4J);
}

code of response data  

package com.example.demo.common.constant;

public enum ResultCode {
    SUCCESS(1000, "Access successful"),
    SYSTEM_FAILURE(1001, "System exception"),
    ;

    private final int code;
    private final String description;

    ResultCode(int code, String description) {
        this.code = code;
        this.description = description;
    }

    public int getCode() {
        return code;
    }

    public String getDescription() {
        return description;
    }
}

Authorization related

package com.example.demo.common.constant;

public interface AuthConstant {
    String ALGORITHM_TYPE = "md5";
    int HASH_ITERATIONS = 2;
}

Entity class

Encapsulate response results  

package com.example.demo.common.entity;

import com.example.demo.common.constant.ResultCode;
import lombok.Data;

@Data
public class Result<T> {
    private boolean success = true;

    private int code = ResultCode.SUCCESS.getCode();

    private String message;

    private T data;

    public Result() {
    }

    public Result(boolean success) {
        this.success = success;
    }

    public Result<T> success(boolean success) {
        Result<T> result = new Result<>(success);
        if (success) {
            result.code = ResultCode.SUCCESS.getCode();
        } else {
            result.code = ResultCode.SYSTEM_FAILURE.getCode();
        }
        return result;
    }

    public Result<T> success() {
        return success(true);
    }

    public Result<T> failure() {
        return success(false);
    }

    /**
     * @param code {@link ResultCode#getCode()}
     */
    public Result<T> code(int code) {
        this.code = code;
        return this;
    }

    public Result<T> message(String message) {
        this.message = message;
        return this;
    }

    public Result<T> data(T data) {
        this.data = data;
        return this;
    }
}

Account information

package com.example.demo.common.entity;

import com.example.demo.common.util.auth.ShiroUtil;
import lombok.Data;

/**
 * Deposit account information.
 *   After logging in, this object will be instantiated and placed in the subject.
 *   Get method: {@ link shiruotil#getprofile()}
 */
@Data
public class AccountProfile {
    private String id;
    private String userName;
}

abnormal

Custom exception (business exception)  

package com.example.demo.common.exception;

public class BusinessException extends RuntimeException{
    public BusinessException() {
        super();
    }

    public BusinessException(String message) {
        super(message);
    }

    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }
}

Tool class

  The holder of the ApplicationContext of Spring

package com.example.demo.common.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext context;

    public void setApplicationContext(ApplicationContext context)
            throws BeansException {
        ApplicationContextHolder.context = context;
    }

    public static ApplicationContext getContext() {
        return context;
    }
}  

jwt tool class

package com.example.demo.common.util.auth;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.example.demo.common.util.ApplicationContextHolder;
import com.example.demo.config.properties.JwtProperties;

import java.util.Date;

public class JwtUtil {
    private static JwtProperties jwtProperties;

    // Create jwt token
    public static String createToken(String userId) {
        if (jwtProperties == null) {
            jwtProperties = ApplicationContextHolder.getContext()
                    .getBean(JwtProperties.class);
        }
        try {
            Date date = new Date(System.currentTimeMillis() + jwtProperties.getExpire() * 1000);
            Algorithm algorithm = Algorithm.HMAC512(jwtProperties.getSecret());
            return JWT.create()
                    // Customize the key value of the private payload. For example:. withClaim("userName", "Tony")
                    // .withClaim("key1", "value1")
                    .withAudience(userId)  // Save the user id in the token
                    .withExpiresAt(date)   // After date, the token expires
                    .sign(algorithm);      // Key of token
        } catch (Exception e) {
            return null;
        }
    }

    // Verification token
    public static boolean verifyToken(String token) {
        if (jwtProperties == null) {
            jwtProperties = ApplicationContextHolder.getContext()
                    .getBean(JwtProperties.class);
        }
        try {
            Algorithm algorithm = Algorithm.HMAC512(jwtProperties.getSecret());
            JWTVerifier verifier = JWT.require(algorithm)
                    // .withIssuer("auth0")
                    // .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (JWTVerificationException exception) {
            // Token errors, token expiration, etc. will come here
            return false;
        }
    }

    public static String getUserIdByToken(String token) {
        try {
            String userId = JWT.decode(token).getAudience().get(0);
            return userId;
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

Shiro tool class

package com.example.demo.common.util.auth;

import com.example.demo.common.entity.AccountProfile;
import org.apache.shiro.SecurityUtils;

public class ShiroUtil {

    // Used to obtain information about the current account.
    public static AccountProfile getProfile() {
        return (AccountProfile) SecurityUtils.getSubject().getPrincipal();
    }

}

test

Test super administrator (admin)

Start the project and access: http://localhost:8080/doc.html

1. Test login

  1. Login succeeded
  2. As you can see, a set cookie header will be returned, and the value is token.

2. Test interfaces with resource permissions

Product interfaces are added to the test.

  1. Successfully accessed.
  2. Cookie s are passed on request

  I use standard: set Cookie, Cookie for authentication. If it is a custom header, you need to write it manually:

3. Test logout

4. Access the interface again

  1. Access succeeded.
  2.   Because the token has not expired and the browser will send it to the server, it is successful.

Test product manager

Start the project and access: http://localhost:8080/doc.html

1. Test login

  1. Login succeeded
  2. As you can see, a set cookie header will be returned, and the value is token.

2. Test interfaces with resource permissions

Product interfaces are added to the test.

  1. Successfully accessed.
  2. Cookie s are passed on request

3. Test the interface without resource permission

We will test and add order interface.

  1. Access failed.
  2. Cookie s are passed on request
  3. One detail: the prompt is red. This is the role of @ ResponseStatus

Click in to see that the status code is specified by me: 403

  4. Test the interface with role permission

We test and edit the order interface.

  1. Access succeeded.
  2. Cookie s are passed on request

4. Test the interface without role permission

We test the delete order interface.

  1. Access failed.
  2. Cookie s are passed on request

The test has both role and permission annotations

1. Log in (productManager)

2. Request an interface with both role and resource permission annotations

Request order viewing interface here.

  1. The request failed.
  2. Prompt no resource permission.

It can be inferred that if there are two annotations, they must be satisfied at the same time. Next, try using the admin role, which has all resource permissions at the same time.

3. Login (admin)

4. Request an interface with both role and resource permission annotations

Request order viewing interface here.

  1. Request succeeded

Note if there are two annotations, they must be satisfied at the same time.

Restart service re request

1. Login

Login succeeded  

2. Restart the server

Restart the application started by Idea.

3. Access authorized interfaces

We have added interfaces for accessing products.

  1. As you can see, the access was successful.

Request after timeout

1. Modify the configuration file and temporarily shorten the token expiration time (10 seconds here)

application.yml

2. Login

3. Wait for more than 10 seconds before requesting

The request failed.

  In my code, I specify this error as 401. Click to verify:

Posted by eshban on Wed, 15 Sep 2021 15:04:28 -0700