一: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> </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