1. 权限的管理
1.1 什么是权限管理
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
1.2 什么是身份认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
1.3 什么是授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的
2. 什么是shiro
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
3. shiro的核心架构

3.1 Subject:主体
外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。
Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
3.2 SecurityManager:安全管理器
对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
3.3 Authenticator:认证器
对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
3.4 Authorizer:授权器
用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
3.5 Realm:领域
相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
 注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
3.6 SessionManager:会话管理
shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
3.7 SessionDAO:会话dao
是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
3.8 CacheManager:缓存管理
将用户权限数据存储在缓存,这样可以提高性能。
3.9 Cryptography:密码管理
shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
4. shiro中的认证
4.1 认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
4.2 shiro中认证的关键对象
4.2.1 Subject:主体
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
4.2.2 Principal:身份信息
是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
4.2.2 credential:凭证信息
是只有主体自己知道的安全信息,如密码、证书等。
4.3 认证流程

- shiro将用户名(身份信息)和密码(凭证信息)打包成一个token(令牌)
- 然后通过token去通过shiro核心架构中的安全管理器进行认证
- 安全管理器调用认证器,认证器调用任务去获取数据
- 如果获取到的数据与系统存储(数据库等)的一致,则认证通过,否则认证失败
4.4 认证的简单开发
4.4.1 引入依赖
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.5.3</version>
</dependency>
4.4.2 引入shiro配置文件
配置文件:名称随意,以 .ini 结尾,放在 resources 目录下;后续用了springboot就不需要此配置文件了,是前期方便我们学习shiro书写我们系统中相关权限数据
注意:在实际的项目开发中并不会使用这种方式,这种方法可以用来初学时练手
[users]
zhangsan=123456
lisi=456789
4.4.3 开发认证代码
public class test01 {
    public static void main(String[] args) {
        //1.创建安全管理器对象
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        //2.给安全管理器设置realm
        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        //3.SecurityUtils给全局安全工具类设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        //4.关键对象subject主体
        Subject subject = SecurityUtils.getSubject();
        //5.创建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123456");
        try {
            System.out.println("认证状态"+subject.isAuthenticated());//fasle
            //用户认证
            subject.login(token);
            System.out.println("认证状态"+subject.isAuthenticated());
        }catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("认证失败,用户名不存在");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("认证失败,密码错误");
        }
    }
}
4.4.4 .常见的异常类型
- DisabledAccountException(帐号被禁用)
- LockedAccountException(帐号被锁定)
- ExcessiveAttemptsException(登录失败次数过多)
- ExpiredCredentialsException(凭证过期)等
4.4.5 认证过程源码解析
可见:shiro相关源码解析
4.5 自定义Realm
通过分析源码可得:
- 认证: 
  - 最终执行用户名比较是 在SimpleAccountRealm类 的 doGetAuthenticationInfo 方法中完成用户名校验
- 最终密码校验是在 AuthenticatingRealm类 的 assertCredentialsMatch方法 中
 
- 总结: 
  - AuthenticatingRealm 认证realm doGetAuthenticationInfo
- AuthorizingRealm 授权realm doGetAuthorizationInfo
 
自定义Realm的作用:放弃使用.ini文件,使用数据库查询
上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm。
4.5.1 shiro提供的Realm

4.5.2 根据认证源码认证使用的是SimpleAccountRealm

 SimpleAccountRealm的部分源码中有两个方法一个是 认证 一个是 授权:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    UsernamePasswordToken upToken = (UsernamePasswordToken)token;
    SimpleAccount account = this.getUser(upToken.getUsername());
    if (account != null) {
        if (account.isLocked()) {
            throw new LockedAccountException("Account [" + account + "] is locked.");
        }
        if (account.isCredentialsExpired()) {
            String msg = "The credentials for account [" + account + "] are expired";
            throw new ExpiredCredentialsException(msg);
        }
    }
    return account;
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    String username = this.getUsername(principals);
    this.USERS_LOCK.readLock().lock();
    AuthorizationInfo var3;
    try {
        var3 = (AuthorizationInfo)this.users.get(username);
    } finally {
        this.USERS_LOCK.readLock().unlock();
    }
    return var3;
}
4.5.3 自定义realm
public class CustomRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("==================");
        return null;
    }
    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //在token中获取 用户名
        String principal = (String) authenticationToken.getPrincipal();
        System.out.println(principal);
        //实际开发中应当 根据身份信息使用jdbc mybatis查询相关数据库
        //在这里只做简单的演示
        //假设username,password是从数据库获得的信息
        String username="zhangsan";
        String password="123";
        if(username.equals(principal)){
            //参数1:返回数据库中正确的用户名
            //参数2:返回数据库中正确密码
            //参数3:提供当前realm的名字 this.getName();
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,password,this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}
4.5.4 自定义登录认证
public class TestAuthenticatorCustomRealm {
    public static void main(String[] args) {
        //1.创建安全管理对象 securityManager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //2.给安全管理器设置realm(设置为自定义realm获取认证数据)
        defaultSecurityManager.setRealm(new CustomRealm());
        //IniRealm realm = new IniRealm("classpath:shiro.ini");
        //3.给安装工具类中设置默认安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //4.获取主体对象subject
        Subject subject = SecurityUtils.getSubject();
        //5.创建token令牌
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");
        try {
            subject.login(token);//用户登录
            System.out.println("登录成功~~");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误!!");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误!!!");
        }
    }
}
4.6 使用MD5+SALT+HASH

4.6.1 MD5
- 作用:一般用来加密或者签名(校验和)
- 特点:MD5算法不可逆如何内容相同无论执行多少次md5生成结果始终是一致
- 网络上提供的MD5在线解密一般是用穷举的方法
- 生成结果:始终是一个16进制32位长度字符串
4.6.2 SALT
- 一段自定义字符,通过此字符+原密码,来使密码更加安全
- 这个自定义字符最好是自定义在代码中
- 还有一种方式是随机生成SALT,并将此SALT一起保存在数据库中(此方式黑客还需要判断SALT和密码的拼接方式)
4.6.3 加密基本测试
public static void main(String[] args) {
    //使用md5,通过hash算法让结果更加安全
    //通过构造方法将结果加密
    // 通过源码可以看到没有加盐默认盐值是1
    Md5Hash md5Hash = new Md5Hash("123");
    System.out.println(md5Hash.toHex());
    //使用MD5 + salt处理
    // 默认盐值是加在后面
    Md5Hash md5Hash1 = new Md5Hash("123", "java");
    System.out.println(md5Hash1.toHex());
    //使用md5 + salt + hash散列(参数代表要散列多少次,一般是 1024或2048)
    Md5Hash md5Hash2 = new Md5Hash("123", "java", 1024);
    System.out.println(md5Hash2.toHex());
}
结果:
202cb962ac59075b964b07152d234b70
b67e00dd69ba454e68c5e3dd228e19ec
2c5f16f56505041f97ab92c8e73c363d
4.6.4 自定义md5+salt的realm
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //获取 token中的 用户名
    String principal = (String) token.getPrincipal();
    //假设这是从数据库查询到的信息
    String username="zhangsan";
    String password="2c5f16f56505041f97ab92c8e73c363d";//加密后
    //根据用户名查询数据库
    if (username.equals(principal)) {
        //参数1:数据库用户名
        //参数2:数据库md5+salt之后的密码
        //参数3:注册时的随机盐
        //参数4:realm的名字
        return new SimpleAuthenticationInfo(principal,
                password,
                ByteSource.Util.bytes("java"),
                this.getName());
    }
    return null;
}
4.6.5 使用md5+salt 认证
public class TestAuthenticatorCustomMd5Realm {
    public static void main(String[] args) {
        //1.创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //2.注入realm
        CustomMd5Realm realm = new CustomMd5Realm();
        //3.设置realm使用hash凭证匹配器
        // 凭证匹配器是对token中密码和用户中密码进行验证的方法,默认是equals
        // 此处是用散列的凭证匹配器告诉shiro,我需要对密码进行md5加密并且设置一个散列
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //声明:使用的算法
        credentialsMatcher.setHashAlgorithmName("md5");
        //声明:散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(realm);
        //4.将安全管理器注入安全工具
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //5.通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        //6.认证
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");
        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }
    }
}
5. shiro中的授权
5.1 授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
5.2 关键对象
授权可简单理解为who对what(which)进行How操作:
- Who,即主体(Subject):主体需要访问系统中的资源。
- What,即资源(Resource):如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
- How,权限/许可(Permission):规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
5.3 授权流程

5.4 授权方式
5.4.1 基于角色的访问控制
RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
if(subject.hasRole("admin")){
   //用户拥有次角色可以操作什么资源
}
5.4.2 基于资源的访问控制
RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
if(subject.isPermission("user:update:01")){ //资源实例
  //对 资源01 用户具有修改的权限
}
if(subject.isPermission("user:update:*")){  //资源类型
  //对 所有的资源 用户具有修改的权限
}
5.5 权限字符串
权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。
例子:
- 用户创建权限:user:create,或user:create:*
- 用户修改实例001的权限:user:update:001
- 用户实例001的所有权限:user:*:001
5.6 shiro中授权编程实现方式
- 编程式
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
	//有权限
} else {
	//无权限
}
- 注解式
@RequiresRoles("admin")
public void hello() {
	//有权限
}
- 标签式
JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
	<!— 有权限—>
</shiro:hasRole>
注意: Thymeleaf 中使用shiro需要额外集成!
5.7 开发授权
5.7.1 realm中授权实现:
public class CustomMd5Realm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String primaryPrincipal = (String)principals.getPrimaryPrincipal();
        System.out.println("身份信息: "+primaryPrincipal); //用户名
        //根据身份信息 用户名 获取当前用户的角色信息,以及权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //假设 admin,user 是从数据库查到的 角色信息
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");
        //假设 ... 是从数据库查到的 权限信息赋值给权限对象
        simpleAuthorizationInfo.addStringPermission("user:*:01");
        simpleAuthorizationInfo.addStringPermission("product:*");//第三个参数为*省略
        return simpleAuthorizationInfo;
    }
    
    。。。。
}
5.7.2 授权测试:
public class TestAuthenticatorCustomMd5Realm {
    public static void main(String[] args) {
		。。。。
		try {
		    subject.login(token);
		        System.out.println("登录成功");
		    } catch (UnknownAccountException e) {
		        e.printStackTrace();
		        System.out.println("用户名错误");
		    } catch (IncorrectCredentialsException e) {
		        e.printStackTrace();
		        System.out.println("密码错误");
		    }
		
		    //授权
		    if (subject.isAuthenticated()){
		        //基于角色权限控制
		        System.out.println(subject.hasRole("admin"));
		        //基于多角色的权限控制
		        System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));//true
		        System.out.println(subject.hasAllRoles(Arrays.asList("admin", "manager")));//false
		        //是否具有其中一个角色
		        boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "manager"));
		        for (boolean aBoolean : booleans) {
		            System.out.println(aBoolean);
		        }
		
		        System.out.println("====这是一个分隔符====");
		
		        //基于权限字符串的访问控制  资源标识符:操作:资源类型
		        //用户具有的权限 user:*:01  prodect:*
		        System.out.println("权限:"+subject.isPermitted("user:update:01"));
		        System.out.println("权限:"+subject.isPermitted("product:update:02"));
		
		        //分别具有哪些权限
		        boolean[] permitted = subject.isPermitted("user:*:01", "user:update:02");
		        for (boolean b : permitted) {
		            System.out.println(b);
		        }
		
		        //同时具有哪些权限
		        boolean permittedAll = subject.isPermittedAll("product:*:01", "product:update:03");
		        System.out.println(permittedAll);
		    }
		}
	}
}



















