`
TableMiao
  • 浏览: 73614 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

shiro权限管理(一)

阅读更多

 

shiro权限管理(一)

          本篇文章主要记录第一次接触shiro,权限细粒度到按钮级别,很多地方也生硬,欢迎圈错,指点,共同学习shiro。本篇主要以前后台结合为主采用(spring+mybatis+springMVC  融合了缓存memcached,可以先忽略,本人主要做后台springMVC也是刚刚了解,勉强凑合用,用的不好的地方,多多圈评,共同学习)后续会结合开涛的第二十章(无状态web集成应用)把权限控制在rest接口端。

 

一.简介

        关于基本介绍网上很多,主要推荐开涛的《跟我学shiro》讲的非常详细(文末附上参考链接),本篇主要记录使用。

         Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。

        Authentication身份认证/登录,验证用户是不是拥有相应的身份;

        Authorization授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

        Session Manager会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

        Cryptography加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

        Web SupportWeb支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

       Concurrencyshiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

       Testing提供测试支持;

       Run As允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

       Remember Me记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

       记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。

 

   二.项目配置

          1.数据表的关系

                5个表(用户表,角色表,权限表,用户角色关系表,角色权限关系表)

               可能有的还分功能点表,功能点权限关系表等等(五个表出来之后  这个可以尝试)。

          2.项目整体的一个结构图

          3.配置spring和mybatis的一些基本信息,主要写在base.xml里面,主要是配置数据源,配置读取application.properties的信息

,集成mybatis,得到一个java类中dao层可用的jdbcTemplate,sqlSessionTemplate实例用来对数据库数据的增删改查操作。代码略……到这一步和shiro还是没有关系,可以test一下看是否能查询数据。

           4.配置spring-mvc.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:p="http://www.springframework.org/schema/p" 
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
	http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://www.springframework.org/schema/mvc
	http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

	<!-- 自动扫描controller包下的所有类,使其认为spring mvc的控制器 -->
	<context:component-scan base-package="com.sss.controller" />
	<bean id="mappingJacksonHttpMessageConverter"
		class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
		<property name="supportedMediaTypes">
			<list>
				<value>text/html;charset=UTF-8</value>
			</list>
		</property>
	</bean>
	<!-- 启动Spring MVC的注解功能,完成请求和注解POJO的映射 -->
	<bean
		class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
		<property name="messageConverters">
			<list>
				<ref bean="mappingJacksonHttpMessageConverter" /><!-- json转换器 -->
			</list>
		</property>
	</bean>
	<!-- HandlerAdapter 表示所有实现了org.springframework.web.servlet.mvc.Controller接口的Bean可以作为Spring 
		Web MVC中的处理器。如果需要其他类型的处理器可以通过实现HadlerAdapter来解决 -->
	<bean
		class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />	
	<!-- 对模型视图名称的解析,即在模型视图名称添加前后缀 -->
	<bean
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="viewClass"
			value="org.springframework.web.servlet.view.JstlView" />
		<property name="prefix" value="/page/" />
		<property name="suffix" value=".jsp" />
	</bean>
</beans>
 此处在web.xml中需要配上一段信息(附上整个web.xml)

 

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 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">
	<display-name>sss</display-name>
	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>
	
	<!-- 设置日志类型,可以直接加载logback ,配法很多-->
	<context-param>
		<param-name>resteasy.logger.type</param-name>
		<param-value>SLF4J</param-value>
	</context-param>
	
	<!-- 加载 applicationContext.xml-->
   <context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			classpath:applicationContext.xml
		</param-value>
	</context-param> 
	
	<!-- 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>*.do</url-pattern>  
    </filter-mapping>  
    <filter-mapping>  
        <filter-name>shiroFilter</filter-name>  
        <url-pattern>*.jsp</url-pattern>  
    </filter-mapping>  
	
	
	<!-- spring 监听 -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- springMvc配置 -->
	<servlet>
		<servlet-name>springMVC</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		 <init-param>
			<description>spring mvc 配置文件</description>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:spring-mvc.xml</param-value>
		</init-param> 
		 <!-- 启动容器时初始化该Servlet -->
		<load-on-startup>1</load-on-startup> 
	</servlet>

	<servlet-mapping>
		<servlet-name>springMVC</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>
</web-app>
运行到这一步我们就可以测一下springMVC是否搭成功,在controller测试一下

 

package com.sss.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class ShowController {

	@RequestMapping("show")
	public ModelAndView showContent() {
		// 1、收集参数、验证参数
		// 2、绑定参数到命令对象
		// 3、将命令对象传入业务对象进行业务处理
		// 4、选择下一个页面
		ModelAndView mv = new ModelAndView();
		// 添加模型数据 可以是任意的POJO对象
		mv.addObject("message", "框架测试!");
		// 设置逻辑视图名,视图解析器会根据该名字解析到具体的视图页面
		mv.setViewName("one");
		return mv;
	}

}
      5.配置shiro

 

            5.1在web.xml配置shiro的过滤器(详见4)       

            5.2 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:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd   
    http://www.springframework.org/schema/util 
    http://www.springframework.org/schema/util/spring-util-3.0.xsd">
	<description>Shiro 配置</description>
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<property name="securityManager" ref="securityManager" />
		<property name="loginUrl" value="/page/login.jsp" />
		<property name="filterChainDefinitions">
			<value>
				/ = authc
			</value>
		</property>
	</bean>
	
	<!-- 把realm 交给 securityManager 管理-->
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<!--设置自定义realm -->
		<property name="realm" ref="loginRealm" />
		<property name="cacheManager" ref="shiroCacheManager" />
	</bean>

	<!--自定义Realm 继承自AuthorizingRealm -->
	<bean id="loginRealm" class="com.sss.realm.LoginRealmNew" />
	
	<!-- 相当于调用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>
	<!-- Shiro生命周期处理器-->  
	<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
	
	<!-- 缓存管理器 -->
	<bean id="shiroCacheManager"  class="org.apache.shiro.cache.MemoryConstrainedCacheManager"   />

</beans>
              
/ = authc表示所有url都需要经过shiro的认证

 

 

<property name="loginUrl" value="/page/login.jsp" />  登录页面不拦截

   此处整个配置里面有个自定义realm。这个realm主要有两个方法,一个是认证,另一个就是授权。文末附上整个项目的代码需要的可以对着看。

   

 

三.流程与思路(代码只是片段,详见附件源码)

       1.首先用户登录,从界面获取用户名和密码,且把密码加密,再通过用户名到数据库中查询该用户是否存在,如果存在,把登录的标志位置为true,不存在就直接return到登录界面且给提示。(详见LoginController)

           1.1密码加密

package com.sss.comm;

import org.apache.shiro.crypto.hash.Md5Hash;

public class EncryptUtils {
	public static final String encryptMD5(String source) {
		if (source == null) {
			source = "";
		}
		Md5Hash md5 = new Md5Hash(source);
		return md5.toString();
	}
}

       1.2登录方法

@RequestMapping(params = "login")
	public ModelAndView login(HttpServletRequest request) {

		boolean isLogin = false;
		String name = request.getParameter("name").toString();
		String pwd = request.getParameter("pwd").toString();
		ModelAndView mv = new ModelAndView();
		// 密码加密
		String encryptPwd = EncryptUtils.encryptMD5(pwd);
		// 通过用户名查询密码
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("name", name);
		List<Map<String, Object>> userList = userDao.selectAll(map);
		if (userList.size() > 0) {
			mv.addObject("name", name);
			isLogin = true;
		} else {
			mv.addObject("msg", "用户名或密码不正确");
			mv.setViewName("/login");
		}
     2.为真之后通过Subject currentUser = SecurityUtils.getSubject(); 获取当前用户,并由UsernamePasswordToken token = new UsernamePasswordToken(name,encryptPwd);把用户名和密码传递到shiro的过滤器,通过shiro提供的currentUser.login(token);跳转到自定义的realm进行认证。如果没有error说明认证成功,即登录成功,否者返回错误信息return到登录页面或同一的出错页面。

 

 

	if (isLogin) {
			Subject currentUser = SecurityUtils.getSubject();
			UsernamePasswordToken token = new UsernamePasswordToken(name,
					encryptPwd);
			// token.setRememberMe(true);只记住当前会话id,可用于购物车功能,其余没什么用
			try {
				currentUser.login(token);
				// 判断是否登录
				if (currentUser.isAuthenticated()) {
					mv.setViewName("index");
				} else {
					mv.addObject("msg", "login errors");
					mv.setViewName("/login");
				}
			} catch (UnknownAccountException uae) {
				mv.addObject("msg", "用户名或密码错误");
				mv.setViewName("/login");
			} catch (IncorrectCredentialsException ice) {
				mv.addObject("msg", "用户名或密码错误");
				mv.setViewName("/login");
			} catch (LockedAccountException lae) {
				mv.addObject("msg", "用户已经被锁定不能登录,请与管理员联系!");
				mv.setViewName("/login");
			} catch (ExcessiveAttemptsException eae) {
				mv.addObject("msg", "错误次数过多!");
				mv.setViewName("/login");
			} catch (AuthenticationException ae) {
				mv.addObject("msg", "其他的登录错误!");
				mv.setViewName("/login");
			}
    3.登录之后进入到了主页(就好比你进入了一栋楼的大门,其中各个部门不知道是否有权限进去),此时触发导航栏或树形菜单操作,进入相应的controller

 

每触而发一次相当于调用一次接口就是一次url的请求。(详见ChenController)eg:

 

	@RequestMapping(params = "main")
	public ModelAndView main() {
		ModelAndView mv = new ModelAndView();
		Subject currentUser = SecurityUtils.getSubject();
		if (currentUser.isPermitted("chen.do?main")) {
			mv.setViewName("chen");
		} else {
			mv.addObject("error", "无权访问该页面");
			mv.setViewName("error");
		}
		return mv;
	}
 此时  通过
Subject currentUser = SecurityUtils.getSubject();得到当前用户

 

 

currentUser.isPermitted("chen.do?main")判断该用户是否有进入该地址的权限,此句相应realm中的授权方法。

 

 

@Override
	protected AuthorizationInfo doGetAuthorizationInfo(
			PrincipalCollection Principal) {

		logger.info("授权参数--------------:{}", Principal);

		String name = Principal.toString();
		logger.info("realm 授权中的名字-----:{}", name);
		AuthorizationInfo info = null;
		if (name != null) {
			info = allPermissions(name);
		}
		return info;
	}

	/***
	 * 根据用户名查出该用户的所有权限 绑定权限和角色
	 * 
	 * @param name
	 *            传入的登录用户名
	 * @return
	 */
	private AuthorizationInfo allPermissions(String name) {
		clearAllCachedAuthorizationInfo();
		boolean isCache = true;
		SimpleAuthorizationInfo info = null;
		try {
			info = (SimpleAuthorizationInfo) memcachedUtils.findCache(name
					+ "cachePermission");
			logger.info("缓存信息中的权限数据位:{}", info);
			if (info != null) {
				isCache = false;
			}
		} catch (Exception e1) {
			logger.info("读取缓存失败:{}", e1.getMessage());
		}
		if (isCache) {
			logger.info("------------------授权信息查询数据库");
			Map<String, Object> map = new HashMap<String, Object>();
			map.put("name", name);
			List<Map<String, Object>> allPermission = userDao
					.selectPermissionByName(map);
			if (allPermission.size() > 0) {
				info = new SimpleAuthorizationInfo();
				info.addRole(allPermission.get(0).get("role_name").toString());
				Set<String> permissions = new HashSet<>();
				for (Map<String, Object> m : allPermission) {
					permissions.add(dom4jReadXML(m.get("permission_name")
							.toString()));
				}
				info.setStringPermissions(permissions);
			}
			try {
				memcachedUtils.addCache(name + "cachePermission", info, 1);
			} catch (Exception e) {
				logger.info("调用缓存存储出错:{}", e.getMessage());
				e.printStackTrace();
			}
		}
		return info;
	}
    如果有权限进入 则进入,否则提示信息 或者 return到统一的出错页面。由于每次触发授权事件都会查询数据库,所以我加到了缓存里面,如果有直接读取缓存信息,否则查询数据库,重新加入到缓存里面。

 

     注:shiro的登录与授权  是两码事,并不是登录成功之后立马就拥有一个权限。登录和授权是分开的。触发授权方式很多,shiro开发手册上讲的很详细,还可以用注解等等方式……

     按钮级别的权限和上诉一致处理。

     4.由于我每次判断权限的时候都是要比对url,如果url改动,代码就会改动很大,所以引入配置文件(权限点与url的映射文件 permission-url-mapping.xml)

        权限表存取的就是权限路劲,xml文件就与之对应,一但权限点变化,只需要改permission-url-mapping.xml即可。

 

<?xml version="1.0" encoding="UTF-8"?>
<!-- 权限点与url的文件映射 -->
<root>
	<!--横向菜单控制页面  -->
	<user-miao>user.do?miao</user-miao>
	<user-chen>user.do?chen</user-chen>
	<user-quan>user.do?quan</user-quan>
	<user-admin>user.do?admin</user-admin>
	
	<!--chen控制页面  -->
	<chen-main>chen.do?main</chen-main>
	<chen-a>chen.do?a</chen-a>
	<chen-b>chen.do?b</chen-b>
	
	<!--miao控制页面  -->
	<miao-main>miao.do?main</miao-main>
	<miao-a>miao.do?a</miao-a>
	<miao-b>miao.do?b</miao-b>
	
	<!--quan控制页面  -->
	<quan-main>quan.do?main</quan-main>
	<quan-main-select>quan.do?main=click</quan-main-select>
	<quan-a>quan.do?a</quan-a>
	<quan-b>quan.do?b</quan-b>
	
	<!--登录、登出  -->
	<login-login>sss/login/login.do</login-login>
	<login-loginout>login.do?loginout</login-loginout>
	
</root>
 以xml文件存取,在判断权限的时候需解析该xml

 

 

/***
	 * 通过一个标签名获取xml中的值
	 * 
	 * @param xmlTag
	 * @return
	 */
	private String dom4jReadXML(String xmlTag) {
		logger.info("传入的节点名字:{}", xmlTag);
		SAXReader reader = new SAXReader();
		Document document = null;
		// 与文件路径无关。有这个xml就能读取
		String filePath = Thread.currentThread().getContextClassLoader()
				.getResource("permission-url-mapping.xml").getPath();
		File file = new File(filePath);
		try {
			// document = reader.read(new
			// File("classpath:permission-url-mapping.xml"));
			document = reader.read(file);
		} catch (DocumentException e) {
			logger.info("读取permission-url-mapping.xml文件失败原因", e.getMessage());
			e.printStackTrace();
		}
		Element root = document.getRootElement();
		String xmlVal = root.element(xmlTag).getTextTrim();
		return xmlVal;
	}
    这样每次
currentUser.isPermitted("chen.do?main")的时候就可以写死为currentUser.isPermitted("chen-main")  里面即为permission-url-mapping.xml的标签名

 

   5. 最后调用登出方法退出

/**
	 * 退出登录
	 * 
	 * @return
	 */
	@RequestMapping(params = "loginout")
	public ModelAndView logout() {
		Subject currentUser = SecurityUtils.getSubject();
		ModelAndView mv = new ModelAndView();
		try {
			currentUser.logout();
			mv.setViewName("login");
		} catch (AuthenticationException e) {
			e.printStackTrace();

		}
		return mv;
	}

 

 

   

   总结:

        1.本篇用到的shiro,只是很小的一部分,可能每个项目中用的都不一致。但是基本可以达到按钮级别的控制,效率未测试。

        2.不足之处,shiro的很多功能都没用到,比如说shiro里面的缓存,与rest接口的风格集成,可以直接把权限配置在shiro.xml文件中,等等……

 

 

 

参考:          http://jinnianshilongnian.iteye.com/blog/2018398

 

  • sss.rar (87.9 KB)
  • 下载次数: 21
分享到:
评论
2 楼 无痕海 2014-12-04  
先占位,MARK一下。
1 楼 yihengvip 2014-12-04  
前排沙发。

相关推荐

Global site tag (gtag.js) - Google Analytics