- 什么是shiro
- 先要做一些配置
- 认证实现
- 主角对象的提取
- 授权实现
- 自定义授权过滤器
- Shiro细颗粒授权控制
什么是shiro
Apache Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能:
认证(Authentication):用户身份识别,常被称为用户“登录”,判断用户是否登陆,如果未登陆,则拦截其请求
授权(Authorization):访问控制。当用户登陆后,判断其身份是否有权限访问相应的资源,如果没有权限则拦截
密码加密(Cryptography):保护或隐藏数据防止被偷窃。将MD5进行二次封装,让其更加容易使用。注意MD5不可逆运算
会话管理(Session Management)
shiro内置过滤器:
anon
authc
authcBasic
perms
port
rest
roles
ssl
user
先要做一些配置
添加pom依赖
<shiro.ver>1.2.3</shiro.ver>
</properties>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.ver}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.ver}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.ver}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-aspectj</artifactId>
<version>${shiro.ver}</version>
</dependency>
配置web.xml
添加过滤器代理DelegatingFilterProxy,要放在struts2的核心过滤器之前
<!-- shiro过滤器 要放在struts2的核心过滤器之前-->
<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>*.action</url-pattern>
<url-pattern>*.html</url-pattern>
<url-pattern>*</url-pattern>
</filter-mapping>
Spring提供的一个简便的过滤器处理方案,它将具体的操作交给内部的Filter对象delegate去处理,而这个delegate对象通过Spring的IOC容器获取,这里采用的是Spring的FactorBean的方式获取这个对象。
虽然中配置了这一个Struts2 的 filter,但是它并没做任何实际的工作,而是把这个工作交由Spring容器中一个bean的id为shiroFilter的类,即ShiroFilterFactoryBean。
添加shiro核心控制器的spring配置文件
添加applicationContext_shiro.xml文件到erp_web的资源目录下
<?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的过滤工厂,相当默认的加载了9个过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--加载核心组件 -->
<property name="securityManager" ref="securityManager" />
<!-- 用户如果没有登陆,当他在访问资源的时候,就会自动跳转到登陆的页面 -->
<property name="loginUrl" value="/login.html" />
<!-- 当用户没有访问某项资源权限的时候,跳转到该页面 -->
<property name="unauthorizedUrl" value="/error.html" />
<!-- 过滤链的定义:定义URL访问的时候对应的认证或授权时处理的过滤器 -->
<property name="filterChainDefinitions">
<value>
<!--前两行anon(认证过滤器)表示,所指定的资源不需要任何权限就可以访问 -->
/error.html = anon
/login_*=anon
<!--后三行authc(认证过滤器)表示,所指定的资源必须认证后才可以访问访问 -->
/*.html = authc
/*.action=authc
/*=authc
</value>
</property>
</bean>
<!-- 安全管理器,shiro核心组件(大脑) Facade模式 -->
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
</bean>
</beans>
如果没有error.html 的话创建
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>提示信息</title>
</head>
<body>
尊敬的客户,你没有访问的权限!
</body>
</html>
认证实现
验证登录的方法(页面点击登录后跳转的后端方法)
/**
* 验证登录 两种验证方式,第一种只最原始的方法,第二种是利用shiro进行认证授权
*/
public void checkUser(){
//第一种方式
// Emp user = empBiz.checkUserLogin(username,pwd);
// if(user == null){
// writeToPage("输入有误");
// }else{
// ActionContext.getContext().getSession().put("user", user);
// writeToPage("登陆成功");
// }
//第二种方式
try {
//1.创建令牌
UsernamePasswordToken upt = new UsernamePasswordToken(username,pwd);
//2.获得主题
Subject subject = SecurityUtils.getSubject();
//3.执行login
subject.login(upt);
writeToPage("登陆成功");
} catch (AuthenticationException e) {
writeToPage("输入有误");
e.printStackTrace();
}
}
public void writeToPage( Object o){
String jsonString = JSON.toJSONString(o);
HttpServletResponse response = ServletActionContext.getResponse();
response.setContentType("text/html;charset=utf-8");
try {
response.getWriter().print(jsonString);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
自定义Realm
真正实现登陆验证的是Realm,而shiro只是去调Realm
创建ErpRealm类继承自AuthorizingRealm
import java.util.List;
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;
/**
* 自定义realm,实现认证授权
* @author admin
*
*/
public class ERPRealm extends AuthorizingRealm{
private IEmpBiz empBiz;//注入biz
/**
* 授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("授权..");
return null;
}
/**
* 认证方法
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("认证..");
UsernamePasswordToken upt = (UsernamePasswordToken)token;
String pwd = new String(upt.getPassword());
Emp user = empBiz.checkUserLogin(upt.getUsername(),pwd);
if(null != user) {
//返回认证信息
//参数一:主角,当前登录的用户
//参数二:这书或者凭证,这里我们用的是密码
//参数三:当前realm的名称
return new SimpleAuthenticationInfo(user,pwd,getName());
}else {
return null;
}
}
public IEmpBiz getEmpBiz() {
return empBiz;
}
public void setEmpBiz(IEmpBiz empBiz) {
this.empBiz = empBiz;
}
}
配置ErpRealm, 在applicationContext_shiro.xml中配置
<?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的过滤工厂,相当默认的加载了9个过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--加载核心组件 -->
<property name="securityManager" ref="securityManager" />
<!-- 用户如果没有登陆,当他在访问资源的时候,就会自动跳转到登陆的页面 -->
<property name="loginUrl" value="/login.html" />
<!-- 当用户没有访问某项资源权限的时候,跳转到该页面 -->
<property name="unauthorizedUrl" value="/error.html" />
<!-- 过滤链的定义:定义URL访问的时候对应的认证或授权时处理的过滤器 -->
<property name="filterChainDefinitions">
<value>
<!--前两行anon(认证过滤器)表示,所指定的资源不需要任何权限就可以访问 -->
/error.html = anon
/login_*=anon
<!--后三行authc(认证过滤器)表示,所指定的资源必须认证后才可以访问访问 -->
/*.html = authc
/*.action=authc
/*=authc
</value>
</property>
</bean>
<!-- 安全管理器,shiro核心组件(大脑) Facade模式 -->
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="erpRealm"></property>
</bean>
<!-- 自定义的realm 实现授权认证的逻辑类-->
<bean id="erpRealm" class="realm.ERPRealm">
<property name="empBiz" ref="empBiz"></property>
</bean>
</beans>
认证完成,启动项目测试,浏览器地址栏输入localhost:8080/erpa/login.html 之外的的url : localhost:8080/erpa/xxx.html,都会自动跳转到登录页面login.html,登录页面输入正确的用户名密码,可以正常登录。
主角对象的提取
Shiro提供了会话管理机制,实际上我们自定义的realm的认证方法返回值对象中的主角对象就是登陆的用户,它可以代替我们之前存入session中的emp对象。我们可以通过subject的getPrincipal方法将其提取出来。
/**
* 显示用户名
* @param o
*/
public void showName(){
//Emp user = (Emp)ActionContext.getContext().getSession().get("user");
//shiro回话特点实现该功能
Emp user = (Emp)SecurityUtils.getSubject().getPrincipal();
if(null != user) {
writeToPage(user.getName());
}
}
/**
* 退出登录
* @param o
*/
public void loginOut(){
//ActionContext.getContext().getSession().remove("user");
SecurityUtils.getSubject().logout();
}
授权实现
修改ErpRealm中的授权方法:
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 entity.Emp;
import entity.Menu;
import service.IEmpBiz;
/**
* 自定义realm,实现认证授权
* @author admin
*
*/
public class ERPRealm extends AuthorizingRealm{
private IEmpBiz empBiz;//注入biz
/**
* 授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("授权..");
Emp user = (Emp)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo sai = new SimpleAuthorizationInfo();
List<Menu> menus = empBiz.getMenusByUser(user);
for (Menu menu : menus) {
System.out.println("当前用户:"+user.getName()+" 权限:"+menu.getMenuname());
sai.addStringPermission(menu.getMenuname());
}
return sai;
}
/**
* 认证方法
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("认证..");
UsernamePasswordToken upt = (UsernamePasswordToken)token;
String pwd = new String(upt.getPassword());
Emp user = empBiz.checkUserLogin(upt.getUsername(),pwd);
if(null != user) {
//返回认证信息
//参数一:主角,当前登录的用户
//参数二:这书或者凭证,这里我们用的是密码
//参数三:当前realm的名称
return new SimpleAuthenticationInfo(user,pwd,getName());
}else {
return null;
}
}
public IEmpBiz getEmpBiz() {
return empBiz;
}
public void setEmpBiz(IEmpBiz empBiz) {
this.empBiz = empBiz;
}
}
这里多说两句,授权方法中根据用户查询器拥有的权限时,emp和role多对多关联,role和menu多对多关联,查询时设计hql的连接查询,这里粘上代码:
/**
* 获取用户权限列表
*/
@Override
public List<Menu> getMenusByUser(Emp user) {
List<Menu> list = new ArrayList<>();
String hql = "select m from Emp e join e.roles r join r.menus m where e.id=?";
return (List<Menu>) this.getHibernateTemplate().find(hql, user.getId());
}
配置授权控制规则
在applicationContext_shiro.xml中配置
<?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的过滤工厂,相当默认的加载了9个过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--加载核心组件 -->
<property name="securityManager" ref="securityManager" />
<!-- 用户如果没有登陆,当他在访问资源的时候,就会自动跳转到登陆的页面 -->
<property name="loginUrl" value="/login.html" />
<!-- 当用户没有访问某项资源权限的时候,跳转到该页面 -->
<property name="unauthorizedUrl" value="/error.html" />
<!-- 过滤链的定义:定义URL访问的时候对应的认证或授权时处理的过滤器 -->
<property name="filterChainDefinitions">
<value>
/error.html = anon
/login_*=anon
/dep.html=perms["部门"]
/dep_*=perms["部门"]
/emp.html=perms["员工"]
/emp_*=perms["员工"]
/goods.html=perms["商品"]
/goods_*=perms["商品"]
/*.html = authc
/*.action=authc
/*=authc
</value>
</property>
</bean>
<!-- 安全管理器,shiro核心组件(大脑) Facade模式 -->
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="erpRealm"></property>
</bean>
<!-- 自定义的realm 实现授权认证的逻辑类-->
<bean id="erpRealm" class="realm.ERPRealm">
<property name="empBiz" ref="empBiz"></property>
</bean>
</beans>
注意:
/dep.html=perms[“部门”]
/dep_=perms[“部门”]
/emp.html=perms[“员工”]
/emp_=perms[“员工”]
/goods.html=perms[“商品”]
/goods_*=perms[“商品”]
perms配置要放在anon 和authc之间,中括号中的双引号里的内容就是权限名称(和授权方法中的这行代码对应sai.addStringPermission(menu.getMenuname());),什么权限对应什么资源!
授权方法的作用:告诉shiro当前用户有什么权限
配置信息的作用:告诉shiro什么资源有什么权限才可以访问
至此,授权实现完成。启动项目测试,发现用户只能访问其所拥有权限对应的资源。
自定义授权过滤器
当一个URL有多个权限需要访问的时候,我们应该怎么配置呢?
如果按下面的方法来配置
/orders.html=perms[“采购订单查询”]
/orders.html=perms[“采购订单审核”]
那么只有最后一条生效,前面的会被后面的覆盖掉
如果按下面的方法来配置
/orders.html=perms[“采购订单查询”,“采购订单审核”]
系统默认是同时具备这两个权限才可以访问此URL,而我们的需求是,只要有具备一种就可以访问此URL。系统使用的是and关系,而不是or关系,那我们如何让它实现or关系呢?这就需要我们去自定义授权过滤器啦
创建自定义过滤器,继承自AuthorizationFilter
package realm;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
public class ERPAuthorizationFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
//获取主题
Subject subject = getSubject(request, response);
//得到配置文件中的权限列表
// /orders.html=perms["采购订单的查询","采购订单的审核","采购订单的确认","采购订单的入库"]
// mappedValue="采购订单的查询","采购订单的审核","采购订单的确认","采购订单的入库"
String[] perms = (String[])mappedValue;
//如果为空或长度为0,放行
if(null == perms || perms.length == 0) {
return true;
}
//权限检查
for (String perm : perms) {
//只要有一个符合就放行
if(subject.isPermitted(perm)) {
return true;
}
}
return false;
}
}
配置过滤器
<?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的过滤工厂,相当默认的加载了9个过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" >
<!--加载核心组件 -->
<property name="securityManager" ref="securityManager" />
<!-- 引入自定义的授权过滤器 -->
<property name="filters">
<map>
<entry key="perms" value-ref="erpAuthorizationFilter"></entry>
</map>
</property>
<!-- 用户如果没有登陆,当他在访问资源的时候,就会自动跳转到登陆的页面 -->
<property name="loginUrl" value="/login.html" />
<!-- 当用户没有访问某项资源权限的时候,跳转到该页面 -->
<property name="unauthorizedUrl" value="/error.html" />
<!-- 过滤链的定义:定义URL访问的时候对应的认证或授权时处理的过滤器 -->
<property name="filterChainDefinitions">
<value>
/error.html = anon
/login_*=anon
/dep.html=perms["部门"]
/dep_*=perms["部门"]
/emp.html=perms["员工"]
/emp_*=perms["员工"]
/goods.html=perms["商品","部门","员工"]
/goods_*=perms["商品","员工"]
/*.html = authc
/*.action=authc
/*=authc
</value>
</property>
</bean>
<!-- 安全管理器,shiro核心组件(大脑) Facade模式 -->
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="erpRealm"></property>
</bean>
<!-- 自定义的realm 实现授权认证的逻辑类-->
<bean id="erpRealm" class="realm.ERPRealm">
<property name="empBiz" ref="empBiz"></property>
</bean>
<!-- 自定义的授权过滤器 -->
<bean id="erpAuthorizationFilter" class="realm.ERPAuthorizationFilter"></bean>
</beans>
注意:引入自定义的授权过滤器时
<property name="filters">
<map>
<entry key="perms" value-ref="erpAuthorizationFilter"></entry>
</map>
</property>
其中map中的key值为什么是perms呢,由什么决定呢?哈哈,因为我们自定义重写的正是perms,这样做是告诉shiro过滤的时候用我们写的erpAuthorizationFilter 进行过滤!
ok,自定义的授权过滤器完成,运行项目测试,发现 只要拥有 部门,员工,商品,三个权限中的任意一个,就都可以访问goods.html。
Shiro细颗粒授权控制
我们前面做的权限控制都是建立在对URL的访问控制,我们把它称之为粗颗粒的访问控制。
我们还可以使用shiro的细颗粒授权控制。
细颗粒授权控制包括:方法级别 与 代码级别
方法级别控制
对某个方法加访问控制,用户必须拥有某项权限才可以访问该方法,没有权限则抛出异常,无法访问
1、开启注解,在applicationContext_shiro.xml中添加:
<!-- 启动shiro注解 -->
<bean
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor" >
<!-- 默认使用JDK代理 ,如被代理类没有实现接口,必须使用下列配置开启 cglib代理 -->
<property name="proxyTargetClass" value="true" />
</bean>
<bean
class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!-- 对安全管理器 增强代码 , spring 后处理器 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
2、修改OrdersBiz和OrderdetailBiz,在其方法添加注解
/**
* 审核
* @param uuid 订单编号
* @param empUuid 审核员
*/
@RequiresPermissions("采购订单审核")
public void doCheck(Long uuid, Long empUuid){
//获取订单,进入持久化状态
Orders orders = ordersDao.get(uuid);
//订单的状态
if(!Orders.STATE_CREATE.equals(orders.getState())){
throw new ErpException("亲!该订单已经审核过了");
}
//1. 修改订单的状态
orders.setState(Orders.STATE_CHECK);
//2. 审核的时间
orders.setChecktime(new Date());
//3. 审核人
orders.setChecker(empUuid);
}
/**
* 确认
* @param uuid 订单编号
* @param empUuid 采购员
*/
@RequiresPermissions("采购订单确认")
public void doStart(Long uuid, Long empUuid){
//获取订单,进入持久化状态
Orders orders = ordersDao.get(uuid);
//订单的状态
if(!Orders.STATE_CHECK.equals(orders.getState())){
throw new ErpException("亲!该订单已经确认过了");
}
//1. 修改订单的状态
orders.setState(Orders.STATE_START);
//2. 确认的时间
orders.setStarttime(new Date());
//3. 确认人
orders.setStarter(empUuid);
}
如果访问了未授权的方法,则会报如下错误:
代码级别控制
代码级别控制:指的是在代码中加入权限控制
我们的采购订单申请和销售订单录入,都会调用OrdersBiz的add方法。这样只要用户具有其中一个权限,就可以执行另一个功能了。这样是很恐怖的!
那可怎么办呢?我们可以把控制粒度放在更细的层面上,也就是代码级别访问控制
修改OrdersBiz的add方法,方法一开始就加入以下代码