一、权限管理
权限管理包括身份认证和授权两部分,简称认证授权。
对于需要访问控制的资源, 用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
1.1 认证
1.1.1 主体(Subject)
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
1.1.2 身份信息(Principal)
是主体(subject)进行身份认证的标识,标识必须具有唯一性 。如用户名、手机号、邮箱地址等,一个主体可以有多个身份
但是必须有一个主身份(Primary Principal)。
1.1.3 凭证信息 (Credential)
是只有主体自己知道的安全信息,如密码、证书等
1.2 授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,
对于某些资源没有权限是无法访问的。
理解以下专业术语:
授权可以理解为主体(Subject)对系统资源(resource)有什么权限(Permission)
1.2.1 资源(Resource)
系统提供的功能和服务
1.2.2 权限(Permission)
一个主体能够访问哪些资源
1.3 权限模型
对上节中的主体、资源、权限通过数据模型表示。
主体(账号、密码)
资源(资源名称、访问地址)
权限(权限名称、资源id)一个主体对哪些资源有访问权限
角色(角色名称)
角色和权限关系(角色id、权限id)
主体和角色关系(主体id、角色id)
不同的角色 对同一个资源 访问的权限是不一样的
系统管理员(角色) 部门管理(资源) 新增、修改、删除、查看(权限)
普通用户(角色) 部门管理(资源) 查看(权限)
通常企业开发中将资源和权限表合并为一张表,如下:
资源(资源名称、访问地址)
权限(权限名称、资源id)合并为:
资源(权限名称、资源名称、资源访问地址)

2、Shiro简介
Apache Shiro 是 Java 的一个安全框架。很多企业使用 Apache Shiro 作为系统的安全框架
- 原因:使用简单
- 相对Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么 复杂的东西,所以使用小而简单的 Shiro 就足够了
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在 JavaEE 环境
Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与Web应用集成、缓存等
2.1 系统架构
和应用代码直接交互的对象是 Subject ,Shiro的对外API核心就是 Subject ;
其每个API的含义:
2.1.1 Subject(主体)
- 代表了当前 用户 ,这个 用户不一定是一个具体的人
- 与当前应用交互的任何东西都是 Subject ,如果门禁卡、指纹、U盾等
- 所有 Subject 都绑定到 SecurityManager ,与 Subject 的所有交互都会委托给 SecurityManager
2.1.2 SecurityManager(安全管理器)
- 即所有与安全有关的操作都会与 SecurityManager 交互
- 安全管理器管理所有 Subject ,它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互
2.1.3 Realm
- Shiro 从 Realm 获取安全数据(如用户、角色、权限),
- SecurityManager 要认证用户身份,需要从 Realm 获取用户信息进行比较以确定用户身份是否合法;
- SecurityManager 要对用户授权检查,需要从 Realm 得到用户相应的角色/权限验证用户是否能进行操作;
2.1.4 总结
- 应用代码通过 Subject 来进行认证和授权,而Subject又委托给 SecurityManager
- 我们需要给 Shiro 的 SecurityManager 注入 Realm ,从而让 SecurityManager 能得到合法的用户及其权限进行判断
2.2 核心对象

2.2.1 Subject(主体)
从上图可以看到,主体可以是任何可以与应用交互的 用户 Subject 通过 SecurityManager 安全管理器进行认证授权
2.2.2 SecurityManager(安全管理器)
所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、负责进行认证和授权、及会话、缓存的管理。
2.2.3 Authenticator (认证器)
对主体(用户)身份进行认证
2.2.4 Authorizer(授权器)
用户通过认证器认证通过后, 用来决定主体是否有权限进行相应的操作即控制着用户能访问应用中的哪些功能
2.2.5 Realm
Realm是一个接口,有多个实现类
SecurityManager 进行安全认证需要通过 Realm 获取用户权限数据
Shiro 不知道你的用户/权限存储在哪及以何种格式存储?(有可能以.ini配置文件存储,也有可能存储在数据库)
所以我们一般在应用中都需要实现自己的Realm
2.2.6 SessionManager(会话管理器 )
- shiro 框架定义了一套会话管理,它不依赖web容器的session(与httpSession是不一样的)
- 所以 shiro 可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理
- 此特性可使它实现单点登录。
2.2.7 SessionDAO
- DAO,数据访问对象,用于会话的 CRUD
- 如果把 Session 保存到数据库,可以实现自己的 SessionDAO,通过如 JDBC 写到数据库
- 如果把 Session 放到 Redis 中,可以实现自己的Redis SessionDAO
- 另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能
2.2.8 CacheManager(缓存管理)
- 用来管理如用户、角色、权限等的缓存信息
- 因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
2.2.9 Cryptography(密码管理)
密码模块,Shiro提供了一些常见的加密/解密组件用于如密码加密,方便开发,比如提供常用的散列、加/解密的功能。
3、Shiro实战
3.1 环境搭建
3.1.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.wyu</groupId>
<artifactId>i-shiro</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<!-- <dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.12.1</version>
</dependency>-->
<!--用于slf4j与log4j2保持桥接 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.12.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3.1.2 log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--先定义所有的appender-->
<appenders>
<!--输出控制台的配置-->
<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式-->
<!--<patternlayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%p] %c %m %n"/>-->
<patternlayout pattern="[%p] %m %n"/>
</console>
</appenders>
<!-- 然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
<!-- 日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<loggers>
<root level="DEBUG">
<!--输出到控制台-->
<appender-ref ref="Console"/>
</root>
<!--org.springframework
<logger name="org.springframework" level="INFO"/>-->
</loggers>
</configuration>
3.1.3 shiro.ini

[users]
zhangsan=123456,admin
lisi=654321,public
[roles]
admin=product:view,product:create,product:update,product:delete
public=public:view
3.2 测试代码
package cn.wyu.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
/**
* @author linwillen
* @create 2020-05-03-15:41
*/
public class ShiroTest {
@Test
public void test1(){
//1.初始化shiro的安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2.设置用户权限信息到安全管理器
Realm realm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(realm);
//3.使用SecurityUtils将securityManager设置到运行环境中
SecurityUtils.setSecurityManager(securityManager);
//4.创建Subject实例
Subject subject = SecurityUtils.getSubject();
//5.创建用于认证的token,记录用户认证的身份和凭证即账号和密码
//org.apache.shiro.authc.UnknownAccountException:账号错误
//org.apache.shiro.authc.IncorrectCredentialsException:密码(凭证)错误
AuthenticationToken token = new UsernamePasswordToken("zhangsan","123456");
System.out.println("用户的认证状态:"+subject.isAuthenticated());
//6.主体进行登录,登录时进行认证检查
subject.login(token);
System.out.println("用户的认证状态:"+subject.isAuthenticated());
//7.检查用户的授权状态
System.out.println("是否拥有admin角色:"+subject.hasRole("admin"));
System.out.println("是否拥有public角色:"+subject.hasRole("public"));
//8.检查权限的授权状态
System.out.println("是否拥有product:view:"+subject.isPermitted("product:view"));
//9.获取主体信息
System.out.println("用户名:"+subject.getPrincipal());
//System.out.println("密码:"+subject.getPrincipal());//密码不能用subject获取
//10.退出
subject.logout();
System.out.println("用户的认证状态:"+subject.isAuthenticated());
}
}
3.3 封装登录

3.3.1 ShiroUtil.java
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
/**
* @author linwillen
* @create 2020-05-03-17:01
*/
public class ShiroUtil {
/**
* 初始化Shiro运行环境
*/
static {
//1.初始化shiro的安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2.设置用户权限信息到安全管理器
Realm realm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(realm);
//3.使用SecurityUtils将securityManager设置到运行环境中
SecurityUtils.setSecurityManager(securityManager);
//login();
}
public static Subject login(String username,String password) {
//1.创建Subject实例
Subject subject = SecurityUtils.getSubject();
//2.创建用于认证的token,记录用户认证的身份和凭证即账号和密码
//org.apache.shiro.authc.UnknownAccountException:账号错误
//org.apache.shiro.authc.IncorrectCredentialsException:密码(凭证)错误
AuthenticationToken token = new UsernamePasswordToken(username,password);
//3.主体进行登录,登录时进行认证检查
subject.login(token);
System.out.println("用户的认证状态:"+subject.isAuthenticated());
return subject;
}
}
3.3.2 测试
@Test
public void test2(){
//登录认证
Subject subject = ShiroUtil.login("zhangsan", "123456");
//授权资源的检查
System.out.println("是否拥有admin角色:"+subject.hasRole("admin"));
//退出
subject.logout();
}
3.4 自定义Realm
上边的程序使用的是 Shiro 自带的 IniRealm , IniRealm 从 ini 配置文件中读取用户的信息,
大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义 realm

最基础的是Realm接口, CachingRealm 负责缓存处理, AuthenticationRealm 负责认证,
AuthorizingRealm 负责授权,通常自定义的realm 继承 AuthorizingRealm 。

3.4.1 ShiroRelam
package cn.wyu.sys.shiro;
import cn.wyu.sys.bean.User;
import org.apache.shiro.authc.*;
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 java.util.HashSet;
import java.util.Set;
/**
* @author linwillen
* @create 2020-05-03-17:32
*/
public class ShiroRealm extends AuthorizingRealm {
/**
* 登录认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("登录认证....");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//1.获取用户名
String username = token.getUsername();
//2.获取密码
String password = new String(token.getPassword());
//3.根据用户名跟密码去数据库中查找对象
User user = new User("zhangsan", "123456");
if (!user.getUsername().equals(username)) {
throw new UnknownAccountException("用户名错误...");
}
if (!user.getPassword().equals(password)) {
throw new CredentialsException("密码错误...");
}
System.out.println("认证成功....");
//创建简单认证信息对象
//public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
SimpleAuthenticationInfo info =
new SimpleAuthenticationInfo(token.getPrincipal(),token.getCredentials(),super.getName());
return info;
}
/**
* 授权
* @param principals
* @return
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("授权...");
String username = principals.getPrimaryPrincipal().toString();
// 通过用户名去数据库查询该用户有哪些角色和权限
// 模拟通过用户名去数据库查询该用户有哪些角色
Set<String> roleNameSet = new HashSet<String>();
roleNameSet.add("系统管理员");
roleNameSet.add("系统运维");
// 模拟通过用户名去数据库查询该用户有哪些权限
Set<String> permissionNameSet = new HashSet<String>();
permissionNameSet.add("sys:user:list");//查看列表
permissionNameSet.add("sys:user:info");//查看用户详情
permissionNameSet.add("sys:user:create");//创建用户
permissionNameSet.add("sys:user:update");//修改用户
permissionNameSet.add("sys:user:delete");//删除用户
// 创建简单授权信息对象,对象中包含包含用户的角色和权限信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roleNameSet);
info.addStringPermissions(permissionNameSet);
System.out.println("授权完成...");
return info;
}
}
3.4.2 ShiroUtil
将读取配置文件的 IniRealm 修改为自定义 自定义的 ShiroRealm

3.4.3 User类

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* @author linwillen
* @create 2020-05-03-17:53
*/
@Setter
@Getter
@ToString
@AllArgsConstructor
public class User {
private String username;
private String password;
}
3.5 常用异常类
org.apache.shiro.authc.UnknownAccountException 用户名不存在
org.apache.shiro.authc.CredentialsException 凭证不合法(密码错误)
org.apache.shiro.authc.DisabledAccountException 账号被禁用
org.apache.shiro.authc.LockedAccountException 账号被锁定
org.apache.shiro.authc.ExpiredCredentialsException 凭证过期(登录超时)
org.apache.shiro.authc.AuthenticationException 认证异常
3.5.1 修改User类


注:认证通过后才会授权,如果认证过程出现异常,是不会执行授权方法的。
3.6 密码加密
3.6.1 散列算法
散列算法一般用于生成一段文本的摘要信息,将内容可以生成摘要,无法将摘要转成原始内容。
- 散列算法不可逆
- 散列算法常用于对密码进行散列
- 常用的散列算法有 MD5、SHA
一般散列算法需要提供一个 salt (盐)与原始内容生成摘要信息,这样做的目的是为了安全性
比如:111111 的 md5值是:96e79218965eb72c92a549dd5a330112,
拿着 96e79218965eb72c92a549dd5a330112 去md5破解网站很容易进行破解
如果要是对111111和salt(盐,一个随机数)进行散列,这样虽然密码都是111111,
但是 加不同的盐会生成不同的散列值 。
3.6.2 加密工具类
import org.apache.shiro.crypto.hash.Md5Hash;
/**
* @author linwillen
* @create 2020-05-03-20:12
*/
public class MD5Util {
// 散列次数
private static int hashIterations = 3;
private static String public_salt = "lwlcxs";
/**
* md5加密工具类
* @param source 要用共盐加密的字符串
* @return
*/
private static String md5_public_salt(String source) {
return new Md5Hash(source, public_salt, hashIterations).toString();
}
/**
*
* @param source 原始密码
* @param salt 用户的私盐
* @return
*/
public static String md5_private_salt(String source, String salt) {
return new Md5Hash(md5_public_salt(source), salt, hashIterations).toString();
}
}
3.7 缓存授权信息
每次执行hasRole方法的时候,ShiroRealm中授权检查的方法都会执行,没有使用缓存也就意味这要调用两次数据库查询用户的权限
使用缓存可以实现只有在用户认证成功的时候调用一下授权方法,后续不再调用该方法。
我们使用ehcache作缓存.
3.7.1 使用内置缓存

3.7.2 使用 ehcache
pom.xml 引入shiro对ehcache的支持包和ehcache包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.8.0</version>
</dependency>

