Springboot整合(9)——Shiro
Springboot整合(9)——Shiro
Shiro基本配置
1. pom增加依赖
<!-- shiro spring. -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
2. 编写自己的shiro域
/**
* 身份校验核心类;
*
* @version v.0.1
*/
publicclass MyShiroRealm extends AuthorizingRealm {
privatestaticfinal Log LOG = LogFactory.getLog(MyShiroRealm.class);
@Resource
UserService userService;
/**
* 认证信息.(身份验证) : Authentication 是用来验证用户身份
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
// 获取用户的输入的账号.
String username = (String) token.getPrincipal();
System.out.println(token.getCredentials());
// 通过username从数据库中查找 User对象,如果找到,没找到.
// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUser userInfo = userService.getByLoginName(username);
System.out.println("----->>userInfo=" + userInfo);
if (userInfo == null) {
returnnull;
}
// 加密方式;
// 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
/*
* 这里调的是SimpleAuthenticationInfo(principal,
* hashedCredentials,credentialsSalt,realmName)
* 第三个参数是盐值,本处示例用的是用户id,实际可根据需要使用任意值或者干脆不用,盐值的具体用处请自行百度
*/
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, // 用户名
userInfo.getLoginPassword(), // 密码
ByteSource.Util.bytes(userInfo.getId()), // salt=username+salt,这里直接使用的userId
getName() // realm name
);
// init session
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute("user", userInfo);
returnauthenticationInfo;
}
/**
* 此方法调用 hasRole,hasPermission的时候才会进行回调.
*
* 权限信息.(授权): 1、如果用户正常退出,缓存自动清空; 2、如果用户非正常退出,缓存自动清空;
* 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。(需要手动编程进行实现;放在service进行调用)
* 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,调用clearCached方法;
* :Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
/*
* 当没有使用缓存的时候,不断刷新页面的话,这个代码会不断执行,当其实没有必要每次都重新设置权限信息,所以我们需要放到缓存中进行管理;
* 当放到缓存中时,这样的话,doGetAuthorizationInfo就只会执行一次了,缓存过期之后会再次执行。
*/
System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRole("testAdmin");
authorizationInfo.addStringPermission("test:Permission");
returnauthorizationInfo;
}
}
注:本文演示shiro使用不给出user,role,permission的具体实现,只演示基本流程,role和permission的设置都直接使用硬编码的方式写在代码里,实际项目使用时将硬编码改成相应的实现逻辑即可。这里将上述realm的实现逻辑流程说明一下
doGetAuthenticationInfo,这个方法在用户登陆时调用,入参AuthenticationToken包含了用户名密码→根据用户名调service获取是否有这个用户并拿到这个用户的信息→验证用户名密码是否正确→做session初始化
doGetAuthorizationInfo,这个方法在hasRole,hasPermission的时候才会进行回调,该方法的具体实现就是为当前用户设置role和permission信息。
3. shiro的配置类ShiroConfiguration
@Configuration
publicclass ShiroConfiguration {
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
*
* Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shiroFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置静态资源匿名访问
filterChainDefinitionMap.put("/vendors/**", "anon");
filterChainDefinitionMap.put("/resources/**", "anon");
// 配置druid连接池后台可以匿名访问
filterChainDefinitionMap.put("/druid/**", "anon");
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
// <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/**", "authc");
// 配置登陆链接
shiroFilterFactoryBean.setLoginUrl("/user/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/user/list");
// 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
returnshiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
returnsecurityManager;
}
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
returnmyShiroRealm;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
// hashedCredentialsMatcher.setHashIterations(2);// 散列的次数,比如散列两次,相当于
// md5(md5(""));
returnhashedCredentialsMatcher;
}
}
4. controller配置
@RequestMapping(value = "user/login", method = RequestMethod.GET)
public ModelAndView login(HttpServletRequest request) {
returnnew ModelAndView("user/login");
}
@RequestMapping(value = "user/login", method = RequestMethod.POST)
public ModelAndView login(HttpServletRequest request, String username, String password) {
Exception exception = null;
try {
SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password)); // 完成登录
} catch (Exception e) {
exception = e;
}
String msg = "";
if (exception != null) {
if (exceptioninstanceof UnknownAccountException) {
System.out.println("UnknownAccountException -- > 账号不存在");
msg = "账号不存在";
} elseif (exceptioninstanceof IncorrectCredentialsException) {
System.out.println("IncorrectCredentialsException -- > 密码不正确");
msg = "密码不正确";
} else {
msg = "else >> " + exception;
System.out.println("else -- >" + exception.getMessage());
}
}
Map<String, Object> model = new HashMap<String, Object>();
model.put("error", msg);
returnnew ModelAndView("user/login", model);
}
5. 将数据库中的用户密码加密
写一个密码加密的工具类:
publicclass PasswordEncoder {
publicstatic String MD5Encoding(String password, String userId) {
returnnew SimpleHash("md5", password, userId, 1).toString();
}
}
产生加密密码
@Test
publicvoid test() {
System.out.println(PasswordEncoder.MD5Encoding("123456", "1"));
}
将产生的密码eeafb716f93fa090d7716749a6eefa72写入数据库
5. 编写login.jsp
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>login</title>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path + "/";
%>
</head>
<body>
<form id="loginForm" action="<%=basePath%>user/login" method="post">
loginName : <input type="text" id="username" name="username"><br>
password : <input type="password" id="password" name="password"><br>
<input type="submit" value="submit"><br>
</form>
<p id="message">${error}</p>
</body>
6. 测试
访问任意非login页面均会跳转至login页面,登陆后跳转至最后访问的页面url,如果没有则跳转至设置的sucessful页面,即:访问user/add→未登录,shiro将页面跳转至login→登陆通过直接跳转至user/add; 访问user/login→未登录,shiro将页面跳转至login→登陆通过直接跳转至user/list(配置的sucessful页面)
Shiro注解的使用
1. 在ShiroConfiguration中开启注解
/**
* 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
returnauthorizationAttributeSourceAdvisor;
}
后面你会发现只是加了上面的代码shiro的注解@RequireRoles和@RequiresPermissions还是无法生效,还需要加入下面的代码,开启spring的自动代理
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
returndefaultAAP;
}
2. 编写controller层测试代码
@RequestMapping(value = "user/testShiro", method = { RequestMethod.GET, RequestMethod.POST })
public String testShiro() {
return"user/testShiro";
}
@RequiresRoles("noExistRole")
@ResponseBody
@RequestMapping(value = "user/testRequiresRolesNotExist", method = { RequestMethod.GET, RequestMethod.POST })
public ReturnResult testRequiresRolesNotExist() {
ReturnResult rs = new ReturnResult();
rs.setMessage("noExistRole");
returnrs;
}
@RequiresRoles("testAdmin")
@ResponseBody
@RequestMapping(value = "user/testRequiresRolesExist", method = { RequestMethod.GET, RequestMethod.POST })
public ReturnResult testRequiresRolesExist() {
ReturnResult rs = new ReturnResult();
rs.setMessage("ExistRole");
returnrs;
}
@RequiresPermissions("notExistPermission")
@ResponseBody
@RequestMapping(value = "user/testRequiresPermissionsNotExist", method = { RequestMethod.GET, RequestMethod.POST })
public ReturnResult testRequiresPermissionsNotExist() {
ReturnResult rs = new ReturnResult();
rs.setMessage("notExistPermission");
returnrs;
}
@RequiresPermissions("test:Permission")
@ResponseBody
@RequestMapping(value = "user/testRequiresPermissionsExist", method = { RequestMethod.GET, RequestMethod.POST })
public ReturnResult testRequiresPermissionsExist() {
ReturnResult rs = new ReturnResult();
rs.setMessage("ExistPermission");
returnrs;
}
3. 编写jsp测试代码
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path + "/";
%>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="<%=basePath%>vendors/jquery/jquery.min.js"></script>
<title>Test Shiro </title>
<script type="text/javascript">
function add(btn) {
var url = "<%=basePath%>user/"+$(btn).val();
$.ajax({
type : 'POST',
cache : false,
url : url,
async : false,
success : function(result) {
$("#message").html(result.message);
},
error : function(result) {
alert(result);
}
});
}
</script>
</head>
<body>
<input type="button" value="testRequiresRolesNotExist" onclick="add(this);">
<input type="button" value="testRequiresRolesExist" onclick="add(this);">
<input type="button" value="testRequiresPermissionsNotExist" onclick="add(this);">
<input type="button" value="testRequiresPermissionsExist" onclick="add(this);">
<p id="message"></p>
</body>
</html>
4. 测试,访问http://localhost:8088/KnowledgeIsland/user/testShiro,分别点击4个button,有权限的会返回结果,没有权限的会显示服务器内部错误(其实是被全局异常处理器RuntimeException处理了),说明注解已经生效
Shiro全局异常处理
上一节最后测试的时候异常被RuntimeException处理了,实际这里报的异常是org.apache.shiro.authz.UnauthorizedException,我们显然希望全局异常中能对这种异常单独做处理,所以在BaseController里加入这个异常的处理逻辑即可(顺便把登陆异常也加进去了)
/**
* 授权异常
*/
@ExceptionHandler({ UnauthorizedException.class })
@ResponseBody
public ReturnResult unauthorizedException() {
returnnew ReturnResult(0, "权限不足!");
}
/**
* 登录异常
*/
@ExceptionHandler({ AuthenticationException.class })
@ResponseBody
public ReturnResult authenticationException() {
returnnew ReturnResult(0, "未登陆!");
}
这样再运行前一节的测试代码就会提示权限不足了
为Shiro配置Cache
我们在前文中测试shiro的权限控制时,每点击一次testRequiresRolesExist或者testRequiresPermissionsExist,观察后台都会发现每次都会去调用一次MyShiroRealm中的doGetAuthorizationInfo方法,即读取一次用户权限,而实际开发中用户的权限信息是不会频繁发生变化的,不需要每次访问的时候都去读取,所以我们可以为Shiro配置缓存,将用户权限信息放在缓存里,避免重复读取,具体配置如下(本文使用ehCache做缓存)
1. pom中添加依赖
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
2. 配置文件ehcache-shiro.xml
<?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>
3. 在ShiroConfiguration里进行配置
① 定义缓存管理器bean
/**
* shiro缓存管理器; 需要注入对应的其它的实体类中: 1、安全管理器:securityManager
* 可见securityManager是整个shiro的核心;
*
* @return
*/
@Bean
public EhCacheManager ehCacheManager() {
System.out.println("ShiroConfiguration.getEhCacheManager()");
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
returncacheManager;
}
② SecurityManager里注入ehCacheManager
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
// 注入缓存管理器;
securityManager.setCacheManager(ehCacheManager());
returnsecurityManager;
}
4. 再次测试,点击按钮时,仅在登陆后第一次调用一次MyShiroRealm中的doGetAuthorizationInfo方法,之后就不再调用
使用Shiro的Remember Me
Shiro中使用RememberMe功能只需要在ShiroConfiguration里做一些配置即可
1. 定义Cookie Bean
/**
* cookie对象;
*
* @return
*/
@Bean
public SimpleCookie rememberMeCookie() {
// 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
// <!-- 记住我cookie生效时间30天 ,单位秒;-->
simpleCookie.setMaxAge(259200);
returnsimpleCookie;
}
2. 定义Cookie管理bean
/**
* cookie管理对象;
*
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager() {
System.out.println("ShiroConfiguration.rememberMeManager()");
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
returncookieRememberMeManager;
}
3. 将cookieManager注入SecurityManager
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
// 注入缓存管理器;
securityManager.setCacheManager(ehCacheManager());
// 注入记住我管理器;
securityManager.setRememberMeManager(rememberMeManager());
returnsecurityManager;
}
4. 将shiro过滤器工厂ShiroFilterFactoryBean中的需要认证的过滤器authc改为通过rememberMe即可访问
注:这2个过滤器是可以共存的,非常敏感的url可以设置为必须认证,不算特别敏感的就可以设置为通过rememberMe即可访问,如可以配置如下:
filterChainDefinitionMap.put("security/**", "authc");
filterChainDefinitionMap.put("normal/**", "user");
5. login.jsp里增加rememberMe参数
6. controller中对login方法做相应修改
7. 测试
点submit登陆,页面不跳转,等下再说怎么解决这个问题。手动将url改为user/list,发现已经可以访问,说明登陆成功。再试试rememberMe是否已经工作,关闭浏览器,直接输入url:user/list,也可正常访问。说明rememberMe已经正常工作。
最后来说这个页面不跳转的问题,解决方法:在filterChain中增加如下配置即可
filterChainDefinitionMap.put("/user/login", "authc");
再次测试,一切正常