项目中的门禁管理者贾维斯———关于 Shiro 框架的 基本使用和基础配置,以及工作流程的生动解释
这几天自己做了一个小的demo,学习并使用了shiro框架用来管理我的登录与授权.学习这个框架到成功运用用了三天时间,前两天都是出于理解和踩坑阶段,第三天才真正开始撸起了代码,并自测成功.
首先我是通过这个这个网站学习的shiro
How2J 的 Java教程 http://how2j.cn/
这个网站非常好,涉及的知识面也特别的广,大家可以来收藏.
有了初步的了解后,我来捋一下我的shiro 的理解.
shiro是用来做登录管理和授权角色管理的,也就是说关于"登录"这部分都可以交给shiro去做.
打个比方: 只能登陆后才能访问的部分,可以比作是我们的家.登录页面,就是门禁系统,那shiro,就是这个门禁系统的核心,更形象一点说,这个shiro就是我们的管家(类似于钢铁侠的贾维斯).
我们可以通过配置贾维斯来控制哪些人(也就是代码里的链接)能进家里而不需要身份认证,哪些人必须要认证后才能进来.一旦贾维斯识别出这个人不是我们提前设置好的,那就统统给我滚回到门禁那里去做身份识别(也就是登录页面).这部分是shiro的登录认证管理.
通过了身份验证后,你就可以通过门禁系统而进入到我家里(也就是项目中的其他部分),并且我们的贾维斯能够记住你的身份,你的身份信息都被保存下来了.我们也可以随时随地都可以拿到你的身份信息.
进入我家后也不是所有的房间你都能进去,比如我的书房只有我的最亲的人才能进去.那当你想要进入我的书房时,贾维斯就开始识别你的身份,来看看你是不是我最亲的人.如果发现你不是,那你就没有权限进入书房,如果是,那就能顺利进入.这部分就是shiro的身份授权管理.
到这里,大家都对shiro是干嘛的有了一个基本的了解了.简单点说,shiro就是个拦截器,就像哨卡一样,拦住你,盘问你,识别你.不过有一点要注意,shiro只是来辅助你做这些事,至于哪些链接需要认证,哪些不需要,权限与角色的配置,这些都算是业务逻辑,必须由我们去完成才可以.
接下来,让我们来看看,要构建这么一个牛逼的安全管家应该怎么做.
由于现在用的比较多是SSM框架,所以这次我讲述的也都是在SSM框架环境下的shiro,其他环境下的我就不在这里赘述了.
首先导包.
就是ssm框架的那一套都导进去,然后 去shiro的官方http://shiro.apache.org/download.html上去下载 shiro的包,如果是maven项目,官网里也有pom中的各种依赖,复制粘贴就OK.
接下来搞SSM和shiro的配置文件.
配置文件有:
web.xml 管理配置文件,sprigmvc核心分发servlet以及shiro 过滤器的配置
spring-mvc.xml springmvc的核心配置文件
spring-shiro.xml shiro的核心配置文件,其实也可以与sping-mvc.xml写在一起
spring-mybatis.xml 管理数据库连接的
各配置的文件名不重要,重要的是里面的内容,并且我在这里列出要注意的和新增加的xml代码,常规的框架配置我就不再赘述.
一.首先是web.xml
因为我这里把shiro单独拿出来做一个xml配置文件了,所以在做xml引用时,我们要把这个文件加进去,由于我命名都是按照"spring-"为前缀,所以用了一个通配符,如果你的文件名不是这样的,那就分别写出来,就像注释里的那样.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:spring-*.xml
<!--
classpath:applicationContext.xml,
classpath:applicationContext-shiro.xml
-->
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
然后配置shiro过滤器
<!-- shiro过滤器定义 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->
<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>
二.spring-mvc.xml
1. springmvc的基本配置(这部分代码不展示了)
2. 增加了对shiro的支持 这样可以在控制器Controller上,使用像@RequireRole 这样的注解,来表示某个方法必须有相关的角色才能访问
3. 指定了异常处理类DefaultExceptionHandler,这样当访问没有权限的资源的时候,就会跳到统一的页面去显示错误信息,这条可有可无,看你的逻辑,如果需要可以配置
<!--启用shiro注解 -->
<bean
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
<bean
class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!-- 控制器异常处理 -->
<bean id="exceptionHandlerExceptionResolver" class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver">
</bean>
<bean class="com.how2java.exception.DefaultExceptionHandler"/>
三.spring-shiro.xml
这部分很关键,我把所有的代码都贴上来
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-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.xsd http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<context:component-scan base-package="com.blog.service"/>
<!-- 配置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" />
</util:map>
</property>
<!-- 权限配置 -->
<property name="filterChainDefinitions">
<value>
<!-- anon表示此地址不需要任何权限即可访问 -->
/loginBlogger=anon
/index=anon
/static/**=anon
/doLogout=logout
<!--所有的请求(除去配置的静态资源请求或请求地址为anon的请求)都要通过登录验证,如果未登录则跳到/login -->
/** = authc
</value>
</property>
</bean>
<!-- 退出过滤器 -->
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="/login" />
</bean>
<!-- 会话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>
<!-- 安全管理器 -->
<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>
<bean id="databaseRealm" class="com.blog.realm.DatabaseRealm">
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
</beans>
具体哪个是做什么的,我不多说.我会着重说几个.
<context:component-scan base-package="com.blog.service"/>
你会疑问了,这个为啥子在这里配呢.这个不应该springmvc.xml的配置文件里配吗? 没错,常规来说,确实是这样的.但是如果不在shiro.xml里配,到时候再shiro的realm中使用service层就会报错.可以看看这篇博客.
shiro框架,自定义realm注入service失败解决办法 - u33445687的博客 - ****博客
https://blog.****.net/u33445687/article/details/83617510
<!-- 配置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" />
</util:map>
</property>
<!-- 权限配置 -->
<property name="filterChainDefinitions">
<value>
<!-- anon表示此地址不需要任何权限即可访问 -->
/loginBlogger=anon
/index=anon
/static/**=anon
/doLogout=logout
<!--所有的请求(除去配置的静态资源请求或请求地址为anon的请求)都要通过登录验证,如果未登录则跳到/login -->
/** = authc
</value>
</property>
</bean>
这部分很重要,这部分就是告诉你的贾维斯,哪些人能过,哪些人是需要认证才能过的.以及退出指令等等.
<bean id="databaseRealm" class="com.blog.realm.DatabaseRealm">
</bean>
这个是告诉shiro我们的realm是哪个.realm是什么,这个我们一会再说,你就先配置上就行.
至于其他的代码,都不用动,直接复制粘贴就行,当然了,那些配置具体是做什么的,可以自行去查找,网上都有.
四.spring-mybatis.xml 这个就是Mybatis的那一套, 没啥说的
我们看看登录效果哈
首先去登录,登陆失败,会到登录页面,并展示出错误信息.登录成功后,这时复制主页链接,点击安全退出后,在地址栏输入主页链接,会自动跳回登录页面.
接下来看看实现的代码
首先要有个登录页,这不必说了,表单的地址要为可通过的(即要在shiro.xml配置好)
后台登录的controller代码如下:
@Controller
@RequestMapping("")
public class LoginController {
@RequestMapping("/loginBlogger")
public String login(Model model,Blogger blogger) {
//获取当前用户的shiro信息
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isAuthenticated()) {
return "redirect:/backgroundIndex";
}
//token安全令牌
UsernamePasswordToken token = new UsernamePasswordToken(blogger.getUserName(), blogger.getPassword());
try {
//调用登录,会自动进入realm
currentUser.login(token);
Session session=currentUser.getSession();
session.setAttribute("subject", currentUser);
//登陆成功
return "redirect:/backgroundIndex";
//登陆失败.抛出异常
} catch (AuthenticationException e) {
model.addAttribute("error", "验证失败");
return "/background/login.jsp";
}
}
}
这个LoginController就相当于门禁系统,他会去先判断有没有账号登录了,如果有登录,就会直接跳到主页去,如果没有,他就会拿着前台传过来的账号密码,来封装成一个安全令牌token,也可以理解为一个小纸条,上面有账号密码,然后拿着这个token去找我们的贾维斯,也就是realm,贾维斯开始判断,你这个过来的小纸条上的人可不可以通过.currentUser.login(token)执行时就会去realm,通过断点debug可以发现,他就会自动进入realm方法.
现在开始说这个shiro中最最关键的realm,英文翻译为"域",在我们这里就是可以理解为贾维斯.贾维斯会进行身份验证,会进行角色认证,也会进行授权认证(今天我们的例子中只有登录身份验证,等下期再说角色授权).
来看看realm的代码:着重去看登录与验证这部分
public class DatabaseRealm extends AuthorizingRealm {
@Autowired
private ILoginService loginSerivce;
/**
* 角色与权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 登录与验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取账号密码
UsernamePasswordToken t = (UsernamePasswordToken) token;
String userName= token.getPrincipal().toString();
String password= new String( t.getPassword());
//获取数据库中的密码
Blogger blogger = loginSerivce.getBloggerByName(userName);
//如果为空就是账号不存在,如果不相同就是密码错误,但是都抛出AuthenticationException,而不是抛出具体错误原因,免得给**者提供帮助信息
if(null==blogger.getPassword() || !blogger.getPassword().equals(password))
throw new AuthenticationException();
//认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(blogger,blogger.getPassword(),getName());
return a;
}
}
逻辑很简单,就是获取到从前台传过来的token,也即是小纸条.然后得到纸条上的账号和密码,一般情况下,用户名都是唯一存在的,所以我们可以用用户名去数据库查找获得用户,获得后,再去进行判断.如果账号或密码不对,就要抛出异常.如果账号和密码匹对上了,我们就要把登陆的用户存放在SimpleAuthenticationInfo认证信息当中,意思就是告诉让贾维斯知道,这个人登陆成功了,也是为了方便我们在以后的业务逻辑当中随时能够获取当当前登录的人.
注意,SimpleAuthenticationInfo 这个对象中的第一个参数,我们尽量要存进去的是这个登录人的实例对象,也是方便以后获取,网上很多案例存的都是用户名.但是万一以后我们要修改账户信息的话,我还得从数据库查一遍,不如现在就存个实例对象,直接调用即可.
你也许会问,为啥在LoginController中执行currentUser.login(token)时就会走到这个realm中呢.?还记得我们在配置shiro.xml时配置过我们的realm路径不?
<bean id="databaseRealm" class="com.blog.realm.DatabaseRealm">
</bean>
class中要写我们realm的全路径名,这样就能找到了.
至于退出的话,无非就是向后台发送一个关于退出的请求,就是在shiro.xml中配置的那个,发送后就会退出,并跳到相应的路径.
到这本次博客就基本结束了.
由于我的资历尚浅,道行不深,大部分也都是在网上找的,然后再加上自己的理解总结出来的.基本上也就是会用,至于为什么这么用,我自己还有很多很多不理解的地方.不管怎样,先跑起来,然后再一点一点地消化
目前我自己的这个小项目还没有涉及到角色权限这块,等我研究研究的再来写一篇关于权限与角色.