今天内容
第一章 Shiro概述
1. 当前项目的问题
- 用户不用登录,就能直接通过url访问saas系统内部的页面
直接输入http://localhost:8080/system/module/list.do就能看到所有模块
- 使用低权限的管理员身份登录,也可以访问高权限的用户菜单
比如使用普通员工登录, 直接url就能访问到它本应访问不到的页面
- 页面上标签未做到权限控制
给一个员工分配了部门管理\查看权限, 但没分配新增权限, 但页面上依旧显示新增按钮
问题:
1. 用户不登录就可以访问系统资源
2. 用户登录之后可以访问到不属于自己的资源
3. 页面标签没有做到权限限制
上述功能的描述,可以使用过滤器或拦截器完成,当然在实际开发者已经有组织把这些功能封装为框架,比如Shiro或者SpringSecurity,来简化权限的控制,我们今天使用shiro框架。
2. Shiro简介
Apache Shiro是Java的一个安全框架。不仅功能强大,而且使用简单,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。
3. Shiro的功能
* Authentication(认证)
用户登录,身份识别 --- 结果:不是系统用户不能访问系统
* Authorization(授权)
限定用户可进行的操作 ---- 结果:没权限不能访问资源
* Cryptography(加密)
安全数据加密
* Session Manager(会话)
用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中(类似于web的session)
* Web Integration
web容器集成
* Integrations
集成其他应用,比如spring、缓存
4. Shiro的架构(画)
* Application Code(应用)
我们自己编写的项目
* Subject(主体)
它是一个工具类,负责用户和shiro框架交互
* SecurityManager(安全管理器)
它是Shiro架构中最核心的组件,通过它可以协调其他组件完成用户认证和授权
* Realm(域)
它定义了访问数据的方式,用来连接不同的数据源,如:数据库,配置文件等
明确:我们使用shiro主要要做两件事
-
认证:认证指的是匹配用户名(邮箱)和密码,让平台认识你。
-
授权:授权指的是当前认证的用户进入平台,能操作哪些页面。
5. Shiro过滤器
过滤器简称 | 对应的java类 | 描述 |
---|---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 未认证访问 |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 认证后访问 |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | httpBasic认证后访问 |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 指定权限访问 |
port | org.apache.shiro.web.filter.authz.PortFilter | 指定端口访问 |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | 指定rest访问 |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 指定角色访问 |
ssl | org.apache.shiro.web.filter.authz.SslFilter | ssl认证后访问 |
user | org.apache.shiro.web.filter.authc.UserFilter | 指定用户访问 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter | 用户退出 |
6. Shiro标签
标签名称 | 标签条件(均是显示标签内容) |
---|---|
<shiro:authenticated > | 登录之后 |
<shiro:notAuthenticated > | 不在登录状态时 |
<shiro:guest > | 用户在没有RememberMe时 |
<shiro:user > | 用户在RememberMe时 |
<shiro:hasAnyRoles name=”abc,123” > | 在有abc或者123角色时 |
<shiro:hasRole name=”abc”> | 拥有角色abc |
<shiro:lacksRole name=”abc”> | 没有角色abc |
<shiro:hasPermission name=”abc”> | 拥有权限资源abc |
<shiro:lacksPermission name=”abc”> | 没有abc权限资源 |
<shiro:principal > | 默认显示用户名称 |
第二章 Shiro环境搭建
1. 导入坐标(已完成)
<!--shiro和spring整合-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!--shiro核心包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
2. web.xml配置
在web.xml中添加如下过滤器,注意filter-name的值是shiroFilter
<!-- Shiro Security filter filter-name这个名字的值将来还会在spring中用到-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<!--过滤所有请求-->
<url-pattern>/*</url-pattern>
</filter-mapping>
3. Spring整合shiro
添加spring整合shiro的配置文件
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" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- filter-name这个名字的值来自于web.xml中filter的名字 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!--认证不通过, 跳转的页面 -->
<property name="loginUrl" value="/login.jsp"></property>
<!--授权不通过, 跳转的页面 -->
<property name="unauthorizedUrl" value="/unauthorized.jsp"></property>
<!--拦截规则,注意拦截的顺序.次规则从上向下执行-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/css/** = anon
/img/** = anon
/plugins/** = anon
/make/** = anon
/login.do = anon
/** = authc
</value>
</property>
</bean>
<!-- 引用自定义的realm -->
<bean id="saasRealm" class="com.itheima.web.realm.SaasRealm"/>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="saasRealm"/>
</bean>
<!--下面所有内容为 shiro注解使用-->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
</bean>
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
4. 自定义Realm
在export_manager_web的com.itheima.web.realm
包创建类SaasRealm
package com.itheima.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 SaasRealm extends AuthorizingRealm {
/**
* @description 用户授权
* @author mryhl
* @date 2020/10/11 16:15 No such property: code for class: Script1
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("=====================授权=====================");
return null;
}
/**
* @description 用户认证
* @author mryhl
* @date 2020/10/11 16:14 No such property: code for class: Script1
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("=====================认证=====================");
return null;
}
}
5. shiro执行流程的说明
第三章 Shiro用户认证(重点)
1. 传统方式和shiro方式完成登录的流程区别
2. 代码实现
2.1 修改LoginController
/**
* @description 用户登陆认证
* @author mryhl
* @date 2020/10/11 16:25 No such property: code for class: Script1
* @return
*/
@RequestMapping("/login")
public String login(String email,String password) {
// 1. 封装email和password为Token
AuthenticationToken authenticationToken = new UsernamePasswordToken(email, new Md5Hash(password, email, 2).toString());
// 2. 获取subject,并且调用login方法
// 通过SecurityUtils获取subject
Subject subject = SecurityUtils.getSubject();
/**
* @author mryhl
* 调用login方法, 传入token.
* 并对此进行捕获异常
*/
try {
subject.login(authenticationToken);
// 登陆成功
User user = (User) subject.getPrincipal();
// 通过则保存用户数据
session.setAttribute("loginUser",user);
//根据用户查询对应的权限
List<Module> moduleList=userService.findModuleByUser(user);
session.setAttribute("modules", moduleList);
return "redirect:/home/main.do";
} catch (Exception e) {
request.setAttribute("error", "用户名或者密码错误");
return "forward:/login.jsp";
}
}
2.2 修改SaasReam
/**
* @description 用户认证
* @author mryhl
* @date 2020/10/11 16:56
* @return AuthenticationInfo 的实现类 SimpleAuthenticationInfo 简单认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 进入方法标识
System.out.println("=====================认证=====================");
// 强制类型转换
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
// 获取传入的email
String email = usernamePasswordToken.getUsername();
// 根据用户进行查询
User user = userService.findByEmail(email);
// 判断返回信息
if (user==null) {
// 返回空的简单身份认证信息,代表没有找到内容
return new SimpleAuthenticationInfo();
}else {
/**
* @author mryhl
* SimpleAuthenticationInfo三个参数
* Object principal 主角---->user
* Object credentials 密码--->user.getPassword()
* String realmName 当前realm的名称--->this.getName()
*/
return new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
}
}
3. 再谈shiro认证代码的执行流程
4. 用户退出
//退出
@RequestMapping(value = "/logout", name = "用户登出")
public String logout() {
SecurityUtils.getSubject().logout(); //登出
return "redirect:/login.jsp";
}
第四章 Shiro用户授权(重点)
1. 授权流程说明
shiro授权的意思就是: 当一个用户试图访问一个资源的时候, shiro会判断此用户是否有访问此资源的权限
shiro授权的步骤:
- 定义每个资源访问所需要的权限
- 当用户访问资源的时候, 要去数据库查询到此用户拥有的权限
- 使用用户查询到的权限列表 跟 访问资源需要的权限比对, 决定授权是否通过
2. 授权代码实现(xml版)
2.1 xml中定义权限
applicationContext-shiro.xml
<!--
URL=perms["权限标识"]
当前访问的用户只有 有权限标识的权限的时候,才能访问URL对应的资源
注意位置必须在/**的上面
-->
/company/list.do = perms["企业管理"]
2.2 SaasRealm中实现授权逻辑
/**
* @description 用户授权
* @author mryhl
* @date 2020/10/11 16:15
* @return simpleAuthorizationInfo 简单授权信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("=====================授权=====================");
// 查询当前用户的权限信息
User user = (User) principalCollection.getPrimaryPrincipal();
List<Module> moduleList = userService.findModuleByUser(user);
// 将信息传递到Shiro
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Module module : moduleList) {
simpleAuthorizationInfo.addStringPermission(module.getName());
}
System.out.println(simpleAuthorizationInfo);
return simpleAuthorizationInfo;
}
3. 授权代码实现(注解版)
3.1 去掉xml中的注解配置
spring/applicationContext-shiro.xml
<!-- filter-name这个名字的值来自于web.xml中filter的名字 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!--认证不通过, 跳转的页面 -->
<property name="loginUrl" value="/login.jsp"></property>
<!--授权不通过, 跳转的页面 -->
<property name="unauthorizedUrl" value="/unauthorized.jsp"></property>
<!--拦截规则,注意拦截的顺序.次规则从上向下执行-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/css/** = anon
/img/** = anon
/plugins/** = anon
/make/** = anon
/login.do = anon
<!--
URL=perms["权限标识"]
当前访问的用户只有 有权限标识的权限的时候,才能访问URL对应的资源
注意位置必须在/**的上面
-->
<!--/company/list.do = perms["企业管理"]-->
/** = authc
</value>
</property>
</bean>
3.2 使用注解声明资源访问权限
CompanyController.java
/**
* 查看列表
* name字段主要用于打印日志
*/
//@RequiresPermissions("企业管理") 代表只有用户有企业管理的权限,才能访问当前方法
//相当于XML中的 /company/list.do = perms["企业管理"]
@RequiresPermissions("企业管理")
@RequestMapping(value = "/list",name = "企业列表查询")
public String list(
@RequestParam(defaultValue = "1", name = "page") Integer pageNum,
@RequestParam(defaultValue = "1") Integer pageSize
){
PageInfo pageInfo = companyService.findByPage(pageNum, pageSize);
request.setAttribute("page", pageInfo);
return "company/company-list";
}
3.3 全局异常处理修改
基于注解的授权控制, 当授权失败的时候, 不会跳转授权失败页面, 而是抛出一个异常, 需要我们在全局异常处理器中自己处理
package com.itheima.web.handlers;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
@ControllerAdvice
public class CommonExceptionHandler {
//处理所有的Exception及其子类
@ExceptionHandler(Exception.class)
public String exceptionHandler(Exception e, HttpServletRequest request) {
//1. 打印异常,给程序员
e.printStackTrace();
request.setAttribute("errorMsg", e.getMessage());
//2. 返回页面, 给用户
return "error";
}
//处理未授权的异常
@ExceptionHandler(UnauthorizedException.class)
public String UnauthorizedException(Exception e, HttpServletRequest request) {
//2. 返回未授权页面, 给用户
return "redirect:/unauthorized.jsp";
}
}
4. 页面元素权限控制
4.1 说明
当用户没有相关资源的具体操作权限的时候,我们应该是不让其看到响应按钮的
这就要使用到了shiro标签, 这里主要介绍一个<shiro:hasPermission>
, 用法如下:
<!--这代表用户有 删除部门 的权限, 才能看到 删除 按钮-->
<shiro:hasPermission name="删除部门">
<button>删除</button>
</shiro:hasPermission>
4.2 代码
以部门管理为例子, 演示效果:
1 引入shiro标签库
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags"%>
2 使用shiro标签控制权限
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ include file="../../base.jsp" %>
<!DOCTYPE html>
<html>
<head>
<!-- 页面meta -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>数据 - AdminLTE2定制版</title>
<meta name="description" content="AdminLTE2定制版">
<meta name="keywords" content="AdminLTE2定制版">
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
</head>
<script>
function deleteById() {
var id = getCheckId()
if (id) {
if (confirm("你确认要删除此条记录吗?")) {
location.href = "/system/dept/delete.do?id=" + id;
}
} else {
alert("请勾选待处理的记录,且每次只能勾选一个")
}
}
</script>
<body>
<div id="frameContent" class="content-wrapper" style="margin-left:0px;">
<section class="content-header">
<h1>
系统管理
<small>部门管理</small>
</h1>
<ol class="breadcrumb">
<li><a href="all-admin-index.html"><i class="fa fa-dashboard"></i> 首页</a></li>
</ol>
</section>
<!-- 内容头部 /-->
<!-- 正文区域 -->
<section class="content">
<!-- .box-body -->
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">部门列表</h3>
</div>
<div class="box-body">
<!-- 数据表格 -->
<div class="table-box">
<!--工具栏-->
<div class="pull-left">
<div class="form-group form-inline">
<div class="btn-group">
<shiro:hasPermission name="新增部门">
<button type="button" class="btn btn-default" title="新建"
onclick='location.href="/system/dept/toAdd.do"'><i
class="fa fa-file-o"></i> 新建
</button>
</shiro:hasPermission>
<shiro:hasPermission name="删除部门">
<button type="button" class="btn btn-default" title="删除"
onclick='deleteById()'><i class="fa fa-trash-o"></i> 删除
</button>
</shiro:hasPermission>
<button type="button" class="btn btn-default" title="刷新"
onclick="window.location.reload();"><i class="fa fa-refresh"></i> 刷新
</button>
</div>
</div>
</div>
<div class="box-tools pull-right">
<div class="has-feedback">
<input type="text" class="form-control input-sm" placeholder="搜索">
<span class="glyphicon glyphicon-search form-control-feedback"></span>
</div>
</div>
<!--工具栏/-->
<!--数据列表-->
<table id="dataList" class="table table-bordered table-striped table-hover dataTable">
<thead>
<tr>
<th class="" style="padding-right:0px;">
<input type="checkbox" name="selid" onclick="checkAll('id',this)">
</th>
<th class="sorting">序号</th>
<th class="sorting">编号</th>
<th class="sorting">上级</th>
<th class="sorting">名称</th>
<th class="text-center">操作</th>
</tr>
</thead>
<tbody>
<c:forEach items="${page.list}" var="dept" varStatus="st">
<tr>
<td><input type="checkbox" name="id" value="${dept.id }"/></td>
<td>${st.count }</td>
<td>${dept.id }</td>
<td>${dept.parent.deptName }</td>
<td><a href="/system/dept/toUpdate.do?id=${dept.id }">${dept.deptName }</a></td>
<th class="text-center">
<shiro:hasPermission name="编辑部门">
<button type="button" class="btn bg-olive btn-xs"
onclick='location.href="/system/dept/toUpdate.do?id=${dept.id}"'>
编辑
</button>
</shiro:hasPermission>
</th>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
<div class="box-footer">
<jsp:include page="../../common/page.jsp">
<jsp:param value="${ctx}/system/dept/list.do" name="pageUrl"/>
</jsp:include>
</div>
</div>
</section>
</div>
</body>
</html>
5. 授权数据缓存
我们现在访问被拦截的页面时,每次shiro的安全过滤器都需要从realm中获取认证方法,也意味这每次查询数据库,浪费服务性能,造成访问压力,这时候我们必须要进行优化:经常访问,但又不经常修改的这部分数据可以使用缓存
spring/applicationContext-shiro.xml
<!--缓存管理器-->
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="saasRealm"/>
<property name="cacheManager" ref="cacheManager"></property>
</bean>
第五章 自定义Shiro过滤器(高级)
1. 需求明确
shiro 支持如下的权限写法, 代表同时拥有多个权限, 才能访问指定资源
/system/dept/edit.do = perms["新增部门","删除部门"]
但是我们现在有这样一个需求: 只需要满足其中一个个权限, 就要能对资源访问, 这时怎么办呢?
2. 自定义过滤器
操作步骤:
- 自定义过滤器, 继承
AuthorizationFilter
, 并实现里面的方法 - 配置文件中添加配置
自定义过滤器
package com.itheima.web.filters;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
public class MyPermissionsAuthorizationFilter extends AuthorizationFilter {
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response); //subject里面就有当前用户的权限 ["查看部门","新增部门","修改部门"]
String[] perms = (String[]) mappedValue; //访问资源需要的权限 ["新增部门","删除部门"]
//如果没有配置,放行
if (perms == null || perms.length == 0) {
return true;
}
for (String perm : perms) {
if (subject.isPermitted(perm)) {
return true;
}
}
return false;
}
}
修改配置文件
<!--拦截规则,注意拦截的顺序.次规则从上向下执行-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/css/** = anon
/img/** = anon
/plugins/** = anon
/make/** = anon
/login.do = anon
<!--
URL=perms["权限标识"]
当前访问的用户只有 有权限标识的权限的时候,才能访问URL对应的资源
注意位置必须在/**的上面
-->
<!--/company/list.do = perms["企业管理"]-->
/company/list.do = myPerms["新增部门","删除部门"]
/** = authc
</value>
</property>
<!-- 将自定义的过滤器加入到Shiro的过滤器链中-->
<property name="filters">
<map>
<!--key就是当前过滤器的一个唯一标识,类似于anon authc perms-->
<entry key="myPerms" value-ref="myPermissionsAuthorizationFilter" />
</map>
</property>
</bean>
<!--将自定义的过滤器声明到Spring中-->
<bean id="myPermissionsAuthorizationFilter" class="com.itheima.web.filters.MyPermissionsAuthorizationFilter"></bean>