ERP管理系统的权限控制实现--shiro
1 Shiro基本原理分析
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro可以使用的地方:命令行程序、移动应用、web项目、spring项目...
应用代码
---- 访问Subject (shiro的Subject 就代表当前登陆用户)
---- Subject 在shiro框架内部自动调用 Shiro SecurityManager 安全管理器,shiro的核心(好比Struts2的核心控制器)
----- 安全管理器调用 Realm域 (程序员自定义的 )
可以看到:应用代码直接交互的对象是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能得到合法的数据。
在这个过程中我们需要关注的问题:
1、 如何获取subject
2、 如何定义一个Realm域
3、 密码比较器如何编写
2、系统登录功能—Shiro与Spring整合
Shiro的使用不依赖Spring框架,javase可以用,javaee也可以用,移动应用程序可以用,大型的网络和企业应用程序也可以使用Shiro。
传统登录方式:
Shiro安全框架实现登录
传统的方式是,客户端发出请求给Action,action接受用户的用户名和密码,然后由Action调用业务逻辑,在Action中调用Service,然后service负责调用数据库进行处理,如果用户名和密码正确,则将用户信息保存到session中,并且进入主页面
如果用户名和密码错误,则保存错误信息,然后将信息输出到客户端。
答:客户端发送请求到Action,action接受用户的用户名和密码,第一步还是一样的,但是第二步不一样了,以前第二步是Action直接调用Service处理,现在Action调用Shiro安全框架去处理,也就是说将认证授权抽取出来,有一个框架专门为你做认证做授权,这里有框架去帮我们完成认证和授权,然后告诉你这个用户名和密码是否可用还是不可用,当然此时,认证成功之后,只要从shiro中取出认证的结果,如果成功的话,将用户保存至session中,然后在跳转至jsp
这个过程相比于早期的操作,相当于验证用户名和密码的业务逻辑交给shiro安全框架来做。并且加密也交给shiro安全框架来做,然后由shiro安全框架来加密,由shiro拿密文与数据库中的密文进行比较。
总结一下:shiro就是一个安全框架,帮助我们解决认证、授权、加密和密码比较的过程。
2.1 shiro开发步骤
2.1.1 导入jar包
在jx_parent项目中,导入shiro的依赖jar包,具体配置如下:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>1.3.2</version> </dependency> |
2.1.2 过滤器的配置
Shiro怎么用呢?他能解决啥问题?认证、授权、会话管理、加密、与web集成、缓存这6块主要功能
认证:其实认证的方式有很多,可以刷脸、刷指纹,shiro主要借助于用户名和密码进行认证。
Shiro能帮你做的就是根据用户名和密码进行认证
使用shiro,需要配置shiro框架。使用Shiro时,需要配置的相关权限过滤器如下(共10个):
认证和授权是两码事.
Anon可以不登陆访问;authc必须登录才能访问。
配置过滤器web.xml:一个过滤器代理类,配置shiroFilter后,可以应用 10种过滤规则
配置shiroFilter其实是一个过滤器链,含有10个Filter
<!-- Shiro Security filter filter-name这个名字的值将来还会在spring中用到 --> <filter> <!-- 这里的 filter-name 要和 spring 的 applicationContext-shiro.xml 里的 org.apache.shiro.spring.web.ShiroFilterFactoryBean的 bean name 相同 --> <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> |
【注意】这个shiroFilter的配置一定要在struts2的控制器之前配置,否则shiroFilter无法启动
以前的web的过滤器谁初始化的?
Servlet容器初始化的。
这个Filter 是 spring提供的,DelegationFilterProxy 是代理Filter
(会自动找和<filter-name> 同名的 <bean> 对象)
2.1.3 shiro标签
什么时候调用授权的方法:主要看页面是否有shiro标签
<shiro:notAuthenticated><inputtype=”button” value=”登录”></shiro:notAuthenticated>
<shiro:authenticated><input type=”button”value=”注销”></shiro:authenticated>
<c:if test=””>
Home文件下下面的title页面就使用了这个标签,具体如下,也可以进入页面查看:
2.1.4 Shiro的配置文件
配置applicationContext-shiro.xml:(shiro权限控制过滤器+shiro安全管理器):
applicationContext-shiro.xml放置的位置:
shiro的配置步骤 -->
1 配置安全管理器SecurityManager
2 realm域配置:由于SecurityManger需要使用realm域,涉及到用户信息、权限信息,处理用户信息的时候需要加密
3 密码比较器:用户输入的铭文进行加密,并且与数据库中的密文进行比较
4 配置生成过滤器的工厂类
5 整个过程中,用户的数据都放入了realm、域中了,那么下次用户访问的时候,还需要查吗?
如果没有缓存,那还需要查,所以这里需要配置缓存,采用ehcache作为缓存提供商
在该文件中需要配置的内容如下:
<?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:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置shiro安全管理器 -->
<!-- 调用Realm域 --> <!-- realm与调用密码比较器 --> <!-- 缓存 --> <!-- 配置具体的过滤规则 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- Single realm app. If you have multiple realms, use the 'realms' property instead. --> <property name="realm" ref="authRealm"/><!-- 引用自定义的realm --> <!-- 缓存 --> <property name="cacheManager" ref="shiroEhcacheManager"/> </bean>
<!-- 自定义权限认证 --> <bean id="authRealm" class="cn.itcast.jx.shiro.AuthRealm"> <property name="userService" ref="userService"/> <!-- 自定义密码加密算法 --> <property name="credentialsMatcher" ref="passwordMatcher"/> </bean>
<!-- 设置密码加密策略 md5hash --> <bean id="passwordMatcher" class="cn.itcast.jx.shiro.CustomCredentialsMatcher"/>
<!-- filter-name这个名字的值来自于web.xml中filter的名字 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <!--登录页面 --> <property name="loginUrl" value="/index.jsp"></property> <!-- 登录成功后 --> <property name="successUrl" value="/home.action"></property>
<property name="filterChainDefinitions"> <!-- /**代表下面的多级目录也过滤 --> <value> /index.jsp* = anon /home* = anon /sysadmin/login/login.jsp* = anon /sysadmin/login/logout.jsp* = anon /login* = anon /logout* = anon /components/** = anon /css/** = anon /images/** = anon /js/** = anon /make/** = anon /skin/** = anon /stat/** = anon /ufiles/** = anon /validator/** = anon /resource/** = anon /** = authc /*.* = authc </value> </property> </bean>
<!-- 用户授权/认证信息Cache, 采用EhCache 缓存 --> <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/> </bean>
<!-- 开启权限控制的注解功能 --> <!-- 后处理器:通过动态代理在某bean实例化的前增强。 --> <!-- 保证实现了Shiro内部lifecycle函数的bean执行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 生成代理,通过代理进行控制 :切面自动代理:相当于以前的AOP标签配置--> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"> <property name="proxyTargetClass" value="true"/> </bean>
<!-- 注入安全管理器:Advisor切面配置:授权属性的切面 --> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
</beans> |
2.1.5 在applicationContext.xml文件中加载shiro配置文件
<import resource="classpath:spring/applicationContext-shiro.xml"></import> |
2.1.6 配置ehcache-shiro.xml文件
直接拷贝文件
2.1.7准备Realm域和密码比较器
1 创建AuthRealm类,继承AuthorizingRealm
package cn.itcast.jx.shiro;
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import cn.itcast.jx.service.UserService; /** * 自定义的Realm域 */ public class AuthRealm extends AuthorizingRealm { private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken arg0) throws AuthenticationException { // 认证
return null; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { // 授权
return null; }
}
|
2 创建密码比较器类CustomCredentialsMatcher,继承SimpleCredentialsMatcher父类。
3 运行:观察结果,不进入登录页面,所以修改LoginAction
2.1.8 MD5加密介绍--shiro内部的加密
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,
常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如123456,,撒一把salt,salt是“admin”,
产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,
可以到一些md5解密网站很容易的通过散列值得到密码“admin”,
即如果直接对密码进行散列相对来说**更容易,此时我们可以加一些只有系统知道的干扰数据,
如用户名和ID(即盐);这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难**。
在工具包jx_util中加入Encrypt工具类,类中代码如下:
package cn.itcast.jx.util;
import org.apache.shiro.crypto.hash.Md5Hash; /** * @Description: * @Author: tengyuan * @Company: http://java.itcast.cn */ public class Encrypt { /* * 散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据, * 常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”, * 产生的散列值是“21232f297a57a5a743894a0e4a801fc3”, * 可以到一些md5解密网站很容易的通过散列值得到密码“admin”, * 即如果直接对密码进行散列相对来说**更容易,此时我们可以加一些只有系统知道的干扰数据, * 如用户名和ID(即盐);这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难**。 */ //高强度加密算法,不可逆 public static String md5(String password, String salt){ /** * 第一个参数:密码 * 第二个参数:盐 * 第三个参数:打乱的次数 */ return new Md5Hash(password,salt,2).toString(); } public static void main(String[] args) { /** * "123456","tony",1 :5c51b703b1399a874e12d38a4cf33e46 * "123456","tony",2: 4eaf863bbc05d88cf4004f7a2da7877f * "123456","tony",3: d63664690a5dad12012b6e63a86e1d49 * "123456","KMNO4",3:8bd35dc14dc07f756478bb44513694f6 * 1 密码明文 * 2 盐 * 3 几把 */ System.out.println(new Md5Hash("123456","KMNO4",3).toString());
/** *Sha1Hash("123456", "tony", 1):1e88f1b63745c7aad14b74c28c09e29a9548e57b * */ System.out.println(new Sha1Hash("123456", "tony", 1));
System.out.println(new Sha256Hash("tony", "tony", 1));
System.out.println(new Sha512Hash("123456", "tony", 1));
System.out.println(new Sha384Hash("123456", "tony", 1)); } } |
2.1.9 编写密码比较器
在jx_web模块中,编写密码比较器类,类名为CustomCredentialsMatcher,这个类需要继承SimpleCredentialsMatcher父类。
具体代码如下:
package cn.itcast.jx.shiro;
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import cn.itcast.jx.util.Encrypt;
/** * @Description: * @Author: 刘腾远 * @Company: http://java.itcast.cn * @CreateDate: 2014年12月30日 */ public class CustomCredentialsMatcher extends SimpleCredentialsMatcher { /** * Authentication:认证 * 重写SimpleCredentialsMatcher类的doCredentialsMatch(密码比较的方法) * 返回true:校验成功 * 返回false:校验失败 * * 第一个参数:AuthenticationToken:用户在界面上输入的用户名和密码,思考问题:页面如何传递过来? * 第二个参数:AuthenticationInfo:数据库中用户的密文 * */ public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //向下转型 UsernamePasswordToken upToken = (UsernamePasswordToken) token; //2 得到原始密码 //错误的方式 //String oldPwd = upToken.getPassword().toString(); //正确的方式 String oldPwd = new String(upToken.getPassword()); // 3 对密码进行加密 String newPwd = Encrypt.md5(oldPwd, upToken.getUsername()); // 获取数据库中的当前用户的密文 Object dbPwd = info.getCredentials(); //将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false return equals(newPwd, dbPwd); } } |
其实我们在applicationContext-shiro.xml中已经配置了密码校验器的代码(此时不需要配置了),具体配置如下:
<!-- 自定义加密策略 --> <bean id="passwordMatcher" class="cn.itcast.jx.shiro.CustomCredentialsMatcher"/> |
4.1.10 编写自定义的Realm域
【第一步】在jx_web中的shiro包中,创建AuthRalm类。
【第二步】类中编写认证的方法,代码如下:
public class AuthRealm extends AuthorizingRealm{ private UserService userService; public void setUserService(UserService userService) { this.userService = userService; }
/** * 认证 登录 * 当用户登录的时候,shiro真正会调用它,来实现登录操作 * * 当这个方法返回null时,就会出现异常 * 当认证成功后,返回的对象不为null时,就会自动进入密码比较器(在shiro的xml文件中,配置了密码比较器) * <property name="credentialsMatcher" ref="passwordMatcher"/> * * 第一个参数:AuthenticationToken token:用户在页面输入的用户名和密码 * */ protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { System.out.println("认证"); // 1 向下转型 UsernamePasswordToken upToken = (UsernamePasswordToken)token; // 2 根据用户名到数据库查找用户是否存在 User user = userService.findUserByName(upToken.getUsername()); // 3 判断用户对象是否存在, if(user==null){ //用户名不存在 return null; }else{ //用户名存在 // 将用户信息封装到AuthenticationInfo对象中,回头再密码比较器中需要调用 /** * 第一个参数:Object principal:当前用户对象 * 第二个参数:Object credentials:数据库中的密文 * 第三个参数:String realmName:普通字符串,realm的名字,this.getName():当前类名对应的完整的包路径 * */ AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); return info; } } } |
在UserService中添加findUserByName方法,代码如下:
public abstract User findUserByName(String username); |
在UserServiceImpl中实现findUserByName方法,代码如下:
public User findUserByName(String username) { List<User> list = baseDao.find("from User where userName = ?", User.class, new String[]{username}); if(list!=null && list.size()>0){ return list.get(0); } else { return null; } } |
【第三步】编写授权的方法
/** * 授权的方法 * 使用的地方:当页面使用Shiro标签时,就会调用授权方法 * (怎么调用?谁调用?由shiro的核心控制器调用) * 第一个参数:PrincipalCollection :principals集合,对象主体集合 * * 如何授权?答:根据当前用户的信息来获取当前用户对应的角色,判断这些角色具备的访问某些模块的权限,从而给予当前用户特定的权限 * 所以授权第一步:获取当前用户 * * */ /** * PrincipalCollection principals:当前登录用户的集合 * * AuthorizationInfo返回值:授权信息 */ protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { // 授权 //1 获取当前登录用户 //方式一 //User user = (User) principals.fromRealm(this.getName()).iterator().next(); //方式二 //User user2 = (User) ServletActionContext.getRequest().getSession().getAttribute(SysConstant.CURRENT_USER_INFO); //方式三 User user = (User) SecurityUtils.getSubject().getPrincipal();
//定义一个集合,用来接收用户权限的模块名字 List<String> list = new ArrayList<String>();
//获取用户的角色 Set<Role> roles = user.getRoles(); for(Role r:roles){ //根据角色获取模块 Set<Module> modules = r.getModules(); //遍历模块 for(Module m:modules){ //判断是否是一级菜单,{系统首页,货物管理,系统管理} if(m.getCtype()==0){ list.add(m.getCpermission()); } } } //将list封装到AuthorizationInfo SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // info.addStringPermissions(list); return info; } |
2.2用户登录
用户登录的代码如下:
//SSH传统登录方式 public String login() throws Exception { if(UtilFuns.isEmpty(username)){ return LOGIN; } try { //shiro认证过程 //1 获取Subject对象 Subject subject = SecurityUtils.getSubject(); //2 实现登录过程:封装用户在页面上提交的用户名和密码 //就会进入Realm域中的认证方法 UsernamePasswordToken token = new UsernamePasswordToken(username, password); //3将封装好的用户名和密码交给shiro安全框架并实现登录 subject.login(token); //4 当认证成功后,要将Shiro中保存的用户对象取出来 User principal = (User) subject.getPrincipal(); //5 将用户对象放入session域中 session.put(SysConstant.CURRENT_USER_INFO, principal); //ServletActionContext.getRequest().getSession().setAttribute(SysConstant.CURRENT_USER_INFO, user); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); request.put("errorInfo","对不起,登录失败,用户名或者面错误"); return LOGIN; } // 6 跳页面 return SUCCESS; } |
【错误】此时抛出错误no session...,由于页面需要使用session,而此时session已经关闭了,主要原因是hibernate4.x开始,需要配置如下配置,方能继续使用session
<!-- 懒加载,配合web.xml中配置的 openSessionInViewFilter --> <property name="hibernate.enable_lazy_load_no_trans">true</property> |
2.3 Shrio的运行原理
2.4 账户不存在的时候报错
【解决方案】
2.5 重复登录的bug
当登录成功之后,每次在页面输入如下地址的时候,还是会再次进入登录页面:
进入登录页面,需要重新登录:
【原因分析】为什么会进入登录页面?
答:
【解决方案】如何解决问题呢?
实现方式一:可以提前通过subject获取用户的信息,如果用户信息存在,直接跳转至success页面
实现方式二:shiro的subject提供了是否登录的方法isAuthenticated
实现方式三:获取session中的信息,判断是否登录了
2.6、注销功能的实现
【第一步】页面分析
点击按钮,请求的方法如下:
【第二步】在LoginAction中添加如下方法:
【第三步】在struts.xml中置Action
3、Shiro开发总结
开发步骤:
1. 使用maven坐标引入shiro的jar:
2. Spring整合Shiro
Web.xml配置:DelgatingFilterProxy(spring的过滤器代理)
ApplicationContext-shiro.xml配置:提供filter同名的bean,该bean需配置安全管理器SecurityManager(DefaultWebSecurityManager基于HttpSession),还可以配置URL过滤规则等。
3. 程序调用Subject,Subject会自动调用SecurityManager,SecurityManager又会自动调用Realm,Realm域调用密码比较器,我们可以自定义Realm来实现安全数据和权限的结合。
4. 自定义Realm实现认证和授权。
5. Realm域调用密码比较器
6. 密码比较器调用加密算法
特点:
Shiro只是一个权限框架,数据(用户、角色、权限)可以来自与任何的的源,通过Realm获取源数据。
重点和总结:
1、 Shiro权限控制好处,为什么要使用Apache Shiro ?
使用简单,灵活数据可以采用自定义Realm 连接安全数据(没有数据来源要求, 来自文件、 数据库、网络 … )
2、 Shiro 运行原理
应用代码 ---- Subject ---SecurityManager ----- Realm
3、 自定义Realm 实现 用户认证和授权
4、 Spring 整合 Shiro 进行 权限控制
代码部分:
Shiro 会配置,会修改(不需要去记忆)
n Subject 代码调用
n 自定义Realm