1. 简介
Apache Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能:
1)认证(Authentication):用户身份识别,常被称为用户“登录”,判断用户是否登陆,如果未登陆,则拦截其请求
2)授权(Authorization):访问控制。当用户登陆后,判断其身份是否有权限访问相应的资源,如果没有权限则拦截
3)密码加密(Cryptography):保护或隐藏数据防止被偷窃。将MD5进行二次封装,让其更加容易使用。注意MD5不可逆运算
4)会话管理(Session Management)
2. Shiro内置过滤器
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
这些过滤器分为两组:
1)认证过滤器:anon(不认证也可以访问),authcBasic, authc(必须认证后才可访问)
2)授权过滤器:perms(指定资源需要哪些权限才可以访问),Roles, ssl, rest, port
3. 使用shiro的加密工具加密——Md5Hash
3.1 添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-aspectj</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
3.2 用法
shiro的加密工具使用起来很简单,直接使用Md5Hash的构造函数,例如:
private String getMd5Pwd(String pwd,String username) {
if(!StringUtils.isEmpty(pwd)) {
/* 使用shiro的Md5Hash对密码进行加密处理
* Md5Hash提供了三个构造函数,这里使用复杂的一种
* 参数一:源
* 参数二:盐,也就是搅乱码,这里使用用户名。
* 参数三:散列次数,也就是加密次数
* */
System.out.println("原密码:" + pwd);
Md5Hash md5 = new Md5Hash(pwd, username, hashIterations);
System.out.println("加密之后的密码:" + md5.toString());
return md5.toString();
}
return pwd;
}
hashIterations就是散列次数,一般设为2,数字越大,加密越复杂。
4. shiro的核心配置
4.1 在web.xml中添加过滤器代理DelegatingFilterProxy
<!-- shiro过滤器 -->
<filter>
<!-- 代理过滤器,转发给 spring 容器中一个id=shiroFilter的Bean来处理 -->
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>*.do</url-pattern>
<url-pattern>*.html</url-pattern>
<url-pattern>*</url-pattern> <!-- * 表示没有后缀名的请求 -->
</filter-mapping>
Spring提供的一个简便的过滤器处理方案,它将具体的操作交给内部的Filter对象delegate去处理,而这个delegate对象通过Spring的IOC容器获取,这里采用的是Spring的FactorBean的方式获取这个对象。
虽然中配置了这一个filter,但是它并没做任何实际的工作,而是把这个工作交由Spring容器中一个bean的id为shiroFilter的类,即ShiroFilterFactoryBean
4.2 添加shiro核心控制器的spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- shiro工厂,创建了该工厂,shiro的9个过滤器就默认的加载了 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 安全管理器,shiro的核心组件 Facade模式 -->
<property name="securityManager" ref="securityManager" />
<!-- 如果没有登录,访问资源就会自动跳转到login.html -->
<property name="loginUrl" value="/login.html" />
<!-- 当用户没有访问某个资源权限的时候,跳转到该页面 -->
<property name="unauthorizedUrl" value="/error.html" />
<!-- 定义过滤链:定义URL访问的时候,对应的认证或授权时处理的过滤器 -->
<property name="filterChainDefinitions">
<value>
/error.html = anon <!-- 注意:匿名的要放在前面 -->
/*.html = authc
/*.do = authc
/* = authc
</value>
</property>
</bean>
<!-- 安全管理器,shiro的核心组件 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
</bean>
</beans>
5. 匿名认证的使用
我们可以先做一个登录的认证
/** 根据用户名和密码查询员
* */
@RequestMapping(value="/checkUser")
public ResultEntity checkUser(HttpSession session,Emp emp) {
try {
if(null != emp) {
return new ResultEntity(false,500,"请输入用户名和密码!");
}
// 1. 创建令牌
UsernamePasswordToken upt = new UsernamePasswordToken(emp.getName(),emp.getPwd());
// 2. 获取主题:封装了当前用户的一些操作
Subject subject = SecurityUtils.getSubject();
// 3. 执行登录
subject.login(upt);
// 执行到这里,证明登录成功,直接返回
return new ResultEntity().ok();
} catch (Exception e) {
System.out.println(e);
return new ResultEntity(false,500,"登录失败!");
}
}
然后在xml文件中配置匿名访问连接
登录系统,发现还是登录不上,这是因为shiro还没这么智能,我们没告诉它去哪里找正确的用户名和密码,它怎么能知道我们是登录成功还是失败了,这时候,我们就要了解shiro的realm了
5.1 自定义Realm实现认证功能
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
回到上面的登录问题上,我们改用subject.login方法后,并不会调用登陆的业务层进行登陆的验证查询,即不会从数据库查找登陆的用户名和密码是否正确,而是将这项工作交给shiro去完成。那shiro是怎么知道登陆的用户名和密码是否正确的呢?其实它也需要用到我的登陆验证业务,这时它就得向“别人”打听一下,那就是Realm了。
真正实现登陆验证的是Realm,而shiro只是去调Realm
5.1.1 新建类,并继承AuthorizingRealm类
package erp.web.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class ErpRealm extends AuthorizingRealm{
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// TODO Auto-generated method stub
return null;
}
}
注意:这里有两个类都可以解决我们的问题一个是AuthenticatingRealm,一个就是AuthorizingRealm,这两个有什么关系了?
AuthenticatingRealm是AuthorizingRealm父类,包含了认证的相关操作,AuthorizingRealm既包含认证也包含授权的相关操作。
5.1.2 在xml文件中配置realm
<!-- 安全管理器,shiro的核心组件 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="erpRealm"></property>
</bean>
<!-- 自定义realm -->
<bean id="erpRealm" class="erp.web.realm.ErpRealm"></bean>
5.1.3 实现认证逻辑
package erp.web.realm;
import javax.annotation.Resource;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import cn.bjc.biz.IEmpBiz;
import cn.bjc.entity.Emp;
public class ErpRealm extends AuthorizingRealm{
@Resource
private IEmpBiz empBiz;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
/**认证
* @return 返回null 表示认证失败 返回AuthenticationInfo的实现类,表示认证通过
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 1. 获取用户名和密码 AuthenticationToken 是 UsernamePasswordToken的父类,所以可以强转
UsernamePasswordToken upt = (UsernamePasswordToken)token;
String username = upt.getUsername();
String password = new String(upt.getPassword());
// 构建查询条件,并查询数据库
Emp emp = empBiz.getEmpByNameAndPwd(username,password);
if(null != emp) {
/*
* 构造参数:
* 1. 主角:登录用户
* 2.授权码:写进去一个值就行,一般就用密码或者用户名
* 3. realm名称
* */
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(emp,password,getName());
return info;
}
return null;
}
}
然后,运行,登录,发现闪了一下,又回到了登录页面,并提示请登录(这里是因为我做了处理的),出现这个的原因是,前面的开发中,loginUser对象放在session中,现在使用了shiro之后,登录对象放在shiro的subject中了,所以session中没有loginUser对象,所以登不进去了。解决办法也很简单,将获取loginUser的showname与logout与getLoginuser方法都改成从subject中获取即可,例如:
@RequestMapping(value="/showName")
public ResultEntity showName(HttpSession session) {
try {
// 1. 获取主题(再realm中有设置new SimpleAuthenticationInfo(emp,password,getName());)
Subject subject = SecurityUtils.getSubject();
// 2. 提取猪脚对象
Emp loginUser = (Emp) subject.getPrincipal();
if(null != loginUser) {
return new ResultEntity(true, loginUser.getUuid(), loginUser.getUsername());
} else {
return new ResultEntity(false, 500, "请先登录!");
}
} catch (Exception e) {
e.printStackTrace();
return new ResultEntity(false, 500, "服务器异常!");
}
}
5.2 认证的过程
shiro的工作是通过它的三大核心组件subject(登录用户)、shiro SecurityManager(管理登录用户)、Realm(认证数据源)来完成认证过程的。
当我们的系统被访问的时候,会调用shiro的主题subject,然后subject的方法会去调用SecurityManager,去进行realm的调用。当我们是subject调用login方法的时候,实际上是调用安全管理SecurityManager的的login方法
6. 授权
授权就是通过设置规则,指定哪些URL需要哪些权限才可以访问
6.1 授权的配置——perms
在applicationContext_shiro.xml中配置一个权限,如图:
表示,当前访问的用户需要有dep权限。
注意:支持中文
访问,带dep的url,发现,无法访问了,如图:
然后,在realm中的授权方法中添加权限,如图;
然后再次访问,可以访问了,如图:
从这个案例中,可以知道,两点
1)授权方法的作用:告诉shiro当前用户有什么权限
2)配置信息的作用:告诉shiro什么资源有什么权限才可以访问
6.2 登录用户授权的实现
首先在配置文件中配置url访问规则,如图:
然后,在realm中,通过用户的权限菜单授权,例如:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("执行了授权的方法。。。");
// 创建授权对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 获取主题对象并强转层emp对象
Emp emp = (Emp)principals.getPrimaryPrincipal();
// 根据用户的id,查询用户下的权限菜单名称
List<String> menuNames = empBiz.getAllMenuNameByEmpuuid(emp.getUuid());
// 添加权限并返回
info.addStringPermissions(menuNames);
return info;
}
注意:在perms中定义权限码的时候,可以定义多个,但是这时候,每个授权码之间是and的关系,即,当在一个perms中配置多个授权码的时候,登录用户必须同时满足这些条件才能登录成功。
例如:/store.html=perms["仓库1号","仓库2号"]
6.3 自定义授权过滤器
前面我们了解到shiro中,当一个连接有多个权限的时候,默认的过滤器不能满足我们的or的诉求,所以,我们强烈的需要可以自定义一个过滤器,以满足我们只要有一个权限码满足就可以访问的诉求,我们先看一下perms过滤器的源码定义:
package org.apache.shiro.web.filter.authz;
import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.subject.Subject;
public class PermissionsAuthorizationFilter extends AuthorizationFilter {
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
String[] perms = (String[]) mappedValue;
boolean isPermitted = true;
if (perms != null && perms.length > 0) {
if (perms.length == 1) {
if (!subject.isPermitted(perms[0])) {
isPermitted = false;
}
} else {
if (!subject.isPermittedAll(perms)) {
isPermitted = false;
}
}
}
return isPermitted;
}
}
可看出,该过滤器有三个参数,request、response、和mappedValue,其中request和response并未用到,这两个参数可以作为扩展参数来看,而mapperValue就是我们配置的perms的参数值,这里首先将Object转成String[],通过其length来判断,当length>1的时候,如图:
可以知道,只要有一个不满足,就直接返回false了,所以,我们只需要重写该过滤器,将这段代码改造一下即可。
6.3.1 过滤器定义
package erp.web.filter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
/**自定义授权过滤器
* @author Administrator
*
*/
public class ErpAuthorizationFilter extends AuthorizationFilter{
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
// 获取主题
Subject subject = getSubject(request, response);
// 权限码
String[] perms = (String[]) mappedValue;
if (perms != null && perms.length > 0) {
for(String str : perms) {
if(subject.isPermitted(str)) { // 只要有一个满足,就返回true
return true;
}
}
} else { // 没有配置权限,放行
return true;
}
return false;
}
}
6.3.2 过滤器配置
1)注入过滤器
<!-- 自定义过滤器配置 -->
<bean id="erpAuthorizationFilter" class="erp.web.filter.ErpAuthorizationFilter"></bean>
2)在shiroFilter中注入过滤器
<!-- 注入过滤器 -->
<property name="filters">
<map>
<entry key="myPerms" value-ref="erpAuthorizationFilter"></entry>
</map>
</property>
3)定义权限码数组
注意:自定义过滤器的key的属性值是什么,这里的权限组就用什么名字,如果自定义的过滤器的key的值为perms,那么咱们的自定义的过滤器就覆盖了shiro原有的perms过滤器了。
总结:
Shiro的三大核心组件
Subject:正与系统进行交互的人,或某一个第三方服务。所有Subject实例都被绑定到(且这是必须的)一个SecurityManager上。
SecurityManager:Shiro架构的心脏,典型的Facade模式。用来协调内部各安全组件,管理内部组件实例,并通过它来提供安全 管理的各种服务。当Shiro与一个Subject进行交互时,实质上是幕后的SecurityManager处理所有繁重的 Subject安全操作。
Realms:本质上是一个特定安全的DAO。当配置Shiro时,必须指定至少一个Realm用来进行身份验证和/或授权。Shiro提供了多 种可用的Realms来获取安全相关的数据。如关系数据库(JDBC),INI及属性文件等。可以定义自己Realm实现来代表自 定义的数据源