shiro笔记
shiro的基本概念
- application code: 应用程序代码,代表的一个shiro的启动入口,用shiro的api来启动,可以理解成把用户的数据用shiro的api传递个shiro,由shiro来处理用户数据。
- subject: 原意是主体,每一个subject代表每一个subject代表的一个用户,抽象的用户,就是用shiro对用户传来的数据进行封装,把数据封给token(令牌),最终可以狭义理解为数据。
- securitymanager: 安全管理中心,是shiro的核心, 所有的数据都要经过shiro的安全管理中心。
- realm:域,可以理解为数据的源头,可以是数据库,文件等。
shiro的原理
- shiro的登录认证
- 用户登录成功就可以访问敏感资源
- 之后的所有访问都通过shiro直接访问指定的资源
- 如果没有登录成功,跳转到指定的登录页面
public Object around(ProceedJoinPoint pjp){ Object returnValue = null; try{ if(登录过){ returnValue = pjp.proceed(); }else{ // 由shiro控制跳转到指定的页面,由pring-shiro.xml提供跳转的 } } } // 注意:没有shiro项目的功能照样跑起来,添加shiro实际就是横切,把shiro横切 // 实际是代理模式
- shiro的权限认证
- 在登录认证完成之后,根据用户的权限显示不同的菜单项
shiro项目的搭建步骤
- 创建项目
- 导入jar
- 手动导入
- maven导入
<!-- Apache Shiro依赖 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>1.2.3</version> </dependency>
- 把shiro的对象交给spring容器来管理
- spring-shiro.xml(名字任意)
- web.xml
- 添加shiro的过滤器,要用这个过滤器过滤所有的url
- 配置所有的shiro的安全管理中心的对象
- 配置shiro的过滤器对象
- 配置登录和权限认证的对象
- 创建java类,实现相应接口
Shiro之HelloWorld
/**
* 用ini文件模拟数据库,测试Shiro的认证
*/
public class ShiroTest {
@Test
public void testLogin() throws Exception{
// 1. 创建securityManager工厂对象:加载配置文件,创建工厂对象
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// 2. 通过工厂对象,创建SecurityManager对象
SecurityManager securityManager = factory.getInstance();
// 3. 将securityManager绑定到当前运行环境中,让系统随时随地都可以访问该对象
SecurityUtils.setSecurityManager(securityManager);
// 4. 创建当前登录的主体,注意,此时主体没有经过认证
Subject subject = SecurityUtils.getSubject();
// 5. 收集主体登录的身份/凭证,即账号密码
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666");
// 6. 主体登录
subject.login(token);
// 7. 判断登录是否成功, 成功返回true,失败返回false
System.out.println("验证登录是否成功:" + subject.isAuthenticated());
// 8. 注销
subject.logout();
System.out.println("验证是否登录成功(退出之后返回false):" + subject.isAuthenticated());
}
}
运行结果:
验证登录是否成功:true
验证是否登录成功(退出之后返回false):false
- login方法可能会抛出两个异常
- 主体账号不正确:抛出org.apache.shiro.authc.UnknownAccountException
- 主体密码不正确:抛出org.apache.shiro.authc.IncorrectCredentialsException
模拟真实地从数据库查询对比验证登录之自定义Realm方式
public class MyRealm extends AuthorizingRealm {
// 授权操作
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证操作
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println(authenticationToken);
String username = (String)authenticationToken.getPrincipal();
if(!"zhangsan".equals(username)){
return null;
}
String password="666";
// info对象表示realm登录对比信息,参数1:用户信息,参数2:表示密码,参数:3:当前ralms的名字
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName());
return info;
}
// 指定当前自定义Realm的名字
public String getName() {
return "MyRealm";
}
}
自定义ini配置文件
# 自定义realm
myRealm=com.wyq.shiro.realm.MyRealm
# 指定securityManager的realms实现
securityManager.realms=$myRealm
Shiro执行流程
- 构造SecuritiManager环境
- Subject.login(token)提交认证
- securitiManager.login()执行认证
- Authenticator执行认证
- Realm根据身份获取认证信息
Shiro加密
- 散列算法:一般用于生成数据的摘要信息,是一种不可逆的算法,适合存储密码之类的数据,常见的散列算法有MD5、SHA等。
- Shiro之MD5加密
@Test
public void testMD5() throws Exception{
String password = "666";
// 加密
Md5Hash md5Hash = new Md5Hash(password);
System.out.println(md5Hash);
// 加密:md5 + salt
md5Hash = new Md5Hash(password, "salt");
System.out.println(md5Hash);
// 加密:md5 + salt + 散列次数
md5Hash = new Md5Hash(password, "salt", 3);
System.out.println(md5Hash);
}
-
Shiro之加密算法认证
- 自定义Realm类并继承AuthorizingRealm重写抽象方法
public class PasswordRealm extends AuthorizingRealm { // 授权 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } // 认证 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 参数authenticationToken:表示登录时包装的UsernamePasswordToken // 通过用户名到数据库中查用户信息,封装成一个AuthonticationInfo对象返回,方便认证器进行对比 // 获取token中的用户名 String username = (String)authenticationToken.getPrincipal(); if(!"zhangsan".equals(username)){ return null; } // 模拟数据库中保存之后密文 String password="c2fde5f6da6c08a5f4da78d5fd803bcd"; // info对象表示realm登录对比信息,参数1:用户信息,参数2:表示密码,参数3:表示加密的salt, 参数:4:当前ralms的名字 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password , ByteSource.Util.bytes("salt"), getName()); return info; } @Override public String getName() { return "PasswordRealm"; } }
- 添加ini配置文件
[main] # 定义凭证匹配器 credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher # 散列算法 credentialsMatcher.hashAlgorithmName=md5 # 散列次数 credentialsMatcher.hashIterations=3 # 自定义realm myRealm=com.wyq.shiro.realm.PasswordRealm myRealm.credentialsMatcher=$credentialsMatcher # 指定securityManager的realms实现 securityManager.realms=$myRealm
Shiro权限管理
授权
- RBAC:基于角色的权限管理
- 用户对象:user-表示当前操作用户
- 角色对象:role-表示权限操作许可权的集合
- 权限对象:permission-资源操作许可权
权限管理的方式
- 编程方式
- 通过写if/else授权代码完成
Subject subject = SecurityUtils.getSubject(); if(subject.hasRole("admin")){ // 有权限 }else{ // 无权限 }
- 注解方式
- 通过在执行的java方法上放置相应的注解完成
@RequiresRoles("admin") public void help(){ }
- JSP标签方式
- 在jsp页面通过相应的标签完成
// 先引入标签库 <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <shiro:hasRole name="admin"> <!-- 有权限 --> </shiro:hasRole>
权限表达式
- 在ini文件中用户、角色、权限的配置规则是:“用户名=密码,角色1,角色2…” “角色=权限1,权限2…”,首先根据用户名找角色,根据角色找权限,角色是权限的集合
- 权限字符串的规则是:“资源标识符:操作:资源实例标识符”,意思是对哪个资源的哪个实例具有什么操作,":"是资源/操作/实例的分割父,权限字符串也可以使用*统配符。
- 例子
- 用户创建权限:user:create,或user:create:*
- 用户修改权限实例001的权限:user:update:001
- 用户实例001的所有权限:user:*:001
- 一般而言,我们操作只需要关注前面两节
- 资源: 操作
权限管理之HelloWorld
@Test
public void testHasRole()throws Exception{
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666");
subject.login(token);
// 进行授权操作前提:用户必须通过认证
System.out.println("是否拥有role1权限:" + subject.hasRole("role1"));
System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2", "role3")));
System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("role1", "role2", "role3"))));
// 如果拥有角色不做任何操作,没有则抛出org.apache.shiro.authz.UnauthorizedException异常
subject.checkRole("role1");
// 判断是否拥有创建权限
System.out.println(subject.isPermitted("user:create"));
}
[users]
# 模拟数据库用户列表,账号=密码
zhangsan=666,role1,role2
lisi=888,role2
[roles]
# 角色role1对资源user拥有create,update的权限
role1=user:create,user:update
# 角色role2对资源user拥有create,delete的权限
role2=user:create,user:delete
# 角色role3对资源user拥有create的权限
role3=user:create
自定义Realm
- 添加配置文件
# 自定义realm
myRealm=com.wyq.shiro.realm.PermissionRealm
# 指定securityManager的realms实现
securityManager.realms=$myRealm
- 创建自定义认证类
public class PermissionRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// principalCollection 用户的凭证信息
String username = (String)principalCollection.getPrimaryPrincipal();
// 模拟查询数据库: 查询用户实现指定的角色,以及用户授权
List<String> roles = new ArrayList<>();
List<String> permission = new ArrayList<>();
roles.add("role1");
permission.add("user:*");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles);
info.addStringPermissions(permission);
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String)authenticationToken.getPrincipal();
if(!"zhangsan".equals(username)){
return null;
}
String password="666";
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
return simpleAuthenticationInfo;
}
public String getName() {
return "PermissionRealm";
}
}
- 测试
@Test
public void testHasRoleByRealm()throws Exception{
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-permission-realm.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "666");
subject.login(token);
// 判断是否拥有某个权限
System.out.println(subject.isPermitted("user:xx"));
}
Shiro之web集成
- Shiro与web集成,主要是通过配置一个shiroFilter过滤器拦截所有的url,其中ShiroFilter类似于SpringMVC的DispatcherServlet,是所有请求点的入口,负责根据配置(ini文件)判断请求进入rul是否需要登录/权限等工作。
- 集成步骤
- 添加jar包
<dependency> <!-- shiro核心jar包 --> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <!-- shiro与web集成的jar包 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.3</version> </dependency>
- 配置web.xml
<context-param> <param-name>shiroEnvironmentClass</param-name> <param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value> </context-param> <context-param> <param-name>shiroConfigLocations</param-name> <param-value>classpath:shiro.ini</param-value> </context-param> <listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
- 添加shiro.ini文件
处理登录认证[main] #默认是/login.jsp authc.loginUrl=/login #用户无需要的角色时跳转的页面 roles.unauthorizedUrl=/nopermission.jsp #用户无需要的权限时跳转的页面 perms.unauthorizedUrl=/nopermission.jsp #登出之后重定向的页面 logout.redirectUrl=/login [users] admin=666,admin zhangsan=666,deptMgr [roles] admin=employee:*,department:* deptMgr=department:view [urls] #静态资源可以匿名访问 /static/**=anon #访问员工列表需要身份认证及需要拥有admin角色 /employee=authc,roles[admin] #访问部门列表需要身份认证及需要拥有department:view的权限 /department=authc,perms["department:view"] #当请求loginOut,会被logout捕获并清除session /loginOut=logout #所有的请求都需要身份认证 /**=authc
@WebServlet(name = "loginServlet", urlPatterns = "/login") public class LoginServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String exceptionClassName = (String)req.getAttribute("shiroLoginFailure"); // 不处理登录成功的情况,shiro认证会自动跳转到上一个请求路径 // 处理登录失败的情况 if(exceptionClassName != null) { if(UnknownAccountException.class.getName().equals(exceptionClassName)) { req.setAttribute("errorMsg", "账号不存在"); }else if(IncorrectCredentialsException.class.getName().equals(exceptionClassName)) { req.setAttribute("errorMsg", "用户名或密码错误"); }else { req.setAttribute("errorMsg", "未知的登录异常"); } } // 登录失败还是跳回login页面 req.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(req, resp); } }
Shiro与Spring整合
- spring依赖
<!--shiro-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.8</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>1.2.2</version>
</dependency>
<!-- shiro与spring整合jar包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--编码过滤器-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- shiro过虑器,DelegatingFilterProx会从spring容器中找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>
</web-app>
- 添加shiro-spring配置文件(注意引入该文件,不然报找不到shiroFilter错误)
<?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: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">
<!--加密器-->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--加密算法-->
<property name="hashAlgorithmName" value="md5" />
<!--散列次数-->
<property name="hashIterations" value="3" />
</bean>
<!-- 登录验证 -->
<!-- 1. 配置自定义的realm -->
<bean id="userRealm" class="cn.wolfcode.shiro.realm.UserRealm">
<!-- 密码需要加密: 加密器 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="userDAO" ref="userDAOImpl"/>
<property name="roleDAO" ref="roleDAOImpl"/>
<property name="permissionDAO" ref="permissionDAOImpl"/>
</bean>
<!-- Shiro之缓存管理 -->
<bean id="ehCacheManager" class ="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:shiro-ehcache.xml" />
<property name="shared" value="true"></property>
</bean>
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="ehCacheManager"/>
</bean>
<!-- 2. 配置安全管理器SecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm"/>
<property name="cacheManager" ref="cacheManager"/> <!-- 给userRealm添加缓存机制 -->
</bean>
<!-- 3. 定义ShiroFilter, id必须跟web.xml中配置代理filter的filtername的值一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/><!-- 登录失败跳转的虚拟路径 -->
<property name="unauthorizedUrl" value="/nopermission.jsp"/> <!-- 没有权限时跳转的路劲 -->
<property name="filterChainDefinitions"> <!-- 所有的请求都必须经过这个过滤器 -->
<value>
/logout=logout
/**=authc
</value>
</property>
</bean>
<!-- 权限控制 -->
<!-- 开启spring的aop,对类代理(默认是对接口代理) -->
<aop:config proxy-target-class="true"></aop:config>
<!-- 开启shiro注解支持 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!-- shiro权限异常处理,springMVC默认会处理异常,shiro即使配置了也不会起作用,必须开启shiro异常处理机制 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.apache.shiro.authz.UnauthorizedException">redirect:/nopermission.jsp</prop>
</props>
</property>
</bean>
- 配置自定义Realm
public class UserRealm extends AuthorizingRealm {
@Setter
private IUserDAO userDAO;
@Setter
private IRoleDAO roleDAO;
@Setter
private IPermissionDAO permissionDAO;
//认证操作
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从token中获取登录的用户名, 查询数据库返回用户信息
String username = (String) token.getPrincipal();
User user = userDAO.getUserByUsername(username);
SimpleAuthenticationInfo info = null;
if(username != null){
info = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getUsername()), getName());
}
return info;
}
//授权操作
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
// 根据用户id查询所有角色
List<String> roles = roleDAO.getRoleSnByUserId(user.getId());
// 根据用户id查询该用户拥有的权限
List<String> permissions = permissionDAO.getPermissionResourceByUserId(user.getId());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissions);
info.addRoles(roles);
return info;
}
@Override
public String getName() {
return "UserRealm";
}
//清除缓存
public void clearCached() {
//获取当前等的用户凭证,然后清除
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
}
- 权限控制
- 在需要权限控制的方法上贴上权限标签:(此处仅仅讨论基于权限表达式为空:permission)
Shiro常用的一些JSP标签
<!-- 引入shiro标签库 -->
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!-- 显示登录的用户名 -->
<shiro:principal property="username"></shiro:principal>
<!-- 验证是否拥有某一个角色 -->
<shiro:hasRole name="管理员">
用户拥有角色
</shiro:hasRole>
<!-- 如果该登录用户有对应的权限则显示标签内容,否则不显示 -->
<shiro:hasPermission name="department:list">
用户拥有该权限
</shiro:hasPermission>