Spring boot integrates Shiro and JWT

Time:2022-5-4

reference resources:
https://blog.csdn.net/qq_43948583/article/details/104437752
https://blog.csdn.net/weixin_42375707/article/details/111145907

It is planned to realize the permission control of adding, deleting, modifying and querying levels

1. Table structure

Menu table

CREATE TABLE `menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) COLLATE utf8mb4_ unicode_ Ci not null comment 'menu name',
  `parent_ ID ` bigint (20) not null comment 'parent node ID',
  `url` varchar(256) COLLATE utf8mb4_ unicode_ Ci not null comment 'path',
  `sort_ ID ` int (11) default '0' comment 'sort ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_ INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_ unicode_ Ci comment = 'menu table';

Menu – operation table

CREATE TABLE `menu_opt` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `menu_ ID ` bigint (20) not null comment 'menu ID',
  `opt_ ID ` bigint (20) not null comment 'operation ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_ INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_ unicode_ Ci comment = 'menu - operation association table';

Operation table

CREATE TABLE `opt` (
  `opt_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `opt_ name` varchar(32) COLLATE utf8mb4_ unicode_ Ci default null comment 'operation name',
  PRIMARY KEY (`opt_id`)
) ENGINE=InnoDB AUTO_ INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_ unicode_ Ci comment = 'operation table';

Role table

CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(256) COLLATE utf8mb4_ unicode_ Ci default null comment 'role name',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Role menu operation table

CREATE TABLE `role_menu_opt` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_ ID ` bigint (20) not null comment 'role ID',
  `menu_ ID ` bigint (20) not null comment 'menu ID',
  `opt_ ID ` bigint (20) not null comment 'operation ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_ INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_ unicode_ Ci comment = 'role menu operation association table';

User table

CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) NOT NULL,
  `pwd` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

User role table

CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_ ID ` bigint (20) not null comment 'user ID',
  `role_ ID ` bigint (20) not null comment 'role ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_ INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_ unicode_ Ci comment = 'role table';

2. Use mybatis plus to automatically generate some classes

Refer to:https://www.jianshu.com/p/c984ac0b67e8

3. Add Shiro and JWT dependencies

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.3</version>
        </dependency>

4. Shiro and JWT configuration

  • shiro config
@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //Associate defaultwebsecuritymanager
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //Add Shiro built-in filter
        /*
         *Anon: access without authentication
         *Auth: authentication is required to access
         *User: you must have the remember me function to use it
         *Perms: you can only access a resource with permission
         *Role: you must have a role permission to access
         * */
        //Add filter
        Map<String, Filter> filters = new HashMap<>(4);
        //Custom authentication authorization filter
        filters.put("auth", new AuthFilter());
        //Add a custom authentication authorization filter
        shiroFilterFactoryBean.setFilters(filters);

        //Put the path to be intercepted in the map
        Map<String, String> filterMap = new LinkedHashMap<>();
        //Release login interface
        filterMap.put("/login", "anon");
        //Release logout interface
        filterMap.put("/logout", "anon");
        //Intercept all paths, and it will automatically run to the user-defined filter authfilter
        filterMap.put("/**", "auth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(AuthRealm authRealm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

        //Associated realm
        defaultWebSecurityManager.setRealm(authRealm);

        /*
         *Close Shiro's own session
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);
        return defaultWebSecurityManager;
    }

    //Inject custom realm into defaultwebsecuritymanager
    @Bean
    public AuthRealm authRealm() {
        return new AuthRealm();
    }

    //By calling initializable Init() and destroyable Destroy () method to manage the Shiro bean life cycle
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    //Open Shiro permission annotation
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(defaultWebSecurityManager);
        return advisor;
    }
}
  • realm
public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private UserRoleMapper userRoleMapper;

    @Autowired
    private RoleMenuOptMapper roleMenuOptMapper;

    //Authentication
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //Get the token from the front end
        String accessToken = (String) token.getPrincipal();
        //Search the user name in the cache according to the token
        String userId = JwtUtils.getAudience(accessToken);
        if (userId == null) {
            //If the user name searched is empty, the token is invalid
            Throw new correctcredentialsexception ("token is invalid, please login again");
        }
        JwtUtils.verifyToken(accessToken, userId);
        SysUser user = sysUserMapper.selectById(Long.valueOf(userId));
        if (user == null) {
            Throw new unknownaccountexception ("user does not exist!");
        }
        //This method needs to return data of type authenticationinfo
        //Therefore, return its implementation class simpleauthenticationinfo, and pass in the user and the obtained token to realize automatic authentication

        return new SimpleAuthenticationInfo(user, accessToken, "");
    }

    //Authorization
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //Get the user object user from the authentication
        SysUser user = (SysUser) principals.getPrimaryPrincipal();
        //This method needs a return value of type authorizationinfo, so it returns its implementation class simpleauthorizationinfo
        //Set the user's permission through addstringpermission() in simpleauthorizationinfo
        QueryWrapper<UserRole> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", user.getId());
        List<UserRole> userRoles = userRoleMapper.selectList(queryWrapper);
        List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
        QueryWrapper<RoleMenuOpt> optWrapper = new QueryWrapper<>();
        optWrapper.in("role_id", roleIds);
        List<RoleMenuOpt> roleMenuOpts = roleMenuOptMapper.selectList(optWrapper);
        List<String> optList = roleMenuOpts.stream().map(opt -> String.valueOf(opt.getOptId()))
                .collect(Collectors.toList());
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addStringPermissions(optList);

        return simpleAuthorizationInfo;
    }

//    @Override
//    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
//// custom password matcher
//        MyCredentialsMatcher currentCredentialsMatcher = new MyCredentialsMatcher();
//        super.setCredentialsMatcher(currentCredentialsMatcher);
//    }
}
  • shiro filter
public class AuthFilter extends AuthenticatingFilter {


    //Generate custom token
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        //Get token from header
        String token = httpServletRequest.getHeader("token");
        return new JwtToken(token);
    }

    //All requests denied access
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //Allow option requests to pass
        return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
    }

    //If the access request is denied, the onaccessdenied method obtains the token first, and then calls the executelogin method
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        //Get request token
        String token = httpServletRequest.getHeader("token");
        // StringUtils. Isblank (string STR) determines whether the str string is empty or the length is 0
        if (token == null || "".equals(token)) {
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpServletResponse.setCharacterEncoding("UTF-8");
            Response msg = Response. Fail ("please login first");
            httpServletResponse.getWriter().write(JSON.toJSONString(msg));
            return false;
        }
        return executeLogin(request, response);
    }

    //Called when the token fails
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpResponse.setCharacterEncoding("UTF-8");
        try {
            //Exception handling login failure
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Response msg = Response. Fail ("login certificate has expired, please login again");
            httpResponse.getWriter().write(JSON.toJSONString(msg));
        } catch (IOException e1) {

        }
        return false;
    }


}
  • jwt util
public class JwtUtils {

    /**
     *Issuing object: the ID of this user
     *Issued on: now
     *Effective time: 30 minutes
     *Load content: temporarily designed as: this person's name, this person's nickname
     *Encryption key: this person's ID plus a string
     */
    public static String createToken(String userId, String realName, String userName) {

        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.MINUTE, 30);
        Date expiresDate = nowTime.getTime();
        return JWT.create()
                //Issuing object
                .withAudience(userId)
                //Release time
                .withIssuedAt(new Date())
                //Effective time
                .withExpiresAt(expiresDate)
                //Load, just write a few
                .withClaim("userName", userName)
                .withClaim("realName", realName)
                //Encryption
                .sign(Algorithm.HMAC256(userId + "HelloLehr"));
    }

    /**
     *Verify the legitimacy. The secret parameter should be the user ID
     *
     * @param token
     */
    public static void verifyToken(String token, String secret) {
        DecodedJWT jwt = null;
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret + "HelloLehr")).build();
            jwt = verifier.verify(token);
        } catch (Exception e) {
            //Validation failure
            //The exception thrown here is one of my custom exceptions. You can write it in other words
            throw new TokenUnavailableException();
        }
    }

    /**
     *Get issuing object
     */
    public static String getAudience(String token) {
        String audience = null;
        try {
            audience = JWT.decode(token).getAudience().get(0);
        } catch (JWTDecodeException j) {
            //Here is the token parsing failure
            throw new TokenUnavailableException();
        }
        return audience;
    }

    /**
     *Get the value of the load by the load name
     */
    public static Claim getClaimByName(String token, String name) {
        return JWT.decode(token).getClaim(name);
    }

}

jwt token

public class JwtToken extends UsernamePasswordToken {

    String token;

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

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

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

With the above classes, the calculation is basically implemented. Next, write login

5. Login

login controller

@RestController
public class LoginController {

    @Autowired
    private SysUserMapper sysUserMapper;

    @PostMapping("/login")
    public Response login(@RequestParam("username") String username, @RequestParam("password") String password) {
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
        if (sysUser == null) {
            return Response. Fail ("account or password error");
        }
        if (!sysUser.getPwd().equals(password)) {
            return Response. Fail ("account or password error");
        }
        String token = JwtUtils.createToken(sysUser.getId().toString(), sysUser.getUsername(), sysUser.getUsername());
        return Response. Success ("login succeeded") add("token", token);
    }

}

6. Test controller

@RestController
@RequestMapping("/menu")
@Slf4j
public class MenuController {

    @Autowired
    private MenuService menuService;

    @GetMapping("/view")
    @RequiresPermissions("1")
    public Response view() {
        return Response.success("success").add("menu", menuService.list());
    }

    @GetMapping("/add")
    @RequiresPermissions("2")
    public Response add() {
        log.info("menu add.....");
        return Response.success("success");
    }

}