Apache Shiro是一个功能强大、灵活的,开源的轻量级安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密,与spring security 相比较,简单易用,灵活性高,springboot本身是提供了对security的支持,毕竟是自家的东西。springboot暂时没有集成shiro,需要自己配置。
一.Shiro能做什么呢?
(1)验证用户身份
(2)用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
(3)在非 web 或 EJB 容器的环境下可以任意使用Session API
(4)可以响应认证、访问控制,或者 Session 生命周期中发生的事件
(5)可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
(6)支持单点登录(SSO)功能
(7)支持提供“Remember Me”服务,获取用户关联信息而无需登录
…
等等——都集成到一个有凝聚力的易于使用的API。
二.Apache Shiro Features 特性
Apache Shiro是一个全面的、蕴含丰富功能的安全框架。下图为描述Shiro功能的框架图:
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:
(1)Authentication(认证):用户身份识别,通常被称为用户“登录”
(2)Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
(3)Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
(4)Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
三具体实现
管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
1、pom.xml添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
2、编写Shiro配置类
Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。
package com.java.core.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/");
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/login.html");
// 设置拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/left", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
//开放登陆接口
filterChainDefinitionMap.put("/login", "anon");
// TODO
filterChainDefinitionMap.put("/test/test1", "anon");
//其余接口一律拦截
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
System.out.println("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(customRealm());
//注入缓存管理器;
securityManager.setCacheManager(ehCacheManager());
return securityManager;
}
/**
* 自定义身份认证 realm;
* <p>
* 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
* 否则会影响 CustomRealm类 中其他类的依赖注入
*/
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
/**
* shiro缓存管理器;
* 需要注入对应的其它的实体类中:
* 1、安全管理器:securityManager
* 可见securityManager是整个shiro的核心;
* @return
*/
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
/**
* thymeleaf和shiro
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
3、编写自定义Realm验证类,user表、user_role表和role_permissions表
package com.java.core.config;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import tk.mybatis.mapper.entity.Example;
import tk.mybatis.mapper.entity.Example.Criteria;
import com.java.system.dao.SystemUserMapper;
import com.java.system.model.SystemUser;
public class CustomRealm extends AuthorizingRealm {
@Autowired
private SystemUserMapper systemUserMapper;
/**
* 获取身份验证信息
* Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
*
* @param authenticationToken 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 身份认证
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
Example example = new Example(SystemUser.class);
Criteria criteria = example.createCriteria();
// 登录名
criteria.andEqualTo("loginName", token.getUsername());
List<SystemUser> users = systemUserMapper.selectByExample(example);
String password = "";
if (users != null && users.size() > 0) {
password = users.get(0).getPassword();
}
if (users == null || users.size() == 0) {
throw new AccountException("用户名不正确");
}
if (!password.equals(new String((char[]) token.getCredentials()))) {
throw new AccountException("密码不正确");
}
if (users.get(0).getStatus() == null || users.get(0).getStatus() != 0) {
throw new AccountException("用户已被禁用");
}
Set<String> permissions = systemUserMapper.queryPermissions(token.getUsername());
if (permissions == null || permissions.size() == 0) {
throw new AccountException("该用户无任何权限");
}
// 把用户信息放到session中
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
request.getSession().setAttribute("user", users.get(0));
// 在登陆的时候清楚一次缓存信息,这样每次登陆重新获取授权信息
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
// 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验
return new SimpleAuthenticationInfo(token.getPrincipal(), password, getName());
}
/**
* 获取授权信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 权限认证
String userName = (String) SecurityUtils.getSubject().getPrincipal();
Set<String> permissions = systemUserMapper.queryPermissions(userName);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permissions);
return info;
}
}
5、ehcache.xml的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache" />
<!--
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 eternal="false" maxElementsInMemory="1000"
overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU" />
<!-- 登录记录缓存锁定10分钟 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
6、以登录身份验证为例,LoginController.java编写
/**
* 登陆
*
* @param username 用户名
* @param password 密码
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public Object login(HttpServletRequest request, String loginName, String password) {
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
String username = loginName;
UsernamePasswordToken token = new UsernamePasswordToken(username, DigestUtils.md5Hex(password));
// 执行认证登陆
subject.login(token);
Map<String,Object> map = new HashMap<String,Object>();
map.put("status",200);
map.put("message","成功登陆");
return map;
}
7、前端页面添加菜单权限管理,name -- 权限名
<shiro:hasAnyPermissions name="expend,recommend,recipe,moonKill,cookbook,carousel">
<ul class="borderBlue">
<p class="yyyy blue" ><span class="span1"></span>种花管理<span class="jiantou span2"></span></p>
<p class="slideP">
<span id="expend" class="slideInside" shiro:hasAnyPermissions="article,recommend" th:onclick="'javascript:go(\''+@{/test}+'\')'">种菜管理</span>
<span id="cookbook" class="slideInside" shiro:hasAnyPermissions="recipe,moonKill,cookbook" th:onclick="'javascript:go(\''+@{/test}+'\')'">买种管理</span>
<span id="carousel" class="slideInside" shiro:hasAnyPermissions="carousel" onclick="go('slideImage.html')">营销管理</span>
</p>
</ul>
</shiro:hasPermission>