Shiro
一、简介
1. 什么是权限管理
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理能实现对用户访问系统的控制,按照安全规则或者安全策略限制用户操作,只允许用户访问被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。
- 身份认证:
就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
- 授权:
即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
2. 什么是Shiro
- Apache Shiro 是 Java 的一个安全(权限)框架。
- Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在 JavaEE 环境。
- Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存等。
- 官网:http://shiro.apache.org/
3. 功能介绍
-
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
-
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用
户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户
对某个资源是否具有某个权限; -
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有
信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的; -
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
-
Web Support:Web 支持,可以非常容易的集成到Web 环境;
-
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可
以提高效率; -
Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
-
Testing:提供测试支持;
-
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
-
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登
录了
4. 架构
从应用层面看Shiro的架构,有三个重要的组件:Subject,SecurityManager,Realm
- Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外API 核心就是 Subject。Subject 代表了当前“用户”, 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;与 Subject 的所有交互都会委托给 SecurityManager;Subject 其实是一个门面,SecurityManager 才是实际的执行者;
- SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager 交互;且其管理着所有 Subject;可以看出它是 Shiro的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中
DispatcherServlet 的角色 - Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource
详细架构内容参考 http://shiro.apache.org/architecture.html
二、认证
1. 认证流程
2. 使用ini认证
步骤:
-
新建maven工程,pom.xml增加shiro相关依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.2</version> </dependency>
-
增加shiro.ini文件,里面存储合法用户的用户名和密码
[users] obj=123 zhangsan=111
-
编写测试代码
@Test public void testLogin(){ Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("obj", "1234"); try { subject.login(token); }catch (UnknownAccountException e) { System.out.println("帐号不存在,登录失败!"); }catch (IncorrectCredentialsException e) { System.out.println("密码错误,登录失败!"); } System.out.println("是否登录成功:" + subject.isAuthenticated()); subject.logout(); System.out.println("是否登录成功:" + subject.isAuthenticated()); }
认证常见的异常:
3. 自定义Realm认证
步骤:
- 编写自己的Realm,继承类AuthorizingRealm,重写3个方法:getName,doGetAuthorizationInfo,doGetAuthenticationInfo
public class MyRealm extends AuthorizingRealm {
@Override
public String getName() {
return "MyRealm";
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("正在获取认证信息");
String username = (String)token.getPrincipal();
if(!"zhangsan".equals(username)) {
return null;
}
String password = "123";
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName());
return info;
}
}
-
在shiro-realm.ini文件中指定使用自定义的Realm
myRealm=com.veryoo.shiro.realm.MyRealm securityManager.realms=$myRealm
-
编写测试代码
@Test public void testRealmLogin(){ Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123"); try { subject.login(token); }catch (UnknownAccountException e) { System.out.println("帐号不存在,登录失败!"); }catch (IncorrectCredentialsException e) { System.out.println("密码错误,登录失败!"); } System.out.println("是否登录成功:" + subject.isAuthenticated()); subject.logout(); System.out.println("是否登录成功:" + subject.isAuthenticated()); }
4. 密码加密
加密算法
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和ID(即盐);这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难破解。
@Test
public void testMD5() {
String password = "123";
//md5直接加密
Md5Hash md5 = new Md5Hash(password);
System.out.println(md5);
//加盐
md5 = new Md5Hash(password, "thisisasalt");
System.out.println(md5);
//增加散列次数
md5 = new Md5Hash(password, "thisisasalt", 3);
System.out.println(md5);
}
步骤
-
自定义加密之后realm: 重写3个方法:getName doGetAuthorizationInfo doGetAuthenticationInfo
public class MyRealm extends AuthorizingRealm { @Override public String getName() { return "MyRealm"; } //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // TODO Auto-generated method stub return null; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("正在获取认证信息"); String username = (String)token.getPrincipal(); if(!"zhangsan".equals(username)) { return null; } String password = "619051321d4386ede71a2b58c6e4fd59"; SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes("thisisasalt"), getName()); return info; } }
-
shiro-crypto.ini中定义加密规则
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher credentialsMatcher.hashAlgorithmName=md5 credentialsMatcher.hashIterations=3 myRealm = com.veryoo.shiro.realm.MyRealm myRealm.credentialsMatcher=$credentialsMatcher securityManager.realms=$myRealm
-
编写测试代码
@Test public void testCryptoLogin(){ Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-crypto.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123"); try { subject.login(token); }catch (UnknownAccountException e) { System.out.println("帐号不存在,登录失败!"); }catch (IncorrectCredentialsException e) { System.out.println("密码错误,登录失败!"); } System.out.println("是否登录成功:" + subject.isAuthenticated()); subject.logout(); System.out.println("是否登录成功:" + subject.isAuthenticated()); }
三、授权
1. 授权流程
2. RBAC模型介绍
RBAC 是基于角色的访问控制(Role-Based Access Control )。在 RBAC 中,权限与角色相关联,用户通过被赋予适当角色而得到这些角色的权限。简单理解为:谁扮演什么角色, 被允许做什么操作
用户对象:user: 当前操作用户
角色对象:role:表示权限操作许可权的集合
权限对象:permission: 资源操作许可权
扩展学习:https://www.cnblogs.com/niuli1987/p/9871182.html
3. 授权方式
Shiro中提供三种方式进行授权:
-
编程方式
Subject subject = SecurityUtils.getSubject(); if(subject.hasRole("admin")) { //有权限 } else { //无权限 }
-
注解方式
@RequiresRoles("admin") @RequiresPermission("employee:save") public void save() { //有权限 }
-
JSP标签方式
<shiro:hasRole name="admin"> <!— 有权限 —> </shiro:hasRole> <shiro:hasPermission name="employee:list"> <a href="/employee">员工列表</a><br> </shiro:hasPermission>
4. 角色和权限检查
步骤:
-
shiro-permission.ini配置用户角色和权限
[users] zhangsan=123,role1,role2 lisi=888,role2 [roles] role1=user:create,user:update role2=user:create,user:delete role3=user:create
2. 编写测试
```java
@Test
public void testAuthor(){
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", "123");
subject.login(token);
System.out.println("是否登录成功:" + subject.isAuthenticated());
System.out.println(subject.isPermitted("user:update"));
System.out.println(subject.isPermittedAll("user:create", "user:update"));
System.out.println(Arrays.toString(subject.isPermitted("user:update", "user:create")));
subject.checkPermission("user:list");
// System.out.println(subject.hasRole("role1"));
// System.out.println(subject.hasRole("role3"));
// System.out.println(subject.hasAllRoles(Arrays.asList("role1", "role2")));
// System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("role1", "role2", "role3"))));
//
//
subject.checkRole("role3");
// subject.checkRoles("role1","role3");
}
5. 自定义Realm授权
步骤:
-
编写自定义Realm
public class MyRealm extends AuthorizingRealm { @Override public String getName() { return "MyRealm"; } //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String)principals.getPrimaryPrincipal(); List<String> roles = new ArrayList<String>(); roles.add("role1"); List<String> permissions = new ArrayList<String>(); permissions.add("user:create"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRoles(roles); info.addStringPermissions(permissions); return info; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("正在获取认证信息"); String username = (String)token.getPrincipal(); if(!"zhangsan".equals(username)) { return null; } String password = "123"; SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName()); return info; } }
-
shiro-realm.ini 配置使用自定义realm
myRealm=com.veryoo.shiro.realm.MyRealm securityManager.realms=$myRealm
-
编写测试代码
@Test public void testAuthor(){ Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123"); subject.login(token); System.out.println("是否登录成功:" + subject.isAuthenticated()); System.out.println(subject.hasRole("role1")); System.out.println(subject.hasRole("role2")); System.out.println(subject.isPermitted("user:create")); System.out.println(subject.isPermitted("user:update")); }
四、Spring集成
1. 项目准备
pom.xml增加shiro依赖
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</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>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- aspectj -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.7.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency>
2. shiro默认过滤器
在web.xml中增加shiro过滤器
<!-- 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>
3. shiro配置
增加spring-shiro.xml
<?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: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/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置自定义的realm-->
<bean id="userRealm" class="com.zryx.car4s.shiro.UserRealm">
</bean>
<!-- 配置安全管理器SecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm"/>
</bean>
<!-- 定义ShiroFilter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/>
<property name="filterChainDefinitions">
<value>
/logout=logout
/css/**=anon
/fonts/**=anon
/images/**=anon
/js/**=anon
/**=authc
</value>
</property>
</bean>
<!-- 开启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>
</beans>
spring主配置中导入shiro的配置
<import resource="classpath*:spring-shiro.xml" />
spring-mvc.xml中也要导入
<import resource="classpath*:spring-shiro.xml" />
4. 认证
自定义UserRealm提供认证信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null) {
return null;
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
return info;
}
修改登录login的验证规则
@PostMapping("/login")
public String doLogin(HttpServletRequest req) {
String exceptionClassName = (String)req.getAttribute("shiroLoginFailure");
if(exceptionClassName != null) {
req.setAttribute("msg", "用户名/密码错误");
}
return "login";
}
去掉springmvc的登录拦截器
5. 注解授权
springmvc.xml的全局异常控制增加无权限的异常类型
<prop key="org.apache.shiro.authz.UnauthorizedException">error/nopermission</prop> <!--shiro权限异常处理-->
6. JSP标签权限控制
7. 数据库授权
8. 缓存管理
9. 密码加密
<!--配置自定义的realm-->
<bean id="userRealm" class="com.zryx.car4s.shiro.UserRealm">
<!--密码需要加密:加密器-->
<property name="credentialsMatcher" ref="credentialsMatcher" />
<property name="userService" ref="userServiceImpl"></property>
<property name="roleService" ref="roleServiceImpl"></property>
</bean>
<!--加密器-->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--加密算法-->
<property name="hashAlgorithmName" value="md5" />
<!--散列次数-->
<property name="hashIterations" value="3" />
</bean>
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
user,
user.getPassword(),
ByteSource.Util.bytes("this_is_a_salt"),
getName());
@Test
public void testMD5() {
String password = "1";
//md5直接加密
Md5Hash md5 = new Md5Hash(password);
System.out.println(md5);
//加盐
md5 = new Md5Hash(password, "this_is_a_salt");
System.out.println(md5);
//增加散列次数
md5 = new Md5Hash(password, "this_is_a_salt", 3);
System.out.println(md5);
}