Shiro series - Shiro + Springboot + JWT integration

Write in front

The appearance of this article means that the research on the implementation of Spring Security Oauth is no longer carried out. The reason is that the original open source project has been abandoned and no longer updated. Moreover, the content of Oauth implementation is a little strange and new spring-authorization-server It has only been released to 0.1.0. By default, it only provides memory based implementation. Personally, I think it is not very perfect and is not suitable for use in the project. Moreover, the Oauth process of Spring Security has been implemented. If you want to modify it, you have to study the implementation logic of spring authorization server again, and then modify and customize it. It's too energy-consuming. It's better to use Shiro to implement the logic of Oauth.

However, this article has nothing to do with Oauth. It is simply the integration of Shiro + Springboot + JWT.

The dependencies used in this article are as follows:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.7.1</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.11.0</version>
</dependency>

Another thing to note is that Shiro has a default configuration for SpringBoot. For details, see the official website:

  • Integration with Spring: http://shiro.apache.org/spring-framework.html
  • Integration with SpringBoot: http://shiro.apache.org/spring-boot.html

However, in this article, the SpringBoot integration method is not used, and the default configuration is not imported according to the tutorials on the official website, because Shiro needs to be configured relatively simply to run. Moreover, from my understanding, Shiro is suitable for the mode of login state storage based on Session, and the state is stored in Token using Jwt. This mode needs to make some changes to the logic of configuration, In addition, there are some pits, so the configuration is not based on the default configuration. The points needing attention will be explained below.

The record post has just contacted Shiro. If there is any error in the content, I hope the boss can correct it.

1. Implementation source code

The sample project has been uploaded to Github and is directly available: https://github.com/nineya/framework-study/tree/master/shiro-study

Project structure:

1.1 SpringBoot program entry

package com.nineya.shiro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author Farewell to shangxue
 * 2021/2/15
 * Program entry
 */
@SpringBootApplication
public class ShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class);
    }
}

1.2 entity class

Simply customize user, role and permission entity classes. A user can contain multiple roles and a role can contain multiple permissions.

package com.nineya.shiro.entity;

/**
 * @author Farewell to shangxue
 * 2021/2/15
 * jurisdiction
 */
public class Permissions {
    private long id;
    private String permissionsName;

    public Permissions() {
    }

    public Permissions(long id, String permissionsName) {
        this.id = id;
        this.permissionsName = permissionsName;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getPermissionsName() {
        return permissionsName;
    }

    public void setPermissionsName(String permissionsName) {
        this.permissionsName = permissionsName;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Permissions{");
        sb.append("id=").append(id);
        sb.append(", permissionsName='").append(permissionsName).append('\'');
        sb.append('}');
        return sb.toString();
    }
}
package com.nineya.shiro.entity;

import java.util.Set;

/**
 * @author Farewell to shangxue
 * 2021/2/15
 * Roles, including permission sets
 */
public class Role {
    private long id;
    private String roleName;
    /**
     * The set of permissions that a role has
     */
    private Set<Permissions> permissions;

    public Role() {
    }

    public Role(long id, String roleName, Set<Permissions> permissions) {
        this.id = id;
        this.roleName = roleName;
        this.permissions = permissions;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public Set<Permissions> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<Permissions> permissions) {
        this.permissions = permissions;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Role{");
        sb.append("id=").append(id);
        sb.append(", roleName='").append(roleName).append('\'');
        sb.append(", permissions=").append(permissions);
        sb.append('}');
        return sb.toString();
    }
}
package com.nineya.shiro.entity;

import java.util.Set;

/**
 * @author Farewell to shangxue
 * 2021/2/15
 * Users, including role sets
 */
public class User {
    private long uid;
    private String userName;
    private String password;
    /**
     * User's corresponding role
     */
    private Set<Role> roles;

    public User() {
    }

    public User(long uid, String userName, String password, Set<Role> roles) {
        this.uid = uid;
        this.userName = userName;
        this.password = password;
        this.roles = roles;
    }

    public long getUid() {
        return uid;
    }

    public void setUid(long uid) {
        this.uid = uid;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("User{");
        sb.append("uid=").append(uid);
        sb.append(", userName='").append(userName).append('\'');
        sb.append(", password='").append(password).append('\'');
        sb.append(", roles=").append(roles);
        sb.append('}');
        return sb.toString();
    }
}

1.3 service layer

Create a Login service layer implementation, which can access and read user information for user Login operation. In actual use, it is modified as; Connect to the database to get data.

package com.nineya.shiro.service;

import com.nineya.shiro.entity.User;

/**
 * @author Farewell to shangxue
 * 2021/2/15
 * User login service interface
 */
public interface LoginService {

    /**
     * Get users by user name
     *
     * @param name user name
     * @return
     */
    User getUserByName(String name);
}
package com.nineya.shiro.service.impl;

import com.nineya.shiro.entity.Permissions;
import com.nineya.shiro.entity.Role;
import com.nineya.shiro.entity.User;
import com.nineya.shiro.service.LoginService;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * @author Farewell to shangxue
 * 2021/2/15
 * Users log in to the service and store user information in the format of map < < user name, user information > for query
 * In actual use, you can modify this to connect to the database query
 */
@Service
public class LoginServiceImpl implements LoginService {
    private final Map<String, User> users = new HashMap<>();

    public LoginServiceImpl() {
        // Define three permissions
        Permissions permissions1 = new Permissions(1, "create");
        Permissions permissions2 = new Permissions(2, "delete");
        Permissions permissions3 = new Permissions(3, "select");
        // Define two roles
        Role role1 = new Role(1, "read", new HashSet<Permissions>(){{add(permissions3);}});
        Role role2 = new Role(1, "write", new HashSet<Permissions>(){{add(permissions1);add(permissions2);}});
        // Define three users corresponding to two roles
        users.put("observe", new User(1, "observe", "123456", Collections.singleton(role1)));
        users.put("admin", new User(1, "admin", "123456", Collections.singleton(role2)));
        users.put("user", new User(1, "user", "123456", new HashSet<Role>(){{add(role1); add(role2);}}));
    }

    @Override
    public User getUserByName(String name) {
        return users.get(name);
    }
}

1.4 Token tools

The token tool class is responsible for the creation and parsing of JWT. In this paper, username is added to the load. In practical use, role and other information can be added together. However, a large amount of data and implicit data should not be added to JWT, because JWT is not encrypted. A large amount of data will lead to too long token and increase the network load.

package com.nineya.shiro.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Date;

/**
 * jwt Processing class
 *
 * @author Farewell to shangxue
 * 2020/11/29
 */
@Component
public class UserTokenUtil {
    /**
     * jwt encryption algorithm 
     */
    private Algorithm algorithm;
    private JWTVerifier verifier;

    public UserTokenUtil() {
        algorithm = Algorithm.HMAC256("secret");
        verifier = JWT.require(algorithm).build();
    }

    /**
     * Create a user token and save the creation time of the token
     *
     * @param username User name
     * @return token character string
     */
    public String createToken(String username) {
        Date expireTime = new Date(System.currentTimeMillis() + 60 * 60 * 1000);
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(expireTime)
                .sign(algorithm);
    }

    /**
     * Verify the legitimacy of the token
     *
     * @param token
     * @return
     */
    public DecodedJWT verifyToken(String token) {
        try {
            return verifier.verify(token);
        } catch (Exception e) {
            throw new TokenExpiredException("token Parsing failed");
        }
    }

    /**
     * Get user name
     *
     * @param token
     * @return
     */
    public String getUserName(String token) {
        DecodedJWT jwt = verifyToken(token);
        return jwt.getClaim("username").asString();
    }
}

1.5 controller (start with Shiro)

Add the login login interface implementation, and add several simple interfaces as subsequent call examples. Here we begin to design different problems of jwt logic described above.

package com.nineya.shiro.controller;

import com.nineya.shiro.entity.User;
import com.nineya.shiro.service.LoginService;
import com.nineya.shiro.util.UserTokenUtil;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Date;

/**
 * @author Farewell to shangxue
 * 2021/2/15
 */
@RestController
public class LoginController {
    @Resource
    private LoginService loginService;
    @Resource
    private UserTokenUtil tokenUtil;

    /**
     * When logging in with jwt, the logic here will be somewhat different
     * If Token is not used, the user will pass subject. In this method Login (usernamepasswordtoken).
     * When using jwt, session will no longer be used to store login status, subject The login (usernamepasswordtoken) logic will be performed when the Filter parses the token, and
     * Each request requires token parsing and login operations.
     * That is to say, there are two steps: authentication and authorization. Originally, you only need to authenticate when logging in and authorize each request. After using jwt, each request requires three steps: memory jwt parsing, authentication and authorization.
     * @param userName user name
     * @param password password
     * @return
     */
    @GetMapping("/login")
    public String login(@RequestParam("userName") String userName, @RequestParam("password") String password) {
        if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
            return "Please enter user name and password!";
        }
        User user = loginService.getUserByName(userName);
        if (!user.getPassword().equals(password)) {
            return "Incorrect password!";
        }
        return tokenUtil.createToken(userName);
    }

    // This is a session based implementation without jwt
//    @GetMapping("/login")
//    public String login(User user) {
//        if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
//            return "please enter your 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 the exception and return the corresponding information
//            subject.login(usernamePasswordToken);
            subject.checkRole("admin");
            subject.checkPermissions("query", "add");
//        } catch (UnknownAccountException e) {
//            return "the user name does not exist!";
//        } catch (AuthenticationException  e) {
//            return "wrong account or password!";
//        } catch (AuthorizationException e) {
//            return "no permission";
//        }
//        return "login success";
//    }

    /**
     * Allow users whose role is read and write to access
     * @return
     */
    @RequiresRoles({"read", "write"})
    @GetMapping("/admin")
    public String admin() {
        return "admin";
    }

    /**
     * Allow users with select permission to access
     * @return
     */
    @RequiresPermissions("select")
    @GetMapping("/select")
    public String select() {
        return "select";
    }

    /**
     * Allow users with create permission to access
     * @return
     */
    @RequiresPermissions("create")
    @GetMapping("/create")
    public String create() {
        return "create";
    }
}
package com.nineya.shiro.controller;

import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author Farewell to shangxue
 * 2021/2/15
 * Handle some exceptions that fail to pass authority authentication
 */
@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler
    @ResponseBody
    public String ErrorHandler(AuthorizationException e) {
        return "Failed permission verification!\n" + e.getMessage();
    }
}

1.6 custom JWT filter

Check whether the request header has the Authorization field. If so, perform token parsing and login operations.

package com.nineya.shiro.filter;

import com.nineya.shiro.controller.ExceptionController;
import org.apache.shiro.authc.BearerToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;

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

/**
 * @author Farewell to shangxue
 * 2021/2/15
 */
public class TokenFilter extends BasicHttpAuthenticationFilter {

    /**
     * Determine whether the user wants to log in.
     * Check whether the header contains the Authorization field
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader(AUTHORIZATION_HEADER);
        return authorization != null;
    }

    /**
     * Here we will explain in detail why the final return is true, that is, access is allowed
     * For example, we provide an address GET /article
     * Login users and visitors see different content
     * If false is returned here, the request will be directly intercepted and the user can't see anything
     * So we return true here. In the Controller, we can use subject Isauthenticated() to determine whether the user logs in
     * If some resources can only be accessed by logged in users, we only need to add the @ requireauthentication annotation on the method
     * However, one disadvantage of this is that it is not able to filter and authenticate GET,POST and other requests separately (because we have rewritten the official method), but it has little impact on the application
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //Judge whether the request header of the request carries "Token"
        if (isLoginAttempt(request, response)) {
            //If it exists, enter the executeLogin method to perform login and check whether the token is correct
            try {
                return executeLogin(request, response);
            } catch (Exception e) {
                //token error
                e.printStackTrace();
                return false;
            }
        }
        //If there is no token in the request header, it may be to perform login operation or visitor status access. There is no need to check the token and return true directly
        return true;
    }

    /**
     * User login
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(AUTHORIZATION_HEADER);
        BearerToken jwtToken = new BearerToken(token, request.getRemoteAddr());
        getSubject(request, response).login(jwtToken);
        return true;
    }
}

1.7 custom Realm

Shiro provides the implementation of JdbcRealm, IniRealm and DefaultLdapRealm by default, but it does not meet the scenario I need, nor can it realize the resolution of jwt. Therefore, Shiro adopts to inherit the authorizing Realm custom Realm class and replicate the two interfaces of doGetAuthorizationInfo (authorization) and doGetAuthenticationInfo (authentication).

In fact, there are pits here, and the logic is not very suitable for the implementation of jwt, but there is no good scheme to modify, which will be described in detail later.

  • This is just an example. The loginservice is authorized and authenticated twice The getuserbyname (name) operation can be avoided.

  • The JWT can be parsed as an intermediate object in the authentication step, and then the intermediate object can be used as the Principal, which can avoid the performance waste caused by repeated parsing of JWT.

package com.nineya.shiro.config;

import com.nineya.shiro.entity.Permissions;
import com.nineya.shiro.entity.Role;
import com.nineya.shiro.entity.User;
import com.nineya.shiro.service.LoginService;
import com.nineya.shiro.util.UserTokenUtil;
import org.apache.catalina.realm.AuthenticatedUserRealm;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.stream.Collectors;

/**
 * Custom realm
 *
 * @author Farewell to shangxue
 * 2021/2/15
 */
public class StudyRealm extends AuthorizingRealm {
    @Resource
    private LoginService loginService;
    @Resource
    private UserTokenUtil tokenUtil;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof BearerToken;
    }

    /**
     * Authorization, executed after authentication
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String token = (String) principals.getPrimaryPrincipal();
        String name = tokenUtil.getUserName(token);
        User user = loginService.getUserByName(name);
        // Add roles and permissions
        SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
        for (Role role : user.getRoles()) {
            // Add role
            simpleAuthenticationInfo.addRole(role.getRoleName());
            // add permission
            simpleAuthenticationInfo.addStringPermissions(role.getPermissions().stream()
                    .map(Permissions::getPermissionsName).collect(Collectors.toSet()));
        }
        return simpleAuthenticationInfo;
    }

    /**
     * authentication
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        if (StringUtils.isEmpty(token.getPrincipal())) {
            return null;
        }
        String name = tokenUtil.getUserName((String) token.getPrincipal());
        User user = loginService.getUserByName(name);
        if (user == null) {
            return null;
        }
        // The first parameter is the principal, which will be encapsulated as principalcollection at the time of authorization Getprimaryprincipal() is used, so the jwt content must be returned
        // The second parameter is the authentication information, that is, the password. For the subsequent verification to pass, it needs to be the same as the content in the token
        // The third parameter is the domain name
        return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), user.getUserName());
    }
}

1.8 Shiro configuration

package com.nineya.shiro.config;

import com.nineya.shiro.filter.TokenFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Farewell to shangxue
 * 2021/2/15
 * Configuration class
 */
@Configuration
public class ShiroConfiguration {

    /**
     * If the agent is not configured, the annotation will not take effect
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * If the agent is not configured, the annotation will not take effect
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * Add your own authentication method to the container
     * @return
     */
    @Bean
    public Realm studyRealm() {
        StudyRealm studyRealm = new StudyRealm();
        return studyRealm;
    }

    /**
     * The implementation of the Filter should not be registered as a bean, otherwise the Filter sequence will be disordered and an exception will be thrown
     * If you must register as a Bean, you can use Order to specify the priority, which has not been tried yet
     * @return
     */
    public TokenFilter tokenFilter() {
        return new TokenFilter();
    }

    /**
     * Filter Factory, set the corresponding filter conditions and jump conditions
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        Map<String, String> map = new HashMap<>();
        //Log out
        map.put("/logout", "logout");
        // Use the jwt filter name we created ourselves
        map.put("/**", "jwt");
        //Sign in
        shiroFilterFactoryBean.setLoginUrl("/login");
        //home page
        shiroFilterFactoryBean.setSuccessUrl("/select");
        //Error page, authentication failed, jump
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        shiroFilterFactoryBean.setFilters(new HashMap<String, Filter>(){{put("jwt", tokenFilter());}});
        return shiroFilterFactoryBean;
    }

    /**
     * Permission management, configuration is mainly the management and authentication of Realm, and cache management can be configured at the same time
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();
        //realm management
        webSecurityManager.setRealm(studyRealm());
        return webSecurityManager;
    }
}

2. Why does Shiro not support JWT very well?

At the beginning of this article, Shiro's support for Jwt mode is not very good. It's all my personal understanding. If the content is wrong, please leave a message and ask questions.

2.1 authentication / authorization mode BUG

  1. Impact on Performance

Shiro as a whole revolves around the authentication / authorization mode, which is applicable to the system with status (i.e. the server stores login information). Authentication is carried out during login. Once login is completed, authentication steps are no longer required, and authentication is carried out directly according to the permission configuration. JWT is a stateless implementation. All login information is stored in the token. In the filter, getsubject (request, response) needs to be executed according to the token information every time Login (jwttoken) method, and then go through the doGetAuthenticationInfo method of realm for authentication.

For JWT, this authorization and authentication step can actually be combined as an authorization step. Login account password verification is the authentication step, but getsubject (request, response) cannot be called during login Login (jwttoken) to log in, you must call the login according to the token in the filter, which leads to a waste of performance. You can parse the JWT as an intermediate object in the authentication step, and then take the intermediate object as the Principal, which can avoid the performance waste caused by repeatedly parsing the JWT, but the performance waste caused by Shiro's repeated authentication process is inevitable.

  1. cache hidden BUG

Because the Token of JWT is time-effective, and the Token is used as the certificate of the authentication step, if the cache is enabled, the cache will be stored according to the Token, so that the subsequent authentication process will not be carried out for the Token, and the invalid Token can also access the system.

The disgusting thing is that the public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) method cannot replicate, so we can only turn off the caching function to solve this problem, or implement these contents manually. If the cache is turned off, the authorization step is the same cache logic, and the cache of the authorization step will also be turned off, which will also affect the performance.

To sum up, Shiro's support for JWT is not very good, but it is not impossible to solve it. It's good to manually copy these classes, but it will be cumbersome.

Tags: Spring Cyber Security Spring Boot Shiro

Posted by CPInteract on Mon, 18 Apr 2022 22:27:57 +0930