接上篇, 关于Shiro框架的学习(一) ,这篇会记录下Shiro整合Web、整合SSM的过程,之后就可以直接应用在项目的安全控制上。
类,这三个类在上一篇文章中已经提及,这里不再重复赘述。 - 数据库 沿用上一篇博文中未加密的数据库,数据库脚本上一篇已提及,同样不再重复贴代码了。 - jar包: 本次需要用到的jar包主要有如下几个
配置文件中指定了寻找DatabaseRealm的方法、指定了每个页面需要什么角色和权限、指定了如果没有权限将会跳转到哪个页面。
[main] #使用数据库进行验证和授权 databaseRealm=com.shiro.DatabaseRealm securityManager.realms=$databaseRealm #当访问需要验证的页面,但是又没有验证的情况下,跳转到login.jsp authc.loginUrl=/login.jsp #当访问需要角色的页面,但是又不拥有这个角色的情况下,跳转到noroles.jsp roles.unauthorizedUrl=/noRoles.jsp #当访问需要权限的页面,但是又不拥有这个权限的情况下,跳转到noperms.jsp perms.unauthorizedUrl=/noPerms.jsp #users,roles和perms都通过前面知识点的数据库配置了 [users] #urls用来指定哪些资源需要什么对应的授权才能使用 [urls] #doLogout地址就会进行退出行为 /doLogout=logout #login.jsp,noroles.jsp,noperms.jsp 可以匿名访问 /login.jsp=anon /noroles.jsp=anon /noperms.jsp=anon #阅读博客,需要登录后才可以查看 /readBlog.jsp=authc #新增博客不仅需要登录,而且要拥有 blogManager 角色才可以操作 /addBlog.jsp=authc,roles[blogManager] #删除博客,不仅需要登录,而且要拥有 deleteBlog 权限才可以操作 /deleteBlog.jsp=authc,perms["deleteBlog"] 复制代码
web.xml配置了加载shiro.ini的配置
在 <web-app>
中配置
<listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> <context-param> <param-name>shiroEnvironmentClass</param-name> <param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value> <!-- 默认先从/WEB-INF/shiro.ini,如果没有找classpath:shiro.ini --> </context-param> <context-param> <param-name>shiroConfigLocations</param-name> <param-value>classpath:shiro.ini</param-value> </context-param> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 复制代码
新建一个LoginServlet,负责控制登录验证。
@WebServlet(name = "loginServlet", urlPatterns = "/login")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String name = request.getParameter("name");
String password = request.getParameter("password");
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
try {
subject.login(token);
//通过subject获取session
Session session=subject.getSession();
session.setAttribute("subject", subject);
response.sendRedirect("");
}catch (AuthenticationException e) {
request.setAttribute("error", "验证失败");
request.getRequestDispatcher("login.jsp").forward(request, response);
}
}
}
复制代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<link rel="stylesheet" type="text/css" href="static/css/style.css" />
</head>
<body>
<div class="workingroom">
<div class="loginDiv">
<c:if test="${empty subject.principal}">
<a href="login.jsp">登录</a><br>
</c:if>
<c:if test="${!empty subject.principal}">
<span class="desc">你好,${subject.principal},</span>
<a href="doLogout">退出</a><br>
</c:if>
<a href="readBlog.jsp">查看博客</a><span class="desc">(登录后才可以查看) </span><br>
<a href="addBlog.jsp">新增博客</a><span class="desc">(要有博客管理员角色, Reader是读者,Object是博客管理员) </span><br>
<a href="deleteBlog.jsp">删除博客</a><span class="desc">(要有删除订单权限, Object有该权限) </span><br>
</div>
</body>
</html>
复制代码
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="java.util.*"%>
<!DOCTYPE html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="static/css/style.css" />
<div class="workingroom">
<div class="errorInfo">${error}</div>
<form action="login" method="post">
账号: <input type="text" name="name"> <br>
密码: <input type="password" name="password"> <br>
<br>
<input type="submit" value="登录">
<br>
<br>
<div>
<span class="desc">账号:Object 密码:123456 角色:blogManager</span><br>
<span class="desc">账号:Reader 密码:654321 角色:reader</span><br>
</div>
</form>
</div>
复制代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> readBlog.jsp ,能进来,就表示已经登录成功了 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div> 复制代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> addBlog.jsp,能进来<br>就表示拥有 blogManager 角色 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div> 复制代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> deleteBlog.jsp ,能进来,就表示有deleteBlog权限 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div> 复制代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> 角色不匹配 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div> 复制代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> 权限不足 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div> 复制代码
span.desc{
margin-left:20px;
color:gray;
}
div.workingroom{
margin:200px auto;
width:400px;
}
div.workingroom a{
display:inline-block;
margin-top:20px;
}
div.loginDiv{
text-align: left;
}
div.errorInfo{
color:red;
font-size:0.65em;
}
复制代码
打开tomcat服务器,在浏览器url输入:localhost:8080/ShiroWeb
登录Object
点击各功能:
登录Reader后点击各功能: 除了ReadBlog可以进以外,其余都失败
核心:
。
这种方式主要是不用在每个页面的映射上都加所需的权限,而是动态地将角色信息和权限信息写入数据库,再读取数据库,看页面是否需要拦截,访问页面需要什么权限等。
从零搭建首先先修改表结构
DROP DATABASE IF EXISTS shiro; CREATE DATABASE shiro DEFAULT CHARACTER SET utf8; USE shiro; drop table if exists user; drop table if exists role; drop table if exists permission; drop table if exists user_role; drop table if exists role_permission; create table user ( id bigint auto_increment, name varchar(100), password varchar(100), salt varchar(100), constraint pk_users primary key(id) ) charset=utf8 ENGINE=InnoDB; create table role ( id bigint auto_increment, name varchar(100), desc_ varchar(100), constraint pk_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create table permission ( id bigint auto_increment, name varchar(100), desc_ varchar(100), url varchar(100), constraint pk_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB; create table user_role ( id bigint auto_increment, uid bigint, rid bigint, constraint pk_users_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create table role_permission ( id bigint auto_increment, rid bigint, pid bigint, constraint pk_roles_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB; 复制代码
实际上上面这段代码就是对原来的表新增了几个字段,新增字段如下: permission: desc_ 、 url role: desc_
插入表数据:
INSERT INTO `permission` VALUES (1,'addblog','新增博客','/addBlog'); INSERT INTO `permission` VALUES (2,'readerBlog','阅读博客','/readBlog'); INSERT INTO `role` VALUES (1,'blogManager','博客管理员'); INSERT INTO `role` VALUES (2,'reader','读者'); INSERT INTO `role_permission` VALUES (1,1,1); INSERT INTO `role_permission` VALUES (2,2,2); INSERT INTO `user` VALUES (1,'Object','a7d59dfc5332749cb801f86a24f5f590','e5ykFiNwShfCXvBRPr3wXg=='); INSERT INTO `user` VALUES (2,'Reader','43e28304197b9216e45ab1ce8dac831b','jPz19y7arvYIGhuUjsb6sQ=='); INSERT INTO `user_role` VALUES (1,2,2); INSERT INTO `user_role` VALUES (2,1,1); 复制代码
配置文件web.xml在web.xml文件中要配置四块内容,分别是, 中文过滤器 、 Spring相关 、 MVC相关 、 Shiro过滤器相关 。 先说说shiro过滤器
<!-- Shiro-->
<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>
复制代码
shiro过滤器默认拦截所有请求
Spring相关:主要有两个,一个是Spring整合Mybatis,一个是Spring整合Shiro
<!-- spring -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml,
classpath:applicationContext-shiro.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
复制代码
MVC相关: 就是日常的 MVC配置
<!-- spring mvc -->
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- spring mvc-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springMVC.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
复制代码
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="databaseRealm" />
<property name="sessionManager" ref="sessionManager" />
</bean>
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean
class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod"
value="org.apache.shiro.SecurityUtils.setSecurityManager" />
<property name="arguments" ref="securityManager" />
</bean>
复制代码
2.HashedCredentialsMatcher:密码匹配器,可散列
<!-- 密码匹配器 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="2"/>
<property name="storedCredentialsHexEncoded" value="true"/>
</bean>
复制代码
3.LifecycleBeanPostProcessor:保证了Shiro内部lifecycle函数的执行
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> 复制代码
4.ShiroFilterFactoryBean:shiro的过滤器工厂类
<!-- url过滤器 -->
<bean id="urlPathMatchingFilter" class="com.shiro.filter.URLPathMatchingFilter"/>
<!-- 配置shiro的过滤器工厂类,id- shiroFilter要和我们在web.xml中配置的过滤器一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 调用我们配置的权限管理器 -->
<property name="securityManager" ref="securityManager" />
<!-- 配置我们的登录请求地址 -->
<property name="loginUrl" value="/login" />
<!-- 如果您请求的资源不再您的权限范围,则跳转到/403请求地址 -->
<property name="unauthorizedUrl" value="/unauthorized" />
<!-- 退出 -->
<property name="filters">
<util:map>
<entry key="logout" value-ref="logoutFilter" />
<entry key="url" value-ref="urlPathMatchingFilter" />
</util:map>
</property>
<!-- 权限配置 -->
<property name="filterChainDefinitions" ref="filterChainDefinitions"/>
</bean>
<!-- 退出过滤器 -->
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="/index" />
</bean>
复制代码
5.FilterChainDefinitions:配置可从数据库中读取页面权限的LinkedHashMap
<bean id="filterChainDefinitions" factory-bean="filterChainDefinitionsFactory" factory-method="buildFilterChainDefinitionMap"></bean> <bean id="filterChainDefinitionsFactory" class="com.shiro.entity.FilterChainDefinitions"> </bean> 复制代码
6.会话相关
<!-- 会话ID生成器 -->
<bean id="sessionIdGenerator"
class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" />
<!-- 会话Cookie模板 关闭浏览器立即失效 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid" />
<property name="httpOnly" value="true" />
<property name="maxAge" value="-1" />
</bean>
<!-- 会话DAO -->
<bean id="sessionDAO"
class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
<property name="sessionIdGenerator" ref="sessionIdGenerator" />
</bean>
<!-- 会话验证调度器,每30分钟执行一次验证 ,设定会话超时及保存 -->
<bean name="sessionValidationScheduler"
class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler">
<property name="interval" value="1800000" />
<property name="sessionManager" ref="sessionManager" />
</bean>
<!-- 会话管理器 -->
<bean id="sessionManager"
class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 全局会话超时时间(单位毫秒),默认30分钟 -->
<property name="globalSessionTimeout" value="1800000" />
<property name="deleteInvalidSessions" value="true" />
<property name="sessionValidationSchedulerEnabled" value="true" />
<property name="sessionValidationScheduler" ref="sessionValidationScheduler" />
<property name="sessionDAO" ref="sessionDAO" />
<property name="sessionIdCookieEnabled" value="true" />
<property name="sessionIdCookie" ref="sessionIdCookie" />
</bean>
复制代码
7.自定义Realm
<bean id="databaseRealm" class="com.shiro.service.impl.DatabaseRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
复制代码
public class DatabaseRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取用户名
String userName = (String) principalCollection.getPrimaryPrincipal();
//从数据库中获取角色和权限
Set<String> permissions = permissionService.getStringPermissionByName(userName);
Set<String> roles = roleService.listPermissionURLs(userName);
//建立简单授权对象
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
//设置权限和角色
s.setStringPermissions(permissions);
s.setRoles(roles);
//授权
return s;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken t = (UsernamePasswordToken) token;
String userName = token.getPrincipal().toString();
User user = userService.getUser(userName);
String passwordInDB = user.getPassword();
System.out.println(passwordInDB);
System.out.println(t.getPassword());
String salt = user.getSalt();
//做用户验证
SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt),
getName());
return a;
}
}
复制代码
Subject会将用户的信息交给上述Realm做验证和授权,做验证很好理解,登录即验证,但是要怎么判断一个url是否要做权限验证呢?
在PermissionService中有两个方法,分别是 needIntercepter
和 listPermissionURLs
,第一个方法的作用是判断一个url是否要验证,如果权限表中有这个url,则需要进行授权,如果没有则直接放行,listPermissionURLs是判断一个用户有权访问的所有url
@Override
public boolean needInterceptor(String requestURI) {
// TODO Auto-generated method stub
List<Permission> permissionList = permissionDao.listPermission();
for(Permission p : permissionList) {
if(p.getUrl().equals(requestURI)) {
return true;
}
}
return false;
}
@Override
public Set<String> listPermissionURLs(String userName) {
// TODO Auto-generated method stub
Set<String> result = new HashSet<>();
List<Permission> permissions = permissionDao.queryPermissionByUsername(userName);
for(Permission p : permissions) {
result.add(p.getUrl());
}
return result;
}
复制代码
于是,在过滤器和Realm中就可以调用这两个方法判断是否需要授权和进行授权了。 过滤器类:
public class URLPathMatchingFilter extends PathMatchingFilter {
@Autowired
PermissionService permissionService;
@Override
protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
String requestURI = getPathWithinApplication(request);
System.out.println("requestURI:" + requestURI);
Subject subject = SecurityUtils.getSubject();
// 如果没有登录,就跳转到登录页面
if (!subject.isAuthenticated()) {
WebUtils.issueRedirect(request, response, "/login");
return false;
}
// 看看这个路径权限里有没有维护,如果没有维护,一律放行(也可以改为一律不放行)
boolean needInterceptor = permissionService.needInterceptor(requestURI);
if (!needInterceptor) {
return true;
} else {
//如果有维护判断是否有权限访问(进入Realm进行授权)
if (subject.isPermitted(requestURI))
return true;
else {
UnauthorizedException ex = new UnauthorizedException("当前用户没有访问路径 " + requestURI + " 的权限");
subject.getSession().setAttribute("ex", ex);
WebUtils.issueRedirect(request, response, "/unauthorized");
return false;
}
}
}
}
复制代码
在运行最后结果之前,先明确一下数据表中用户的角色与权限。
所以运行结果如下: