Integrating shiro framework with spring boot and redis

Catalog

Preparation

  • About shiro
    Three core components: Subject, SecurityManager and Realms

    • Subject: security operation of current user or current user
    • SecurityManage: Security Manager, which manages security operations for all users.
    • Realms: the interaction layer (equivalent to dao layer) between Shiro and project data source to complete user authentication (login) and authentication. There is at least one custom realm in the project to inherit authoring realm.
  • Introduce dependency

		<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- shiro ehcache -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.3.2</version>
        </dependency>
         <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.1.0</version>
        </dependency>
  • Create entity class and related business code

    The main entity classes are user table, role table, menu (including permissions), userRole and role menu middle table.

Basic steps

  1. Create MyRealm, complete related authentication and authentication
package com.cm.shiro;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.cm.system.domain.dto.RoleDO;
import com.cm.system.domain.dto.UserDO;
import com.cm.system.service.MenuService;
import com.cm.system.service.RoleService;
import com.cm.system.service.UserService;
import com.cm.utils.CommomUtil;
import org.apache.commons.lang3.StringUtils;
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.stereotype.Component;

import java.util.List;
import java.util.Set;

/**
 * Operation related to authority authentication and authorization
 * @Author: zlw
 * @Date: 2019/7/9 15:21
 */
@Component("myRealm")
public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Autowired
    private RoleService roleService;

    @Autowired
    private MenuService menuService;

    /**
     * To grant authorization
     * This method is called when the target resource or target method is accessed, not immediately after authentication
     * @param principal role
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //1. Obtain the user information and verify whether the user exists
        UserDO userDO = (UserDO) principal.getPrimaryPrincipal();
        if (null == userDO || null == userDO.getUserId()) {
            //If the user information is empty, an empty collection is returned
            return null;
        }
        //2. Add role set and permission set to info according to user information
        List<RoleDO> roles = roleService.findRolesByUserId(userDO.getUserId());
        roles.forEach(roleDO -> authorizationInfo.addRole(roleDO.getRoleName()));
        Set<String> perms = menuService.listPerms(userDO.getUserId());//Query all permissions of the user according to the id
        authorizationInfo.addStringPermissions(perms);
        return authorizationInfo;
    }


    /**
     * Landing verification
     * Execute subject.login(token) in the login interface; the method will be called
     * @param authenticationToken Authenticated user information
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //0. Convert token to UsernamePasswordToken to get user name
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        // Solve the problem of SQL injection
        if (StringUtils.isBlank(username) || CommomUtil.sqlInjCheck(username)) {
            throw new UnknownAccountException();
        }
        //1. Query the user information from the database through username, and judge the account status.
        EntityWrapper<UserDO> wrapper = new EntityWrapper<>();
        wrapper.eq("username", username);
        UserDO userDO = userService.selectOne(wrapper);
        if (null == userDO) {
            //Account does not exist
            throw new UnknownAccountException();
        }

        //Judge account status, can be omitted
        if (userDO.getStatus() == 0) {
            //Account locked
            throw new LockedAccountException();
        }
        //2. Generate the attribute of adding salt in the authenticationInfo object todo user object, and return jsonIgnore ByteSource.Util.bytes("") to the front end.
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userDO,userDO.getPassword(), ByteSource.Util.bytes("zlwcm"), getName());
        return authenticationInfo;
    }
}

  1. Related configuration of shiro
package com.cm.shiro;

import com.cm.system.service.MenuService;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * Shiro To configure
 */
@Slf4j
@Configuration
public class ShiroConfig {

    @Autowired
    private RedisProperties redisProperties;
    @Autowired
    private MenuService menuService;

    @Bean
    public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        bean.setSuccessUrl("/Successful login url");
        bean.setLoginUrl("/login");

        // Permission Filter
        Map<String, Filter> filterMap = new LinkedHashMap<>(1);
        filterMap.put("authc", new CaptchaFormAuthenticationFilter());
        bean.setFilters(filterMap);

        //todo configuration access
        LinkedHashMap<String, String> filterChainDefinitionMap = Maps.newLinkedHashMap();
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/course/list/**", "authc");
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return bean;
    }

    /**
     * Certificate matcher (because our password verification was handed over to Shiro's SimpleAuthenticationInfo for processing)
     * @return HashedCredentialsMatcher
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // Using MD5 hash algorithm
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // Hash times
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    /**
     * Set the password encryption mode to be consistent with the password encryption mode when the user registers. See tool class Md5Utils for specific operation
     *      Otherwise, incorrect password exception will be reported during verification.
     * @return
     */
    @Bean
    public MyRealm authShiroRealm() {
        MyRealm authShiroRealm = new MyRealm();
        authShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return authShiroRealm;
    }

    /**
     * Security Manager
     * @return
     */
    @Bean
    public org.apache.shiro.mgt.SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authShiroRealm());
        // Custom session management, using Redis
        securityManager.setSessionManager(sessionManager());
        // Customized cache implementation, using Redis
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }

    /**
     * Customizing session manager
     * @return sessionManager
     */
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /**
     * To configure shiro redisManager, the Shiro redis open source plug-in is used.
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisProperties.getHost());
        redisManager.setPort(redisProperties.getPort());
        redisManager.setPassword(redisProperties.getPassword());
        redisManager.setDatabase(redisProperties.getDatabase());
        return redisManager;
    }

    /**
     * cacheManager Cache redis implementation
     */
    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao Layer is implemented through redis
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setKeyPrefix("shiro:user:");
        return redisSessionDAO;
    }

    /**
     * Enable Shiro aop annotation support
     * Use proxy mode; therefore, code support needs to be turned on.
     * @param securityManager securityManager
     * @return advisor
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }


}

  1. Login to call LoginController
package com.cm.system.controller;


import com.cm.aop.SysLog;
import com.cm.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * Login related operations
 */
@RestController
@Slf4j
public class LoginController {
    
    @SysLog("Land")
    @GetMapping("login")
    public R login(@RequestParam("username")String username,@RequestParam("password") String password) {
            Subject subject = SecurityUtils.getSubject();
//            password = MD5Utils.encrypt(password);
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            String failMsg = "";
            try {
                subject.login(token);
            }catch (UnknownAccountException e) {
                failMsg = "user does not exist";
            } catch (IncorrectCredentialsException e) {
                failMsg = "Wrong password!";
            } catch (LockedAccountException e) {
                failMsg = "Login failed, the user has been frozen";
            } catch (Exception e) {
                log.info("System internal exception!!{}", e);
                failMsg = "System internal exception!!";
            }
        return failMsg == "" ? R.ok() : R.error(failMsg);
    }


}

  1. Related tools
package com.cm.utils;

import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MD5Utils {
	private static final String SALT = "zlwcm";

	private static final String ALGORITH_NAME = "md5";

	private static final int HASH_ITERATIONS = 2;

	private static final Logger LOGGER = LoggerFactory.getLogger(MD5Utils.class);

	private MD5Utils() {
		super();
	}

	public static String encrypt(String pswd) {
		String newPassword = new SimpleHash(ALGORITH_NAME, pswd, ByteSource.Util.bytes(SALT), HASH_ITERATIONS).toHex();
		return newPassword;
	}

}

Common abnormality

  • The database password is inconsistent with the user password: the encryption method is inconsistent, the user password is entered or the transmission conversion is wrong
org.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - admin, rememberMe=false] did not match the expected credentials.

Keywords: Shiro Apache Redis Java

Added by mobtex on Fri, 25 Oct 2019 21:58:14 +0300