基于springboot跟shiro的后台权限管理系统

发布时间:2019-05-10
技术:springboot1.5.1 + shiro1.4.0 + maven3.0.5 +jdk1.8.0

概述

SpringBoot整合Shiro,Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。

详细

一、运行效果

image.png

image.pngimage.png


二、实现过程

①、创建shiro配置

@Configuration
public class ShiroConfiguration {

	private Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);

	/**
	 * ShiroFilterFactoryBean 处理拦截资源文件问题。
	 * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在
	 * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
	 * 
	 * Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
	 * 3、部分过滤器可指定参数,如perms,roles
	 * 
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
		logger.info("ShiroConfiguration.shiroFilter()");
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

		// 必须设置 SecurityManager
		shiroFilterFactoryBean.setSecurityManager(securityManager);

		// 拦截器.
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

		// 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
		filterChainDefinitionMap.put("/admin/sys/logout", "logout");

		// 配置记住我或认证通过可以访问的地址
		filterChainDefinitionMap.put("/admin/index", "user");

		// <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
		// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
		filterChainDefinitionMap.put("/statics/**", "anon");
		filterChainDefinitionMap.put("/admin/captcha.jpg", "anon");
		filterChainDefinitionMap.put("/admin/sys/login", "anon");
		filterChainDefinitionMap.put("/admin/sys/logout", "anon");
		filterChainDefinitionMap.put("/admin/**", "authc");

		// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
		shiroFilterFactoryBean.setLoginUrl("/admin/login.html");
		// 登录成功后要跳转的链接
		shiroFilterFactoryBean.setSuccessUrl("/admin/");
		// 未授权界面;
		shiroFilterFactoryBean.setUnauthorizedUrl("/error.html");

		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		return shiroFilterFactoryBean;
	}

	@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		// 设置realm.
		securityManager.setRealm(getUserRealm());

		// 注入缓存管理器;  这个如果执行多次,也是同样的一个对象;
		securityManager.setCacheManager(ehCacheManager());

		// 注入记住我管理器;
		securityManager.setRememberMeManager(rememberMeManager());

		return securityManager;
	}

	/**
	 * 身份认证realm; (这个需要自己写,账号密码校验;权限等)
	 * 
	 * @return
	 */
	@Bean
	public UserRealm getUserRealm() {
		UserRealm myShiroRealm = new UserRealm();
		return myShiroRealm;
	}

	/**
	 * 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持
	 * 
	 * @param securityManager
	 * @return
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}

	/**
	 * shiro缓存管理器; 需要注入对应的其它的实体类中: 1、安全管理器:securityManager
	 * 可见securityManager是整个shiro的核心
	 * 
	 * @return
	 */
	@Bean
	public EhCacheManager ehCacheManager() {
		logger.info("ShiroConfiguration.getEhCacheManager()");
		EhCacheManager cacheManager = new EhCacheManager();
		cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
		return cacheManager;
	}

	/**
	 * cookie对象
	 * 
	 * @return
	 */
	@Bean
	public SimpleCookie rememberMeCookie() {
		logger.info("ShiroConfiguration.rememberMeCookie()");
		// 这个参数是cookie的名称,对应前端的checkbox 的name = rememberMe
		SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
		// <!-- 记住我cookie生效时间30天 ,单位秒;-->
		simpleCookie.setMaxAge(259200);
		return simpleCookie;
	}

	/**
	 * cookie管理对象
	 * 
	 * @return
	 */
	@Bean
	public CookieRememberMeManager rememberMeManager() {
		logger.info("ShiroConfiguration.rememberMeManager()");
		CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
		cookieRememberMeManager.setCookie(rememberMeCookie());
		byte[] cipherKey = Base64.decode("wGiHplamyXlVB11UXWol8g==");
		cookieRememberMeManager.setCipherKey(cipherKey);
		return cookieRememberMeManager;
	}

}

②、自定义userRealm

/**
 * Shiro认证
 *
 */
public class UserRealm extends AuthorizingRealm {
    private Logger logger = LoggerFactory.getLogger(UserRealm.class);

    /**
     * 授权(验证权限时调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("授权(验证权限时调用)");
        SysUserEntity user = (SysUserEntity) principals.getPrimaryPrincipal();
        Long userId = user.getUserId();

        List<String> permsList = null;

        // 系统管理员,拥有最高权限
        if (userId.equals(TempUtil.constant.adminId)) {
            List<SysMenuEntity> menuList = TempUtil.sysMenuService.queryList(new HashMap<String, Object>(10));
            permsList = new ArrayList<>(menuList.size());
            for (SysMenuEntity menu : menuList) {
                permsList.add(menu.getPerms());
            }
        } else {
            permsList = TempUtil.sysUserService.queryAllPerms(userId);
        }

        // 用户权限列表
        Set<String> permsSet = new HashSet<String>();
        for (String perms : permsList) {
            if (StringUtils.isBlank(perms)) {
                continue;
            }
            permsSet.addAll(Arrays.asList(perms.trim().split(",")));
        }

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permsSet);
        return info;
    }

    /**
     * 认证(登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        logger.info("认证(登录时调用)");
        String username = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());
        // 查询用户信息
        SysUserEntity user = TempUtil.sysUserService.queryByUserName(username);

        // 账号不存在
        if (user == null) {
            throw new UnknownAccountException("账号或密码不正确");
        }

        // 密码错误
        if (!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException("账号或密码不正确");
        }

        // 账号锁定
        if (user.getStatus() == 0) {
            throw new LockedAccountException("账号已被锁定,请联系管理员");
        }

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
    }

}

③、登录接口

    @ResponseBody
	@RequestMapping(value = "/sys/login", method = RequestMethod.POST)
	@Transactional(rollbackOn = Exception.class)
	public R login(String username, String password, String captcha, HttpServletRequest request) throws IOException {
		if (StringUtils.isBlank(username)) {
			return R.error("请输入用户名!");
		}
		if (StringUtils.isBlank(password)) {
			return R.error("请输入密码!");
		}
		String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY);
		if (!captcha.equalsIgnoreCase(kaptcha)) {
			return R.error("验证码不正确");
		}
		try {
			Subject subject = ShiroUtils.getSubject();
			// sha256加密
			password = new Sha256Hash(password).toHex();
			UsernamePasswordToken token = new UsernamePasswordToken(username, password);
			subject.login(token);
		} catch (UnknownAccountException e) {
			return R.error(e.getMessage());
		} catch (IncorrectCredentialsException e) {
			return R.error(e.getMessage());
		} catch (LockedAccountException e) {
			return R.error(e.getMessage());
		} catch (AuthenticationException e) {
			e.printStackTrace();
			return R.error("账户验证失败");
		}
		SysUserEntity user = getAdmin();
		// 登录IP
		String ipAddress = GetIpAddress.getIpAddress(request);
		// 当前时间戳
		Long currentUnixTime = DateUtils.getCurrentUnixTime();
		user.setLastLoginIp(ipAddress);
		user.setLastLoginTime(currentUnixTime);
		boolean updateById = sysUserService.updateById(user);
		if (!updateById) {
			return R.error("系统异常,请联系管理员!");
		}
		// 记录登录日志
		SysUserLoginLogEntity entity = new SysUserLoginLogEntity();
		entity.setBrowser(HttpUtil.getUserBrowser(request));
		entity.setLoginTime(currentUnixTime);
		entity.setOperatingSystem(HttpUtil.getUserOperatingSystem(request));
		entity.setUserId(getAdminId());
		entity.setLoginIp(ipAddress);
		boolean insert = sysUserLoginLogService.insert(entity);
		// 这里只能抛异常回滚事务
		if (!insert) {
			throw new RRException("系统异常,请联系管理员!");
		}
		return R.ok();
	}

④、缓存机制

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">

	<diskStore path="java.io.tmpdir"/>
    
	<!-- 
			name:缓存名称。 
			maxElementsInMemory:缓存最大数目 
			maxElementsOnDisk:硬盘最大缓存个数。  
			eternal:对象是否永久有效,一但设置了,timeout将不起作用。  
			overflowToDisk:是否保存到磁盘,当系统当机时
			timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。 
			timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
			diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.  
			diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。  
			diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。 
			memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。  
			clearOnFlush:内存数量最大时是否清除。 
			 memoryStoreEvictionPolicy:
				 Ehcache的三种清空策略;
				 FIFO,first in first out,这个是大家最熟的,先进先出。 
				 LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
				 LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
		--> 
	<defaultCache
		maxElementsInMemory="10000"
		eternal="false"
		timeToIdleSeconds="120"
		timeToLiveSeconds="120"
		overflowToDisk="false"
		diskPersistent="false"
		diskExpiryThreadIntervalSeconds="120"
		/>
            
            
	<!-- 登录记录缓存 锁定10分钟 -->
	<cache name="passwordRetryCache" maxEntriesLocalHeap="2000"
		eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0"
		overflowToDisk="false" statistics="true">
	</cache>
    
</ehcache>


三、项目结构图

image.png

四、补充

 根据Shiro的设计思路,用户与角色之前的关系为多对多,角色与权限之间的关系也是多对多。在数据库中需要因此建立5张表,分别是用户表(存储用户名,密码,盐等)、角色表(角色名称,相关描述等)、权限表(权限名称,相关描述等)、用户-角色对应中间表(以用户ID和角色ID作为联合主键)、角色-权限对应中间表(以角色ID和权限ID作为联合主键)。具体dao与service的实现本文不提供。总之结论就是,Shiro需要根据用户名和密码首先判断登录的用户是否合法,然后再对合法用户授权。而这个过程就是Realm的实现过程。 

本实例支付的费用只是购买源码的费用,如有疑问欢迎在文末留言交流,如需作者在线代码指导、定制等,在作者开启付费服务后,可以点击“购买服务”进行实时联系,请知悉,谢谢
手机上随时阅读、收藏该文章 ?请扫下方二维码