shiro细颗粒权限说白了就是url的判断,精确到增删改查. 本项目在ssm的基础上集成了shiro. web端可以参考 shiro教程 上面提到了shiro标签,可以用来前端的权限.
applicationContent-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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 声明一个密码匹配器 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 设置该密码匹配器使用的算法是 md5 -->
<property name="hashAlgorithmName" value="md5"/>
</bean>
<!-- 声明一个自定义的 Realm -->
<bean id="myRealm" class="cn.com.ecenter.system.realm.MyRealm">
<!-- 将上面声明的密码匹配器注入到自定义 Realm 的属性中去 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<!-- 将自定义的权限匹配器注入到自定义 Realm 中 -->
<property name="permissionResolver" ref="permissionResolver"/>
<!-- 配置缓存相关 -->
<!-- 启用缓存 -->
<property name="cachingEnabled" value="true"/>
<!-- 开启认证缓存-->
<property name="authenticationCachingEnabled" value="true"/>
<!-- 指定认证缓存的名字(与 ehcache.xml 中声明的相同) -->
<property name="authenticationCacheName" value="shiro-authenticationCache"/>
<!--开启授权缓存-->
<property name="authorizationCachingEnabled" value="true"/>
<!-- 指定授权缓存的名字(与 ehcache.xml 中声明的相同) -->
<property name="authorizationCacheName" value="shiro-authorizationCache"/>
</bean>
<!-- 自定义一个权限匹配器 -->
<bean id="permissionResolver" class="cn.com.ecenter.system.permission.UrlPermissionResolver"/>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!-- 如果认证不通过,浏览器通过 Get 方式请求到 /login 上 -->
<property name="loginUrl" value="/login"/>
<!-- override these for application-specific URLs if you like:
<property name="successUrl" value="/home.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/> -->
<!-- The 'filters' property is not necessary since any declared javax.servlet.Filter bean -->
<!-- defined will be automatically acquired and available via its beanName in chain -->
<!-- definitions, but you can perform instance overrides or name aliases here if you like: -->
<!-- <property name="filters">
<util:map>
<entry key="anAlias" value-ref="someFilter"/>
</util:map>
</property>-->
<property name="filterChainDefinitions">
<value>
/admin/**=authc,resourceCheckFilter
/login=anon
</value>
</property>
</bean>
<!-- 声明一个自定义的过滤器 -->
<bean id="resourceCheckFilter" class="cn.com.ecenter.system.filter.ResourceCheckFilter">
<!-- 为上面声明的自定义过滤器注入属性值 -->
<property name="errorUrl" value="/unAuthorization"/>
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- Single realm app. If you have multiple realms, use the 'realms' property instead. -->
<!-- 设置安全管理器的安全数据源为自定义的 Realm -->
<property name="realm" ref="myRealm"/>
<!-- By default the servlet container sessions will be used. Uncomment this line
to use shiro's native sessions (see the JavaDoc for more): -->
<!-- <property name="sessionMode" value="native"/> -->
<property name="cacheManager" ref="ehCacheManager"/>
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 配置 shiro 的 ehcache 缓存相关,这个缓存只和 Realm 相关 -->
<bean id="ehCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"></bean>
<!-- 配置 Spring 的 EhCacheManagerFactoryBean ,须要 spring-context-support 的支持 -->
<bean id="ehCacheManagerFactoryBean" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml"/>
</bean>
<!-- 配置 Spring 的 EhCacheCacheManager,须要 spring-context-support 的支持 -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehCacheManagerFactoryBean"/>
</bean>
</beans>
web.xml配置:
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!-- Spring -->
<!-- 配置Spring配置文件路径 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:applicationContext.xml
</param-value>
</context-param>
<!-- Spring -->
<!-- 配置Spring字符编码过滤器 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>xssFilter</filter-name>
<filter-class>ecenter.base.xss.XssFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>xssFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--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>
<!--连接池监控 -->
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>
<!-- WebStatFilter用于采集web-jdbc关联监控的数据 -->
<filter>
<filter-name>DruidWebStatFilter</filter-name>
<filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
<init-param>
<param-name>exclusions</param-name>
<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DruidWebStatFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 配置Spring上下文监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 防止Spring内存溢出监听器 -->
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
</listener>
<!-- Spring MVC 核心控制器 DispatcherServlet 配置 -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<!-- 拦截所有 的请求,交给DispatcherServlet处理,性能最好 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>initServlet</servlet-name>
<servlet-class>cn.com.ecenter.system.controller.InitServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- 首页 -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- 错误页 -->
<error-page>
<error-code>404</error-code>
<location>/commons/404.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/commons/500.jsp</location>
</error-page>
</web-app>
sql:
drop database ssm_shiro;
# 创建数据库 ssm_shiro
CREATE DATABASE ssm_shiro DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
# 使用数据库 ssm_shiro
USE ssm_shiro;
drop table if EXISTS t_user;
drop table if EXISTS t_role;
drop table if EXISTS t_user_role;
drop table if EXISTS t_resource;
drop table if EXISTS t_role_resource;
# 创建数据表 t_user
CREATE TABLE t_user(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '用户 ID',
username VARCHAR(30) NOT NULL comment '用户名',
`password` VARCHAR(32) NOT NULL comment '密码',
nickname VARCHAR(30) NOT NULL comment '昵称',
`status` TINYINT not null comment '状态:1 启用,2 禁用'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户信息表';
# 创建数据用于测试
INSERT INTO t_user(username,`password`,nickname,`status`)
VALUES('admin','a66abb5684c45962d887564f08346e8d','超级管理员',1);
INSERT INTO t_user(username,`password`,nickname,`status`)
VALUES('dev','c43812121e594f158520698ba706118f','开发工程师',1);
INSERT INTO t_user(username,`password`,nickname,`status`)
VALUES('test','47ec2dd791e31e2ef2076caf64ed9b3d','测试工程师',1);
INSERT INTO t_user(username,`password`,nickname,`status`)
VALUES('doc','5afd1e481507a2a181decc3860b32d15','文档工程师',1);
# 创建数据表 t_role
# name 字段用于显示给人看, sn 字段用在代码中做角色匹配
CREATE TABLE t_role(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '角色表 ID',
`name` VARCHAR(20) NOT NULL comment '角色名称',
sn VARCHAR(20) NOT NULL comment '角色字符串'
)engine=innodb auto_increment=1 DEFAULT charset=utf8 comment='角色信息表';
# 创建数据用于测试
INSERT INTO t_role(`name`,`sn`) VALUES('管理员','admin'),('开发工程师','dev'),('测试工程师','test'),('文档工程师','doc');
# 创建数据表 t_user_role
CREATE TABLE t_user_role(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '用户角色关联表 ID',
user_id TINYINT NOT NULL,
role_id TINYINT NOT NULL
)engine=innodb auto_increment=1 charset=utf8 comment='用户角色关联表';
# 创建数据用于测试
INSERT INTO `t_user_role`(`user_id`,`role_id`)
VALUES(1,1),(2,2),(3,3),(4,4);
# 创建资源表 t_resource
# 资源在本项目中的含义就是 "权限"
CREATE TABLE t_resource(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '资源 ID',
`name` VARCHAR(20) NOT NULL comment '资源名称,一般是中文名称(给人看的)',
permission VARCHAR(40) NOT NULL comment '资源权限字符串,一般是 Shiro 默认的通配符表示(给人看的)',
url VARCHAR(40) NOT NULL comment '资源 url 表示,我们设计的系统让 Shiro 通过这个路径字符串去匹配浏览器中显示的路径'
)engine=innodb auto_increment=1 charset=utf8 comment='资源表';
# 创建数据用于测试
INSERT INTO t_resource(`name`,permission,url)
VALUES('系统管理','admin:*','/admin/**'),
('用户管理','user:*','/admin/user/**'),
('用户添加','user:add','/admin/user/add'),
('用户删除','user:delete','/admin/user/delete'),
('用户修改','user:update','/admin/user/update'),
('用户查询','user:list','/admin/user/list'),
('用户资源查询','user:resources:*','/admin/user/resources/*'),
('角色管理','role:*','/admin/role/**'),
('角色添加','role:add','/admin/role/add'),
('角色删除','role:delete','/admin/role/delete'),
('角色修改','role:update','/admin/role/update'),
('角色查询','role:list','/admin/role/list'),
('角色资源查询','role:resources:*','/admin/role/resources/*'),
('资源管理','resource:*','/admin/resource/**'),
('资源增加','resource:add','/admin/resource/add'),
('资源删除','resource:delete','/admin/resource/delete'),
('资源修改','resource:update','/admin/resource/update'),
('资源查询','resource:list','/admin/resource/list');
# 创建角色资源关联表
CREATE TABLE t_role_resource(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '角色资源关联 ID',
role_id TINYINT not null comment '角色 id',
resource_id TINYINT not null comment '资源 id'
)engine=innodb auto_increment=1 charset=utf8 comment='角色资源关联表';
# 创建数据用于测试
INSERT INTO t_role_resource(role_id,resource_id)
VALUES(1,1),
(2,3),(2,5),(2,6),(2,7),(2,9),(2,11),(2,12),(2,13),(2,15),(2,17),(2,18),
(3,6),(3,7),(3,8),(3,14),
(4,6),(4,7),(4,12),(4,18);
自定义shirofilter:
package cn.com.ecenter.system.filter;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @program: baseFramework
* @description: 人间有味是清欢
* @author: liuSha.pufengjun
* @create: 2018-08-07 17:31
**/
public class ResourceCheckFilter extends AccessControlFilter {
private String errorUrl;
private static final Logger logger = LoggerFactory.getLogger(ResourceCheckFilter.class);
public String getErrorUrl() {
return errorUrl;
}
public void setErrorUrl(String errorUrl) {
this.errorUrl = errorUrl;
}
/**
* 表示是否允许访问 ,如果允许访问返回true,否则false;
* @param servletRequest
* @param servletResponse
* @param o 表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
* @return
* @throws Exception
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest,servletResponse);
String url = getPathWithinApplication(servletRequest);
logger.debug("当前用户正在访问的 url => " + url);
boolean flag = subject.isPermitted(url);
return subject.isPermitted(url);
}
/**
* onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回 true 表示需要继续处理;如果返回 false 表示该拦截器实例已经处理了,将直接返回即可。
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
logger.debug("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");
HttpServletRequest request =(HttpServletRequest) servletRequest;
HttpServletResponse response =(HttpServletResponse) servletResponse;
response.sendRedirect(request.getContextPath() + this.errorUrl);
// 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
return false;
}
}
realm自定义:
package cn.com.ecenter.system.realm;
import cn.com.ecenter.system.entity.SysResource;
import cn.com.ecenter.system.entity.SysUser;
import cn.com.ecenter.system.service.SysUserService;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* @program: baseFramework
* @description: 人间有味是清欢
* @author: liuSha.pufengjun
* @create: 2018-08-01 14:45
**/
public class MyRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(MyRealm.class);
@Autowired
private SysUserService userService;
/**
* 授权
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("myRealm doGetAuthorizationInfo");
//获取认证过的主体信息
SysUser user = (SysUser) principals.getPrimaryPrincipal();
Integer userId = user.getId();
List<SysResource> resourceList = userService.listAllResource(userId);
List<String> roleSnList = userService.listRoleSnByUser(userId);
List<String> resStringList = new ArrayList<String>();
for(SysResource sysResource: resourceList){
resStringList.add(sysResource.getUrl());
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(new HashSet<String>(roleSnList));
info.setStringPermissions(new HashSet<String>(resStringList));
// 以上完成了动态地对用户授权
logger.debug("role => " + roleSnList);
logger.debug("permission => " + resStringList);
return info;
}
/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("--- MyRealm doGetAuthenticationInfo ---");
String username = authenticationToken.getPrincipal().toString();
String password = new String((char[])authenticationToken.getCredentials());
// 以后我们使用 Spring 管理 Shiro 的时候,就不必要这样得到 UserService 了
// userService = (IUserService) InitServlet.getBean("userService");
// User user = userService.login(username,password);
// 这里应该使用 load 方法,比对用户名的密码的环节应该交给 Shiro 这个框架去完成
// 在测试调试的时候发现,这里还是应该使用 login 判断,因为登录不成功的原因有很多,
// 可以在登录的逻辑里面抛出各种异常
// 再到 subject.login(token) 里面去捕获对应的异常
// 显示不同的消息到页面上
SysUser user = userService.login(username,password);
if(user!=null){
// 第 1 个参数可以传一个实体对象,然后在认证的环节可以取出
// 第 2 个参数应该传递在数据库中“正确”的数据,然后和 token 中的数据进行匹配
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),getName());
// 设置盐值
info.setCredentialsSalt(ByteSource.Util.bytes(username.getBytes()));
return info;
}
return null;
}
@Override
protected void clearCachedAuthenticationInfo(PrincipalCollection principals) {
Cache c = getAuthenticationCache();
logger.info("清除【认证】缓存之前");
for(Object o : c.keys()){
logger.info( o + " , " + c.get(o));
}
super.clearCachedAuthenticationInfo(principals);
logger.info("调用父类清除【认证】缓存之后");
for(Object o : c.keys()){
logger.info( o + " , " + c.get(o));
}
// 添加下面的代码清空【认证】的缓存
SysUser user = (SysUser) principals.getPrimaryPrincipal();
SimplePrincipalCollection spc = new SimplePrincipalCollection(user.getUid(),getName());
super.clearCachedAuthenticationInfo(spc);
logger.info("添加了代码清除【认证】缓存之后");
int cacheSize = c.keys().size();
logger.info("【认证】缓存的大小:" + c.keys().size());
if (cacheSize == 0){
logger.info("说明【认证】缓存被清空了。");
}
}
@Override
protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
logger.info("清除【授权】缓存之前");
Cache c = getAuthorizationCache();
for(Object o : c.keys()){
logger.info( o + " , " + c.get(o));
}
super.clearCachedAuthorizationInfo(principals);
logger.info("清除【授权】缓存之后");
int cacheSize = c.keys().size();
logger.info("【授权】缓存的大小:" + cacheSize);
for(Object o : c.keys()){
logger.info( o + " , " + c.get(o));
}
if(cacheSize == 0){
logger.info("说明【授权】缓存被清空了。");
}
}
}
文中用的mysql,而自己开发用的sqlserver
其他dao层service层 controller层实现参照: