shiro简介
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源,不多不少。
shiro就是一个权限管理的框架,是一个轻量级的权限框架,与之相应的还有一个spring的框架Spring security。他们之间的区别在于shiro是轻量级的,入门简单,但是是粗粒度的(一般够用了,实在要细粒度的管理的话可以自己写),而Spring security是细粒度的,但是是重量级的(他自己不会承认),入门相对不易。
shiro使用的是RBAC权限模型:RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。用户(n)-角色(n)-权限(n)(资源)(权限和资源的对应关系可以是多对多、多对一、一对一,根据自己的需求定,各有优劣),我这里使用的是一对一。
shiro四大基石
身份认证(登录) Authentication
授权(权限) Authorization
密码学 Cryptography
会话管理 Session Management
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
重要的对象
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
shiroDemo(基本原理)
建立项目
这里我们创建一个普通的maven项目,然后在pom文件里增加下面的包:
<!--shiro的核心包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<!--日志包-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!--测试包-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
</dependency>
自定义realm
写一个Realm,继承AuthorizingRealm
提供了两个方法,一个是授权doGetAuthorizationInfo,一个是身份认证 doGetAuthenticationInfo,这里面使用的密码是使用MD5加密的。
public class MyRealm extends AuthorizingRealm {
//授权认证功能就写在这里面
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//从数据库中获取角色并放且放到授权对象中
Set<String> roles = getRoles();
authorizationInfo.setRoles(roles);
//从数据库中获取权限并放且放到授权对象中
Set<String> perms = getPerms();
authorizationInfo.setStringPermissions(perms);
return authorizationInfo;
}
/**
* 假设这里获取到当前用户的角色
*/
private Set<String> getRoles(){
Set<String> roles = new HashSet<>();
roles.add("admin");
roles.add("it");
return roles;
}
/**
* 假设这里获取到当前用户的权限
*/
private Set<String> getPerms(){
Set<String> perms = new HashSet<>();
perms.add("employee:index");
return perms;
}
/**
* 记住:如果这个方法返回null,就代表是用户名错误,shiro就会抛出:UnknownAccountException
*/
//身份认证(登录)就写在这里面
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.拿到令牌(UsernamePasswordToken)
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//2.拿到用户名,判断这个用户是否存在
// 2.1 拿到传过来的用户名
String username = token.getUsername();
// 2.2 根据用户名从数据库中拿到密码(以后会拿用户对象)
String password = this.getUsers(username);
// 2.3 如果没有拿到密码(没有通过用户名拿到相应的用户->用户不存在)
if(password==null){
return null;
}
//记住:我们只在正常完成这里的功能,shiro会判断密码是否正确
//3.返回 AuthenticationInfo这个对象
/**
* 咱们创建对象需要传的参数:
* Object principal:主体(可以乱写) -> 登录成功后,你想把哪一个对象存下来
* Object credentials:凭证(就是密码) -> 数据库中的密码
* credentials(密码)Salt:盐值
* String realmName : realm的名称(可以乱写)
*/
//拿到咱们的盐值对象(ByteSource)
ByteSource salt = ByteSource.Util.bytes("itsource");
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,salt,"myRealm");
return authenticationInfo;
}
/**
* 假设这里是根据用户名进行的查询
* MD5:e10adc3949ba59abbe56e057f20f883e
* MD5+10次:4a95737b032e98a50c056c41f2fa9ec6
* MD5+10次+itsource:831d092d59f6e305ebcfa77e05135eac
*/
public String getUsers(String username){
if("admin".equals(username)){
return "831d092d59f6e305ebcfa77e05135eac";
}else if("zhang".equals(username)){
return "123";
}
return null;
}
}
编写测试类
@Test
public void testMyRealm() throws Exception{
//一.创建一个SecurityManager对象
// 1.创建realm对象
MyRealm myRealm = new MyRealm();
// 2.创建SecurityManager对象
DefaultSecurityManager securityManager = new DefaultSecurityManager(myRealm);
//②.相当于把SecurityManager放到了当前上下文
SecurityUtils.setSecurityManager(securityManager);
//③.拿到当前用户
Subject subject = SecurityUtils.getSubject();
//Hashed(哈希)Credentials(认证)Matcher(匹配器)
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//设置哈希算法
matcher.setHashAlgorithmName("MD5");
//设置迭代次数
matcher.setHashIterations(10);
//把匹配器交给shiro
myRealm.setCredentialsMatcher(matcher);
System.out.println("用户是否登录:"+subject.isAuthenticated());
//④.如果没有登录,让他登录
if(!subject.isAuthenticated()){
try {
UsernamePasswordToken token = new UsernamePasswordToken("admin","123456");
subject.login(token);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("神秘错误");
}
}
System.out.println("用户是否登录:"+subject.isAuthenticated());
System.out.println("是否是admin角色:"+subject.hasRole("admin"));
System.out.println("是否是hr角色:"+subject.hasRole("hr"));
System.out.println("是否有employee:index权限:"+subject.isPermitted("employee:index"));
System.out.println("是否有employee:save权限:"+subject.isPermitted("employee:save"));
System.out.println("是否有department:index权限:"+subject.isPermitted("department:index"));
}
集成spring
搭建项目(略)
在pom文件里增加下面的依赖
<!-- shiro(权限框架)的支持包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.4.0</version>
<type>pom</type>
</dependency>
<!-- shiro与Spring的集成包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
在web.xml中配置过滤器
这个过滤器只做拦截,不对其做任何操作,真正的操作的过滤器在后面的applicationContext-shiro.xml。
<!-- shiro的过滤器(帮我们拦截请求)-》什么事情都不做
Delegating:授(权); 把(工作、权力等)委托(给下级); 选派(某人做某事)
Proxy:代理 -> 需要通过名称(shiroFilter)去找真正的过滤器
-->
<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>
准备自定义的realm
public class JpaRealm extends AuthorizingRealm {
//授权认证功能就写在这里面
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//从数据库中获取角色并放且放到授权对象中
Set<String> roles = getRoles();
authorizationInfo.setRoles(roles);
//从数据库中获取权限并放且放到授权对象中
Set<String> perms = getPerms();
authorizationInfo.setStringPermissions(perms);
return authorizationInfo;
}
/**
* 假设这里获取到当前用户的角色
*/
private Set<String> getRoles(){
Set<String> roles = new HashSet<>();
roles.add("admin");
roles.add("it");
return roles;
}
/**
* 假设这里获取到当前用户的权限
*/
private Set<String> getPerms(){
Set<String> perms = new HashSet<>();
perms.add("employee:index");
// perms.add("user:*");
return perms;
}
/**
* 记住:如果这个方法返回null,就代表是用户名错误,shiro就会抛出:UnknownAccountException
*/
//身份认证(登录)就写在这里面
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.拿到令牌(UsernamePasswordToken)
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//2.拿到用户名,判断这个用户是否存在
// 2.1 拿到传过来的用户名
String username = token.getUsername();
// 2.2 根据用户名从数据库中拿到密码(以后会拿用户对象)
String password = this.getUsers(username);
// 2.3 如果没有拿到密码(没有通过用户名拿到相应的用户->用户不存在)
if(password==null){
return null;
}
//记住:我们只在正常完成这里的功能,shiro会判断密码是否正确
//3.返回 AuthenticationInfo这个对象
/**
* 咱们创建对象需要传的参数:
* Object principal:主体(可以乱写) -> 登录成功后,你想把哪一个对象存下来
* Object credentials:凭证(就是密码) -> 数据库中的密码
* credentials(密码)Salt:盐值
* String realmName : realm的名称(可以乱写)
*/
//拿到咱们的盐值对象(ByteSource)
ByteSource salt = ByteSource.Util.bytes("itsource");
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,salt,"myRealm");
return authenticationInfo;
}
/**
* 假设这里是根据用户名进行的查询
* MD5:e10adc3949ba59abbe56e057f20f883e
* MD5+10次:4a95737b032e98a50c056c41f2fa9ec6
* MD5+10次+itsource:831d092d59f6e305ebcfa77e05135eac
*/
public String getUsers(String username){
if("admin".equals(username)){
return "831d092d59f6e305ebcfa77e05135eac";
}else if("zhang".equals(username)){
return "123";
}
return null;
}
}
准备工厂返回权限
这个工厂主要用于返回applicationContext-shiro.xml中需要的权限map,因为我们的权限大多数时候都是存在数据库里面的,我们没有办法在配置文件中获取。但是一个一个的写又太多的了,这时候我们将数据装在这个map里,再在配置文件中获取就好了。
map.put("/**",“authc”);这个一定要放在最后面,因为这个如果放在前面,那么所有的路径都需要登录才能操作,后面的就不会生效了。
/**
* 用于返回下面的这些值(这里的值是有顺序的:LinkedHashMap)
* <value>
/login = anon
/s/permission.jsp = perms[user:index]
/** = authc
</value>
这里修改后要重新启动tomcat
*/
public class ShiroFilterMapFactory {
public Map<String,String> createMap(){
Map<String,String> map = new LinkedHashMap<>();
//anon:需要放行的路径
map.put("/login","anon");
//perms:权限拦截(模拟从数据库获取)
map.put("/s/permission.jsp","perms[employee:index]");
//authc:拦截
map.put("/**","authc");
return map;
}
}
applicationContext-shiro.xml的配置
先在 applicationContext.xml 中引入它 ,不然不会加载这个文件,你也可以用其他的办法。这里涉及到另外一个知识点,就是创建Bean的方法,我这里也有介绍https://blog.csdn.net/qq_36892672/article/details/103542323
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!--
Shiro的核心对象(权限管理器)
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(jpaRealm)
-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="jpaRealm"/>
</bean>
<!--
JpaRealm jpaRealm = new JpaRealm();
配置咱们的自定义realm
-->
<bean id="jpaRealm" class="cn.itsource.aisell.web.shiro.JpaRealm">
<!--Realm的名称-->
<property name="name" value="jpaRealm"/>
<property name="credentialsMatcher">
<!-- 配置哈希密码匹配器 -->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--加密方式:MD5-->
<property name="hashAlgorithmName" value="MD5"/>
<!--迭代次数-->
<property name="hashIterations" value="10" />
</bean>
</property>
</bean>
<!-- 这三个配置好,可以支持注解权限 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!--
shiro真正的过滤器(功能就是它完成的)
这个bean的名称必需和web.xml里的的代理过滤器名字相同
-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--必需要用到权限管理器-->
<property name="securityManager" ref="securityManager"/>
<!--如果你没有登录,你会进入这个页面-->
<property name="loginUrl" value="/s/login.jsp"/>
<!--登录成功后,进入的页面(一般没什么用)-->
<property name="successUrl" value="/s/main.jsp"/>
<!--如果你没有权限,你会进入这个页面-->
<property name="unauthorizedUrl" value="/s/unauthorized.jsp"/>
<!-- 过滤描述
anon:不需要登录也可以访问
authc:登录与权限的拦截
perms:如果你有user:index的权限,你就可以访问:/s/permission.jsp
-->
<!--
<property name="filterChainDefinitions">
<value>
/login = anon
/s/permission.jsp = perms[user:index]
/** = authc
</value>
</property>
-->
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap" />
</bean>
<!--
以前在四个创建Bean的方法中讲过
ShiroFilterMapFactory shiroFilterMapFactory = new shiroFilterMapFactory();
Map filterChainDefinitionMap=shiroFilterMapFactory.createMap();
-->
<!--拿到shiroFilterMapFactory里面的createMap方法的值 -->
<bean id="filterChainDefinitionMap" factory-bean="shiroFilterMapFactory" factory-method="createMap" />
<!--配置返回shiro权限拦截的bean-->
<bean id="shiroFilterMapFactory" class="cn.itsource.aisell.web.shiro.ShiroFilterMapFactory"/>
</beans>