1,系统中存在的权限问题
-
请求需要经过登录验证(登录成功才能访问)
-
某些请求需要经过权限验证(只有具备权限才能访问)
-
页面资源(按钮,超链接)需要根据权限动态展示
2,shiro的概述
2.1,概述
Apache Shiro是Java的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密的解决方案。
专业术语
-
认证(Authentication):认证(用户登录)
-
授权(Authorization) :授权(权限校验)
-
密码比较和加密
2.2 shiro中的内部结构
- subject:工具类,负责和shiro交互
- securityManager:shiro的核心,统一调度shiro框架运行
- realm域:和数据库交互
- 密码比较器:在认证时,对密码加密比较
2.3 shiro中的过滤器
shiro中所有的权限处理是通过过滤器的形式配置,内置了10个过滤器。只需要关注如下三个即可
这里的权限过滤器在配置文件中存入多个权限时需同时都满足才可,这时如果需要满足一个权限时就可以访问的话需要自定义过滤器。如:详情见最下面的优化。
3、搭建shiro的运行环境
3.1 引入依赖
<!--shiro-->
<!--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>
<!--ehcache-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
3.2 配置shiro和spring整合
web项目的resource/spring文件夹下创建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">
<description>Shiro与Spring整合</description>
<!--自定义的过滤器-->
<bean id="MyPermissionsFilter" class="cn.itcast.web.shiro.MyPermissionsFilter"></bean>
<!--redis配置-->
<bean id="redisManager" class="org.crazycake.shiro.RedisManager">
<property name="host" value="127.0.0.1:6379"></property>
</bean>
<!--定义redis的缓存管理器-->
<bean id="cacheManager" class="org.crazycake.shiro.RedisCacheManager">
<property name="redisManager" ref="redisManager"></property>
</bean>
<!-- 1、自定义Realm域的编写 -->
<bean id="authRealm" class="cn.itcast.web.shiro.AuthRealm">
<!-- 注入自定义的密码比较器 -->
<property name="credentialsMatcher" ref="customerCredentialsMatcher" ></property>
</bean>
<!--2、自定义的密码比较器 -->
<bean id="customerCredentialsMatcher" class="cn.itcast.web.shiro.CustomCredentialsMatcher"></bean>
<!--3、安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 引用自定义的realm -->
<property name="realm" ref="authRealm"/>
<!--缓存管理器-->
<property name="cacheManager" ref="cacheManager"></property>
</bean>
<!--4、 filter-name这个名字的值来自于web.xml中filter的名字
代理过滤器:代理shrio中的所有过滤器
一对10:在web.xml中只需要配置一个代理过滤器,自动的将请求绑定到多个shiro内部过滤器上
业主 和 中介
-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--指定过滤器的简称-->
<property name="filters">
<map>
<entry key="myPerms" value-ref="MyPermissionsFilter"></entry>
</map>
</property>
<property name="securityManager" ref="securityManager"/>
<!--登录页面 -->
<property name="loginUrl" value="/login.jsp"></property>
<!-- 鉴权失败后 -->
<property name="unauthorizedUrl" value="/unauthorized.jsp"></property>
<property name="filterChainDefinitions">
<!-- /**代表下面的多级目录也过滤 -->
<value>
/system/module/list.do = perms["模块管理"]
/index.jsp = anon
/login.jsp = anon
/login* = anon
/css/** = anon
/img/** = anon
/plugins/** = anon
/make/** = anon
/** = authc
</value>
</property>
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 安全管理器 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
3.3 在web.xml中配置代理过滤器
<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.4 自定义Realm域
package cn.itcast.web.shiro;
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;
/**
* 自定义realm域
* 1、继承父类 AuthorizingRealm
* 2、实现2个抽象方法(认证,授权)
*/
public class AuthRealm extends AuthorizingRealm {
/**
* 授权
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 认证
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
3.5 自定义密码比较器
package cn.itcast.web.shiro;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
/**
* 自定义密码比较器
* 1、继承父类 SimpleCredentialsMatcher
* 2、重写doCredentialsMatch方法(密码加密比较)
*/
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
//密码比较加密
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
return super.doCredentialsMatch(token, info);
}
}
4、基于shiro的用户认证
4.1 传统登录的比较
改造LoginController
通过shiro方式发起shiro调用
@RequestMapping(value = "/login",name = "用户登录")
public String login(String email,String password) {
//判断邮箱和密码是否为空
if (StringUtils.isEmpty(email) || StringUtils.isEmpty(password)){
request.setAttribute("error","用户名或密码不能为空");
return "forward:login.jsp";
}
try{
//获取工具类subject
Subject subject = SecurityUtils.getSubject();
// 在shiro中,将登录的用户名密码封装成功token对象
UsernamePasswordToken token = new UsernamePasswordToken(email,password);
//调用subject的login方法,进入shiro内部完成登录
subject.login(token);
//4.正常执行,登录成功(数据存入session,跳转主页)
//通过shiro框架获取当前登录的用户对象
User user = (User)subject.getPrincipal();//获取安全数据(用户对象)
session.setAttribute("loginUser",user);
List<Module> modules = moduleService.findByUser(user);
session.setAttribute("modules",modules);
return "home/main";
}catch (Exception e){
e.printStackTrace();
//抛出异常,登录失败
request.setAttribute("error","用户名或密码错误");
return "forward:login.jsp";
}
}
4.3 在Realm完成认证方法
找到自定义的AuthRealm域对象,补充认证方法
注意:这里需要调用UserService根据邮箱查询用户,需要注入UserService。
/**
* 认证: 用户登录
* * 提供数据(提供用户对象),调用service查询数据
* 参数:AuthenticationToken (用户登录输入的邮箱和密码)
* 返回值:AuthenticationInfo(认证数据)
* * 用户对象
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户输入的登录信息
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String email = upToken.getUsername();
//用户输入的密码用char数组存的,将char数组转化为string字符串
String password = new String(upToken.getPassword());
//根据邮箱查询用户
User user = userService.findByEmail(email);
//如果用户存在,构造返回值AuthenticationInfo
if (user != null){
/**
* AuthenticationInfo:认证数据
* 1.安全数据(用户对象)
* 2.密码:为了方便操作,存入数据库密码
* 3.realm域名称:约定俗称(当前类名)
*/
return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
}
//4.如果用户不存在,返回null(抛出异常)
return null;
}
4.4 在密码比较器中比较密码
找到密码比较器CustomCredentialsMatcher
,改造密码比较方法doCredentialsMatch
/**
* 密码比较方法
* 参数:
* token : 用户登录输入的用户名(邮箱)和密码
* info:realm域认证的返回值(用户对象,数据库密码,域名称)
* 返回值boolean:
* true:登录输入密码和数据库密码一致
* flase:不一致,自动的抛出异常
*/
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//1.获取输入的用户名(邮箱)密码
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String email = upToken.getUsername();
String password = new String(upToken.getPassword());
//2.获取数据库中的密码
String dbPassword = (String)info.getCredentials();
//3.对用户登录输入的密码加密
password = Encrypt.md5(password,email);
//4.比较
return dbPassword.equals(password);
}
4.5 退出清空登录数据
修改Logincontroller中ogout方法
//退出
@RequestMapping(value = "/logout",name="用户登出")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout(); //退出(清空登录数据)
return "forward:login.jsp";
}
4.6 修改web.xml的servlet配置
让tomcat启动的时候,创建spring容器,创建对象
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 指定加载的配置文件 ,通过参数contextConfigLocation加载-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/springmvc.xml;classpath*:spring/applicationContext-*.xml</param-value>
</init-param>
<!--当tomcat启动,初始化servlet,创建spring容器-->
<load-on-startup>1</load-on-startup>
</servlet>
4.7 登录校验的过程分析
- 用户在浏览器输入或者点击某个URL
- 通过web.xml中代理过滤器的设置找到spring和shiro整合的配置
- 找符合此链接的过滤器配置(/**=authc)
- authc:登录成功才能访问,内部会自动的判断当前请求是否已经登录
- 如果没有登录:跳转到执行的登录页面(loginUrl)配置
5、基于shiro的用户授权
5.1 完成Realm域的授权方法
授权方法:realm的授权方法主要目的是获取当前登录人员的可操作性权限数组(权限名称数组)
当realm域中准备好了权限数据,shiro框架会自动的根据权限配置(xml,注解),进行权限校验
修改了AuthRealm域中的授权方法,提供登录用户的权限数据
/**
* 授权方法:
* 本质:提供当前登录用户的权限数组(权限名称集合)
* 参数:PrincipalCollection(安全数据集合),只获取唯一的一个安全数据(用户对象)
* 返回值:
* AuthorizationInfo:授权数据(权限名称集合)
*
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//1.获取当前登录用户(安全数据)
User user = (User)principals.getPrimaryPrincipal();
//2.根据当前登录的用户,查询此用户的模块权限 List<Module>
List<Module> list = moduleService.findByUser(user);
//3.提取所有的模块名称:set集合
Set<String> modulsNames = new HashSet<>();
for (Module module : list) {
modulsNames.add(module.getName());
}
//4.构造返回值
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(modulsNames);
return info;
}
5.2 基于XML的权限配置
在 3.2 配置shiro和spring整合中的配置文件中已经配置,如图所示:
5.3 基于注解的权限配置
- 注解配置到请求URL对应的控制器方法上
- 通过注解的形式替代xml的配置
- @RequiresPermissions注解,注解的value属性中执行所需权限
- 使用注解形式配置权限,如果权限不足抛出异常
需求:当抛出UnauthorizedException异常的时候,表示权限不足。跳转到一个权限不足页面
5.3.1 自定义异常处理器
package cn.itcast.web.controller.exception;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义的异常处理器
* 1、实现HandlerExceptionResolver
* 2、交给容器管理
*/
@Component
public class ExceptionHandler implements HandlerExceptionResolver {
/**
* 异常处理器方法
* 返回值:ModelAndView(跳转页面,携带错误信息)
* 参数: Exception 当前抛出的异常
*/
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
//当出现的异常时权限不足异常时:UnauthorizedException跳转到权限不足页面
ModelAndView mv = new ModelAndView();
if(ex instanceof UnauthorizedException){
ex.printStackTrace();
mv.addObject("errorMsg","你没有访问这个的权限");
mv.setViewName("forward:/unauthorized.jsp");
}else{
ex.printStackTrace();
mv.addObject("errorMsg", "王牌程序猿请求出战");
mv.setViewName("error");
}
return mv;
}
}
5.4 页面标签库控制页面资源
- 在需要控制的资源(按钮,超链接)页面上引入shiro标签库
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
- 通过shiro:hasPermission控制显示对应的按钮
<shiro:hasPermission name="删除部门">
<button type="button" ><i class="fa fa-trash-o"></i> 删除</button>
</shiro:hasPermission>
6、shiro优化
6.1 缓存优化
- 用户的权限数据一旦确定很少修改,为了缓解数据库压力。可以将用的数据(权限数据)保存到redis当中。
- 在shiro中redis缓存不需要编写代码,值需要配置即可
- 借助shiro中内置缓存管理器实现
在 3.2 配置shiro和spring整合中的配置文件中已经配置,如下这段代码:
<!--redis配置-->
<bean id="redisManager" class="org.crazycake.shiro.RedisManager">
<property name="host" value="127.0.0.1:6379"></property>
</bean>
<!--定义redis的缓存管理器-->
<bean id="cacheManager" class="org.crazycake.shiro.RedisCacheManager">
<property name="redisManager" ref="redisManager"></property>
</bean>
<!--安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 引用自定义的realm -->
<property name="realm" ref="authRealm"/>
<!--缓存管理器-->
<property name="cacheManager" ref="cacheManager"></property>
</bean>
6.2 自定义过滤器
说明:shiro中内置了10个过滤器。往往不能满足我们的要求。
需求:针对/company/list.do链接,只要满足[“企业管理”,“部门管理”]其中的一项就可以访问
自定义过滤器。
package cn.itcast.web.shiro;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* 自定义shiro过滤器。继承父类 AuthorizationFilter
*/
public class MyPermissionsFilter extends AuthorizationFilter {
/**
* 判断是否具有某种权限
* /company/** = perms["企业管理","部门管理"]
* mappedValue : 配置过滤器中,需要匹配的权限数组
* 返回值:
* true:具有权限
* false:没有权限
* 需求:对传入的权限数据,只需要满足其中一个权限,返回true即可
*/
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
//获取配置的filter的参数,数据转型获取数组
String[] perms = (String[])mappedValue;
//获取subject
Subject subject = getSubject(request, response);
//循环判断权限,满足其一即可返回true
if (perms != null && perms.length>0){
for (String perm : perms) {
if (subject.isPermitted(perm)){
return true;
}
}
return false;
}else {
return true;
}
}
}
再在web项目的resource/spring文件夹下的applicationContext-shiro.xml配置文件中配置
<!--自定义过滤器交给spring容器管理-->
<bean id="myPermissionsFilter" class="cn.itcast.web.shiro.MyPermissionsFilter"></bean>
<!-- filter-name这个名字的值来自于web.xml中filter的名字
代理过滤器:代理shrio中的所有过滤器
一对10:在web.xml中只需要配置一个代理过滤器,自动的将请求绑定到多个shiro内部过滤器上
业主 和 中介
-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--指定过滤器的简称-->
<property name="filters">
<map>
<entry key="myPerms" value-ref="myPermissionsFilter"></entry>
</map>
</property>
<property name="securityManager" ref="securityManager"/>
<!--登录页面 -->
<property name="loginUrl" value="/login.jsp"></property>
<!-- 没有操作权限的时候,跳转的页面 -->
<property name="unauthorizedUrl" value="/unauthorized.jsp"></property>
<property name="filterChainDefinitions">
<!-- /**代表下面的多级目录也过滤 -->
<value>
<!--URL = perms["所需要权限的名称"]
与的关系:多个权限同时满足才能访问
-->
<!-- /company/** = perms["企业管理","部门管理"]-->
<!--配置过滤器-->
<!--/company/** = myPerms["企业管理","部门管理"]-->
/index.jsp = anon
/login.jsp = anon
/login* = anon
/css/** = anon
/img/** = anon
/plugins/** = anon
/make/** = anon
/** = authc
</value>
</property>
</bean>