Shiro安全框架
百度一下:
Apache Shiro(读作“sheeroh”,即日语“城”)是一个开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
- Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Shiro安全框架交互过程:
- Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
- SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
- Realm:域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
程序员关注2个部分:如果获得Subject与如何定义一个符合规定的Realm(包括密码比较器)。
Shiro安全框架过滤器配置:
过滤器简称 | 对应的java类 |
---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
logout | org.apache.shiro.web.filter.authc.LogoutFilter |
Shiro标签:
标签名称 | 标签条件(均是显示标签内容) |
---|---|
shiro:authenticated | 登录之后 |
shiro:notAuthenticated | 不在登录状态时 |
shiro:guest | 用户在没有RememberMe时 |
shiro:user | 用户在RememberMe时 |
shiro:hasAnyRoles name=”abc,123” | 在有abc或者123角色时 |
shiro:hasRole name=”abc” | 拥有角色abc |
shiro:lacksRole name=”abc” | 没有角色abc |
shiro:hasPermission name=”abc” | 拥有权限资源abc |
shiro:lacksPermission name=”abc” | 没有abc权限资源 |
shiro:principal | 默认显示用户名称 |
使用Shiro
1,maven坐标的配置
在parent的工程中已经配置好了。
2,配置过滤器(放到struts的配置之前)
<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>/*</url-pattern>
</filter-mapping>
3,在spring applicationContext.xml中记载shiro配置文件,放在事务管理器之前配置
这里就是产生代理的方式,应该放在事务代理器之前
<!--2.Shiro安全框架产生代理子类的方式: 使用cglib方式 -->
<aop:aspectj-autoproxy proxy-target-class="true" />
4,导入Shiro的配置文件
<?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">
<description>Shiro的配置</description>
<!-- SecurityManager配置 -->
<!-- 配置Realm域 -->
<!-- 密码比较器 -->
<!-- 代理如何生成? 用工厂来生成Shiro的相关过滤器-->
<!-- 配置缓存:ehcache缓存 -->
<!-- 安全管理 -->
<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.jk.shiro.AuthRealm">
<property name="userService" ref="userService"/>
<!-- 自定义密码加密算法 -->
<property name="credentialsMatcher" ref="passwordMatcher"/>
</bean>
<!-- 设置密码加密策略 md5hash -->
<bean id="passwordMatcher" class="cn.itcast.jk.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>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 生成代理,通过代理进行控制 -->
<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>
</beans>
做一点简单的描述吧。
在shiro的配置文件中,需要我我们手动处理的几个部分。
安全管理器的配置
需要注入我们自己的验证域和一个缓存管理。缓存管理比较简单,验证域应该就是我们比较和新的验证业务。
缓存管理的内容如下,这个路径是需要我们手动更改的。
自定义的认证域配置
我们需要注入一个是用户的service,另一个是我们的密码比较器。
我们看到出了注入下方的bean就是我们自定义的密码比较器。
5,先配置一些必要的注入等内容:
5.1,先导入缓存管理的文件
我们之前看到上图中的缓存管理注入的位置是“classpath:ehcache-shiro.xml”,所以我们就在resource中直接创建这个文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shiroCache">
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
</ehcach
5.2,密码比较器
先用一个密码比较器的空模板,包的位置看一下注入的property
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
public boolean doCredentialsMatch(AuthenticationToken token,
AuthenticationInfo info) {
return false;
}
}
5.3,AuthRealm的注入set方法。
public class AuthRealm extends AuthorizingRealm{
public UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
// TODO Auto-generated method stub
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException {
// TODO Auto-generated method stub
return null;
}
}
6,在Spring中导入Shiro的配置文件
<import resource="classpath:applicationContext-shiro.xml"></import>
运行起来之后,点击部门管理会变成下面这个死样子
原因就是部门管理不能直接访问了,需要转到登录页,但是我们的系统目前没有登录功能,就只能回到这个首页了。
很难得,一个错误的页面验证了之前的工作都是正确的。保存一下草稿,丢了N回了。
解决一下,在fmain.jsp中添加一个script
<script type="text/javascript">
if (self.location != top.location) {
top.location = self.location;
}
</script>
解释一下,由于shiro过滤器告诉失败会让我们的页面回到index.jsp,而我们顶层框架是部门管理。于是我们通过上述代码让顶层框架访问index.jsp。其原因是我们现在没有配合登录页面。
于是我们回到LoginAction类中,看到我们之前注释掉的代码,做一点修改。
public String login() throws Exception {
// if(true){
// String msg = "登录错误,请重新填写用户名密码!";
// this.addActionError(msg);
// throw new Exception(msg);
// }
// User user = new User(username, password);
// User login = userService.login(user);
// if (login != null) {
// ActionContext.getContext().getValueStack().push(user);
// session.put(SysConstant.CURRENT_USER_INFO, login); //记录session
// return SUCCESS;
// }
// return "login";
if (UtilFuns.isEmpty(username)) {
return "login";
}
return SUCCESS;
}
再看一下。
7,补充登录功能
7.1,密码比较器编写
7.1.1,导入md5hash工具类
在util中添加Encrypt工具类。
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();
}
}
7.1.2,密码比较器的编写
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
// token:用户在页面输入的用户名密码,info代表从密码中得到的加数据
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
// 向下转型
UsernamePasswordToken usertoken = (UsernamePasswordToken) token;
// 将用户输入的原始密码加密
// 注意token.getPassword()拿到的是一个char[],不能直接用toString(),它底层实现不是我们想的直接字符串,只能强转
// 用户名做为salt
Object tokenCredentials = Encrypt.md5(String.valueOf(usertoken.getPassword()), usertoken.getUsername());
//取得数据库中的密码数据
Object accountCredentials = getCredentials(info);
return equals(tokenCredentials, accountCredentials);
}
}
这里的使用的equals是父类提供的。
7.2,认证域的编写
package com.liuyang19900520.liussh.shiro;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import com.liuyang19900520.liussh.domain.Module;
import com.liuyang19900520.liussh.domain.Role;
import com.liuyang19900520.liussh.domain.User;
import com.liuyang19900520.liussh.service.UserService;
public class AuthRealm extends AuthorizingRealm {
// 注入
public UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
System.out.println("授权");
// 首先根据name找到对象,对象的next就是一个user
User user = (User) arg0.fromRealm(this.getName()).iterator().next();// 根据realm的名字去找对应的realm。
Set<Role> roles = user.getRoles();// 兑现导航
List<String> permissions = new ArrayList<>();
for (Role role : roles) {
Set<Module> moudles = role.getModules();
for (Module m : moudles) {
// 这些标签在shiro:hasPermission的标签中会出现重复,
// 但是标签的操作时用hascontents方法来进行,所以有重复也没有关系
permissions.add(m.getName());
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissions);// 添加用户的权限--即我们的模块
return info;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException {
System.out.println("认证");
// 向下转型(代表用户用在页面上输入的用户名与密码)
UsernamePasswordToken upToken = (UsernamePasswordToken) arg0;
// 调用业务方法,根据业务名查询
String hql = "from User where userName=?";
List<User> list = userService.find(hql, User.class, new String[] { upToken.getUsername() });
if (list != null && list.size() > 0) {
User user = list.get(0);
AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassWord(), this.getName());
return info;// 此处如果返回,就会进入到密码比较器。
}
return null;
}
}
7.3,编写loginAction写登录逻辑
// SSH传统登录方式
public String login() throws Exception {
if (UtilFuns.isEmpty(username)) {
return "login";
}
try {
// 得到Subject
Subject subject = SecurityUtils.getSubject();
// 构造token(我们的username和password是页面上的)
UsernamePasswordToken upToken = new UsernamePasswordToken(username, password);
// 盗用shiro中的登录方法
subject.login(upToken);// 当这一行代码执行时,就会自动跳转到比吗比较器
// shiro有缓存,用户信息存在缓存中,
User user = (User) subject.getPrincipal();
// 将User放入sessoin中
session.put(SysConstant.CURRENT_USER_INFO, user);
} catch (Exception e) {
request.put("errorInfo", "对不起,用户名密码错误");
return "login";
}
return SUCCESS;
}
8,顶部菜单释放
如果你也想问,为什么左侧的菜单不用加shiro标签么,那么请看一下左侧菜单的代码。
如图所示也就是说我们后续进到的模块都是通过user的角色到数据库查询出来的,不在需要shiro标签进行授权了,就是说菜单都是动态的。
所以我们要解开这个
还有值得注意的一点是,一旦JSP页面上出现shiro标签,就会执行授权方法。即认证域中的doGetAuthorizationInfo方法。
日常改错
输入正确的用户密码都不好使啊。这下验证得实在是太好了。
于是我重启了一下服务,JRbel真的不好使啊。于是成功了。
虽然权限看起来不同了,可是菜单怎么突然变多了这么多,并且还是重复的,一定是哪里出了问题。我们再来查询一下。
结果是清一下浏览器缓存,应该就没有问题了。