Shiro简介以及与Spring集成Demo

一:Shiro工作流程

1.指定配置文件,配置文件中指定authenticator(认证)类型。初始化生成securityManager,初始化securityManager中的authenticator(认证)和realms(源)。securityManager存储为全局变量。
2.创建或获取subject(用于代表当前用户的实体),线程私有变量,存储于threadlocal上。
3.subject调用login(UsernamePasswordToken)方法,用于模拟用户登录,UsernamePasswordToken代表用户名和密码的抽象。
4.委派给securityManager处理。
5.securityManager委派给初始化时指定的authenticator(认证)处理。
6.authenticator循环realms,调用realm中的doGetAuthenticationInfo(用于身份验证)进行身份认证。可继承realm,
重写doGetAuthenticationInfo方法,在其中编写身份认证的业务逻辑。验证失败需抛异常。
7.若需判断用户的角色或权限,调用subject(代表当前用户的实体)的hasroles等方法。

二、Shiro简介

Apache Shiro是Java的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。

实际上,Shiro的主要功能是管理应用程序中与安全相关的全部,同时尽可能支持多种实现方法。Shiro是建立在完善的接口驱动设计和面向对象原则之上的,支持各种自定义行为。Shiro提供的默认实现,使其能完成与其他安全框架同样的功能,这不也是我们一直努力想要得到的吗!

Apache Shiro相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。

三、Shiro可以做什么

● 验证用户身份
● 用户访问控制,比如用户是否被赋予了某个角色;是否允许访问某些资源
● 在任何环境都可以使用Session API,即使不是WEB项目或没有EJB容器
● 事件响应(在身份验证,访问控制期间,或是session生命周期中)
● 集成多种用户信息数据源
● SSO-单点登陆
● Remember Me,记住我
● Shiro尝试在任何应用环境下实现这些功能,而不依赖其他框架、容器或应用服务器。

四、主要架构阅览
在这里插入图片描述
Subject:主体,既可以代表用户,也可以代表程序(网络爬虫等),它需要访问系统,系统则需要对其进行认证和授权,可以看到主体可以是任何可以与应用交互的“用户”。

SecurityManager: 安全管理,用户请求Url,对应于一个Subject对象,由SecurityManager统一对Subject进行认证和授权(父)。

Authenricator: 认证器,主要对Subject进行认证,Subject的信息在shrio中是通过AuthenticationToken对象来储存,由AuthenricationStrategy进行验证管理.(子)。如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authorizer:授权器,Subject认证后,由它来对其授予对应角色权限.(子)即控制着用户能访问应用中的哪些功能;

SessionManager: Shiro的session管理方式,Shiro提供了一个专门管理session的方式,通常的web程序中的session是HttpSession的对象,是由web容器来管理的.如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);

SessionDao: session的接口,Shiro通过它来管理session数据,个性化的session数据储存需要使用sessionDao.

CacheManager: 缓存控制器,主要对session数据和授权数据进行缓存,减小数据库的访问压力.可以通过和ehcache的整合对缓存数据进行管理.

Pluggable Realms: 可扩展领域,相当于数据源,我们通过上面内容可以大致了解到Shiro的工作原理,但Shiro是怎样得知Subject的信息和数据库的信息是否匹配呢?Shiro这里就提供了一个realms的概念,它的作用就是得到数据库中的信息.这个realm是可以多个并且可以自定义,只需继承AuthorizingRealm这个接口就可以了.可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等,由用户提供,Shiro不知道你的用户/权限存储在哪及以何种格式存储,所以我们一般在应用中都需要实现自己的Realm密码模块

注意:对Subject进行认证和授权都需要调用realm,所以realm不仅仅相当于数据源,更加包含了认证和授权的一种逻辑.

Cryptography: 密码模块,一个密码管理工具,提供了一套加密/解密的组件.比如常用的散列,加/解密等功能,日常练习所使用的md5算法其实是一种散列算法,只能加密,不能解密.

五、Shiro认证流程

Shiro处理一个Subject流程图
在这里插入图片描述
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:

Subject主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm:域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个Shiro应用:

1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。

将shiro与spring进行简单的整合,需要以下步骤:
1.配置web.xml的filter
2.在spring里配置filter
3.在spring里配置SecurityManager
4.在spring里配置Realm

Demo地址如下:
SpringShiroDemo

举例说明
1、登录界面
在这里插入图片描述
login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="/WEB-INF/tlds/formCtrlsTag.tld" prefix="sot"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta name="decorator" content="none" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录</title>
<link rel="stylesheet" type="text/css"
	href="${pageContext.request.contextPath}/styles/bpmportal/login.css" />

<script type="text/javascript"
	src="${pageContext.request.contextPath}/scripts/jquery/jquery-1.11.1.min.js"></script>
<script type="text/javascript"
	src="${pageContext.request.contextPath}/scripts/console/login.js"></script>
<script type="text/javascript"
			src="${pageContext.request.contextPath}/scripts/layui/layui.js"></script>
	<script type="text/javascript"
			src="${pageContext.request.contextPath}/scripts/layui/layui.all.js"></script>
	<link rel="stylesheet" type="text/css"
		  href="${pageContext.request.contextPath}/scripts/layui/css/layui.css" />


<script  src="${pageContext.request.contextPath}/ibm_security_logout?logout=Logout"></script>
<script>
$(function(){
	if("zh-CN"==navigator.browserLanguage||"zh-CN"==navigator.language){
		$("#userName").text("用户名称");
		$("#passWord").text("用户密码");
	}else{
		$("#userName").text("Username");
		$("#passWord").text("Password");
	}
})
</script>	
</head>
<BODY>
		<form id="form1" action="${pageContext.request.contextPath}/console/user/login.action" method="post">
					<DIV id="loginbg">
			<DIV style="height: 280px;"></DIV>
			<TABLE id="logintable">
				<TBODY>
					<TR>
						<TH id="userName"></TH>
						<TD><INPUT name="username" class="txtbox" id="username"
							type="text" value=""></TD>
					</TR>
					<TR>
						<TH id="passWord"></TH>
						<TD><INPUT name="password" class="txtbox" id="password"
							value="" type="password" ></TD>
					</TR>
					<TR>
						<TD>&nbsp;</TD>
						<TD>
						<INPUT id="btnsubmit" type="submit" value="">
						<!-- <a id="btnf"  href="http://10.162.47.81:8876/" target="_blank" style="cursor: pointer;display: block;color: red;padding-top: 1%;font-size: 19px;">国内管理编制考勤打卡</a> -->
						<br/><br/>
							<span id="msg"><font color="red">${msg}</font></span>
						</TD>
					</TR> 
					
					<TR>
						<TD class="tipsmsg" colspan="2"></TD>
					</TR>
				</TBODY>
			</TABLE>
		</DIV>
	</FORM>
</BODY>

</html>

2、项目中用到的spring-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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:util="http://www.springframework.org/schema/util"
	xmlns:aop="http://www.springframework.org/schema/aop" 
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:tx="http://www.springframework.org/schema/tx" 
	xsi:schemaLocation="
	http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
	http://www.springframework.org/schema/context 
	http://www.springframework.org/schema/context/spring-context-4.1.xsd
	http://www.springframework.org/schema/aop
	http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
	http://www.springframework.org/schema/tx
	http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
	http://www.springframework.org/schema/util 
	http://www.springframework.org/schema/util/spring-util-4.1.xsd
	http://www.springframework.org/schema/mvc 
	http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd">
  <bean id="myRealm" class="com.gzsolartech.smartforms.shiro.MyRealm">  
  </bean>
  <bean id="datAppAuthorizationFileter" class="com.gzsolartech.smartforms.shiro.DatAppAuthorizationFilter"/>

  <bean id="documentAuthorizationFileter" class="com.gzsolartech.smartforms.shiro.DocumentAclFilter"/>
  <bean id="menuAuthorizationFilter" class="com.gzsolartech.smartforms.shiro.MenuAuthorizationFilter"/>
    
    
    <!-- Shiro安全管理器 -->  
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">  
	<property name="realm" ref="myRealm"></property>  
    </bean>  
 

	<!-- 配置shiro的过滤器工厂类,id- shiroFilter要和我们在web.xml中配置的过滤器一致 -->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<property name="securityManager" ref="securityManager" />
		<property name="loginUrl" value="/console/login.xsp" />
		<property name="successUrl" value="/bpmportal/home/index.xsp" />
		<property name="unauthorizedUrl" value="/console/login.xsp" />
		<property name="filters">
		     <util:map>
			<entry key="datAppAuthorizationFileter" value-ref="datAppAuthorizationFileter"/> 
			<entry key="documentAuthorizationFileter" value-ref="documentAuthorizationFileter"/> 
			<entry key="menuAuthorizationFilter" value-ref="menuAuthorizationFilter"/> 
		     </util:map>
		 </property> 
		 <!-- 自定义权限配置 -->
		 <property name="filterChainDefinitionMap" ref="chainDefinitionSectionMetaSource" />
	 </bean>  
	 <!--自定义filterChainDefinitionMap -->
	 <bean id="chainDefinitionSectionMetaSource" class="com.gzsolartech.smartforms.shiro.ChainDefinitionSectionMetaSource">
	     <property name="filterChainDefinitions">
		<value>
			/scripts/** = anon
			/styles/** = anon
			/images/** = anon
			/api/** = anon
			/console/login.xsp = anon
			/**=authc,menuAuthorizationFilter
		</value>
	      </property>
	 </bean>
	
    <!--   
       开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,  
       并在必要时进行安全逻辑验证  
    -->  
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
	<property name="securityManager" ref="securityManager"/>
    </bean>
</beans>  

3、用户管理控制层UserController

/**
* 用户管理控制层
* 
* @author wwd
*
*/
@Controller
@Scope("prototype")
@RequestMapping("/console")
public class UserController extends BaseWebController {
public static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
@Autowired
private OrgEmployeeService orgEmployeeService;
@Autowired
private AACEmployeeService aacEmployeeService;
@Autowired
private UserSync userSys;
@Autowired
private OrgEmployeeMenuItemServcie orgEmployeeMenuItemServcie;
@Autowired
private MenuItemService menuItemService;
@Autowired
private AACSSOService aACSSOService;
@Autowired
private BpmGlobalConfigService bpmGlobalConfigService;
@Autowired
private CommonMethodService commonMethodService;
@Autowired
private CookieLocaleResolver cookieLocaleResolver;
@Autowired
private SysUserProfileService userProfileService;
@Autowired
private SysMetadataService sysMetadataService;

/**
 * 
 * @Title: index
 * @Description: 登录视图界面
 * @param
 * @return
 * @return String 返回类型
 * @throws
 */
@RequestMapping("/login")
public String index() {
	writeLog(OperationType.LOGIN_ACTION_OPTR, "进入登录界面");
	return "/console/login";
}

/**
 * 登录
 * 
 * @param username
 *            账号
 * @param password
 *            密码
 * @param request
 * @return
 */
@RequestMapping(value = "/user/login", produces = "text/html; charset=utf-8")
public String login(String username, String password,
		HttpServletRequest request,String defaultPath,String code,String ltpatoken,String ltpSecret) {
	try {
		/**
		 * 退出bpm
		 */
		request.getSession().removeAttribute("adminLtpaToken2");
		request.getSession().removeAttribute("LtpaToken2");
		request.getSession().removeAttribute("smartThirdPartLoginCode");
		writeLog(OperationType.LOGIN_ACTION_OPTR, "用户登录,username="+ username + ",password=******");
		String ekpBpmClientkey=null;
		String ekpBpmClienToken=null;
	        Cookie[] cookies = request.getCookies();
		for (int i = 0; i < cookies.length; i++) {
		    Cookie cookie1 = cookies[i];
		    if ("ekpBpmClientkey".equals(cookie1.getName())) {
			ekpBpmClientkey=cookie1.getValue();
		    }
		    if ("ekpBpmClienToken".equals(cookie1.getName())) {
			ekpBpmClienToken=cookie1.getValue();
		    }
		}
		// code不为空时 登录认证
		if (StringUtils.isNotBlank(code)) {
			username=new String(Base64.getDecoder().decode(code.getBytes()));
			if (StringUtils.isBlank(username)){
				username="";
			}
			password=aACSSOService.getById(username.toLowerCase());
	        }else if (StringUtils.isNotBlank(ltpatoken)&&StringUtils.isNotBlank(ltpSecret)) {
			SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");// 设置日期格式
			String date = df.format(new Date());// new Date()为获取当前系统时间
			df = new SimpleDateFormat("yyyy");// 设置日期格式
			String nowYear = df.format(new Date());
			String key = "-" + nowYear + "-" + date;
			username=AESUtil.decrypt(key, ltpatoken);
		       password=AESUtil.decrypt(key,ltpSecret);
	        }else if (StringUtils.isNotBlank(ekpBpmClientkey)&&StringUtils.isNotBlank(ekpBpmClienToken)
			  &&StringUtils.isBlank(username)&&StringUtils.isBlank(password)) {
		          /*  SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");// 设置日期格式
				String date = df.format(new Date());// new Date()为获取当前系统时间
				df = new SimpleDateFormat("yyyy");// 设置日期格式
				String nowYear = df.format(new Date());
				String key = "-" + nowYear + "-" + date;
				username=AESUtil.decrypt(key, ekpBpmClientkey);
			        password=AESUtil.decrypt(key,ekpBpmClienToken);*/
	        }
		
		writeLog(OperationType.LOGIN_ACTION_OPTR, "用户登录,username="+ username + ",password=******");
		if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
			request.setAttribute("msg", "用户名或密码不能为空!");
			return "/console/login";
		}

		// 想要得到 SecurityUtils.getSubject() 的对象.访问地址必须跟 shiro 的拦截地址内.不然后会报空指针
		// 用户输入的账号和密码,,存到UsernamePasswordToken对象中..然后由shiro内部认证对比,
		// 认证执行者交由ShiroDbRealm中doGetAuthenticationInfo处理
		// 当以上认证成功后会向下执行,认证失败会抛出异常
		Subject user = SecurityUtils.getSubject();
		UsernamePasswordToken token = new UsernamePasswordToken(username,password);
		try {
			user.login(token);
		} catch (AuthenticationException e) {
			LOGGER.error("登录异常",e);
			token.clear();
			request.setAttribute("msg", e.getMessage());
			return "/console/login";
		}
		
		//设置系统语言到request
		String language = commonMethodService.smartformLanguage(request);
		String userNum = (String) getSession().getAttribute(HttpSessionKey.CURRENT_USER_NUM);
		if("".equals(language)){
			SysUserProfile bean = userProfileService.getUserProfile(userNum);
			if(bean!=null){
				language = bean.getDefaultLang();
			}
		}
		//暂时默认都是中文
		language = "messages_zh_CN";
		cookieLocaleResolver.setLocale(request, response, new Locale(language) );
		request.getSession().setAttribute("smartformLanguage", StringUtils.lowerCase(language));
		OrgEmployee emp = orgEmployeeService.getUser(userNum);
		request.getSession().setAttribute("currentUser", emp);
		//设置会话超时时间
		getSession().setTimeout(3600000);
		// 用户信息保存到cookie
		accountWriteCookie(username, password);
		BpmGlobalConfig gcfg = bpmGlobalConfigService.getFirstActConfig();
		new BpmClientUtils(gcfg, false).doLoginSetCookie(request, response, username, password);
		try {
			String domHost = new SysPropertyUtil().getProperty("DominoServer", "/sysConfigure.properties");
			AacEmployee aacEmp = aacEmployeeService.loadByAdName(username);
			// String url = domHost+"/names.nsf?login&username=";
			//System.out.println("登录domino:" + url);
			request.getSession().setAttribute("Dominousername",aacEmp.getDominousername());
			request.getSession().setAttribute("Dominouserpsw",URLEncoder.encode(StringUtil.simpleDecrypt(aacEmp.getDominouserpsw()),"iso-8859-1"));
			
			CookieStore cookieStore = new BasicCookieStore();
			CloseableHttpClient httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
		} catch (Exception e) {
			//e.printStackTrace();
			LOGGER.error("登录异常",e);
		}
		// System.out.println("执行domino登录返回的结果:"+result);
		request.removeAttribute("msg");
	} catch (Exception e) {
		LOGGER.error("登录异常",e);
		//e.printStackTrace();
		// request.setAttribute("msg", "登录异常,请联系管理员!");
		// return "/console/login";
	}

	SavedRequest savedRequest = (SavedRequest) SecurityUtils.getSubject()
			.getSession().getAttribute("shiroSavedRequest");
	if (savedRequest != null) {
		String redirectUrl = savedRequest.getRequestUrl();
		String path = request.getContextPath();
		if (StringUtils.isNotBlank(redirectUrl)) {
			redirectUrl = redirectUrl.replace(path, "");
			if (redirectUrl.indexOf(".xsp") > 0) {
				return "redirect:" + redirectUrl;
			}
		}
	}
	
	String beforeloginUrl=(String) request.getSession().getAttribute(HttpSessionKey.BEFORE_LOGIN_URL);
	request.getSession().removeAttribute(HttpSessionKey.BEFORE_LOGIN_URL);
	if(StringUtils.isNotBlank(beforeloginUrl)){
		//System.out.println(beforeloginUrl);
		defaultPath=beforeloginUrl;
	}
	if(StringUtils.isBlank(defaultPath)){
		defaultPath="/bpmportal/home/index.xsp";
	}
	Session session = SecurityUtils.getSubject().getSession();
	String userNum = (String) session.getAttribute(
			HttpSessionKey.CURRENT_USER_NUM);
	if(StringUtils.isNotBlank(userNum)){
	   menuItemService.userAuthorityAdminMenu(userNum);
	}
		return "redirect:"+defaultPath;
	}
}

参考资料如下:
https://blog.youkuaiyun.com/u011781521/article/details/55094751
https://blog.youkuaiyun.com/pyfysf/article/details/81952889

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值