loading...
Shiro基础
Published in:2022-01-31 | category: Shiro
Words: 3.6k | Reading time: 15min | reading:

权限管理

什么是权限管理

​ 基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

​ 权限管理包括用户身份认证授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

什么是身份认证

身份认证,就是判断一个用户是否合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。

什么是授权

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

什么是Shrio

Shiro是一个功能强大且易于使用的Java安全框架,它执行身份认证、授权、加密和会话管理.使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的应用程序到最大的web和企业应用程序.

Shiro是apache旗下的一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权,加密,绘画管理等功能,组成一个通用的安全认证框架.

shiro的核心架构

WkwQJJ.png

Subject

Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序.Subject在Shiro中是一个接口,接口定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权.

SecurityManager

SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理.通过SecurityManager可以完成subject的认证、授权等.实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等.

SecurityManager是一个接口,继承了Authenticator,Authorizer,SessionManager这三个接口.

Authenticaor

Authenticaor即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器.

Authorizer

Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能操作权限.

Realm

Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库中,那么Realm就需要从数据库获取用户身份信息.

SessionManager

sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录.

SessionDAO

SessionDAO即会话Dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库中.

CacheManager

CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能.

Cryptography

Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发.比如提供常用的散列、加/解密等功能.

Shiro中的认证

认证

​ 身份认证,就是判断一个用户是否为合法用户的处理过程.最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确.

shiro中认证的关键对象

  • Subject: 主体

    访问系统的用户,主体可以是用户、程序等,进行认证的都成为主体;

  • Principal: 身份信息

    是主体(Subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal).

  • Credential:凭证信息

    是只有主体自己知道的安全信息,如密码、证书等.

认证的流程

WA95Ct.png

认证的开发

创建项目并引入依赖

1
2
3
4
5
6
        <!--Shiro 依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>

ini配置文件

ini配置文件 用来学习shiro书写我们系统中相关得权限数据

1
2
3
4
[users]
xiaochen=123
zhangsan=123456
lisi=789

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class TestAuthenticator {
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("xiaochen","123");

//
try {
System.out.println("认证状态:" + subject.isAuthenticated());
subject.login(token); //用户认证
System.out.println("认证状态:" + subject.isAuthenticated());
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("认证失败: 用户名不存在" );
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败: 密码输入不正确");
}
}
}

开发认证源码

SimpleAccountRealm的部分源码中有两个方法一个是 认证 一个是 授权.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SimpleAccountRealm extends AuthorizingRealm {
//.......省略
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;
}

**认证: **

  1. 最终执行用户名比较 SimpleAccountRealm

    doGetAuthenticationInfo 在这个方法中完成用户名的校验

  2. 最终密码的校验是在 AuthenticaingRealm中

    assertCredentialsMatch

总结:

AuthenticatingRealm 认证realm doGetAuthenticationInfo

AuthorizingRealm 授权realm doGetAuthorizationInfo

自定义Realm

自定义Realm实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 自定义Realm实现
* 将认证/授权数据的来源转为数据库实现
*
* @author YoungKai
* @date 2021/7/13 16:19
*/
public class CustomerRealm extends AuthorizingRealm {

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//在token中获取用户名
String principal = (String)token.getPrincipal();
System.out.println(principal);
//根据身份信息使用jdbc mybatis查询相关数据库
if("xiaochen".equals(principal)){
//参数1:返回数据库中正确的用户名
//参数2:返回数据库中正确的密码
//参数3:提供当前Realm的名字 this.getName()
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("xiaochen","123456",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}

使用自定义realm认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.shiro.authenticator;

import com.shiro.realm.CustomerRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

/**
* 使用自定义Realm
*
* @author YoungKai
* @date 2021/7/13 16:22
*/
public class TestCustomerRealmAuthenticator {

public static void main(String[] args) {

//创建securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

//设置自定义realm
defaultSecurityManager.setRealm(new CustomerRealm());

//将安全工具类设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);

//通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();

//创建token
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123456");

try {
subject.login(token);
System.out.println(subject.isAuthenticated());
}catch(UnknownAccountException e){
e.printStackTrace();
System.out.println("认证失败: 用户名不存在" );
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败: 密码输入不正确");
}
}
}

使用MD5和Salt

实际应用是将盐和散列后的值存在数据库中,自动realm从数据库取出盐和加密后的值由shiro完成密码校验。

测试MD5、Salt、散列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TestShiroMD5 {
public static void main(String[] args) {

// //创建一个MD5算法
// Md5Hash md5Hash = new Md5Hash();
//
// md5Hash.setBytes("123".getBytes());
//
// String s = md5Hash.toHex();
// System.out.println(s);


//使用md5
Md5Hash md5Hash = new Md5Hash("123");

System.out.println(md5Hash.toHex());

//使用MD5 + salt处理
Md5Hash md5Hash1 = new Md5Hash("123","x0*7ps");

System.out.println(md5Hash1.toHex());

//使用md5 + salt + hash散列
Md5Hash md5Hash2 = new Md5Hash("123","x0*7ps",1024);
System.out.println("散列1024次: "+md5Hash2.toHex());
}
}

创建有加密的Realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 使用自定义realm 加入md5 + salt + hash
*
* @author YoungKai
* @date 2021/7/14 17:38
*/
public class CustomerMd5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取身份信息
String principal = (String) token.getPrincipal();

if ("xiaochen".equals(principal)){
//md5方式
// return new SimpleAuthenticationInfo(principal,"c15be9a15a0a238084e0c5a846f3a7b4",this.getName());

//md5 + salt
/**
* 参数1: 数据库用户名
* 参数2: 数据库md5 + salt之后的密码
* 参数3: 注册时的随机salt
* 参数4: realm的名字
*/
return new SimpleAuthenticationInfo(principal,"44c42bc682c33a4dae2af47eba4c8011", ByteSource.Util.bytes("x0*7ps"),this.getName());
}

return null;
}
}

测试加密后的认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class TestCustomerMd5RealmAuthenticator {

public static void main(String[] args) {

//创建安全管理起
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();


CustomerMd5Realm realm = new CustomerMd5Realm();

HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//添加MD5认证格式
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//如果添加salt,不需要设置特殊格式,只需要再Realm认证时传入注册时的随机salt的值即可

//设置散列次数
hashedCredentialsMatcher.setHashIterations(1024);
//设置realm使用hash凭证匹配器
realm.setCredentialsMatcher(hashedCredentialsMatcher);
//注入Realm
defaultSecurityManager.setRealm(realm);

//将安全管理器注入安全工具
SecurityUtils.setSecurityManager(defaultSecurityManager);
//通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();
//认证
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");

try {
subject.login(token);
System.out.println(subject.isAuthenticated());
}catch(UnknownAccountException e){
e.printStackTrace();
System.out.println("认证失败: 用户名不存在" );
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败: 密码输入不正确");
}

}
}

密码重试次数限制

​ 如在一个小时内密码最多重试5次,如果尝试次数超过了5次就锁定一个小时,1个小时后可再次重试,如果还是重试失败,可以锁定如1天,以此类推,防止密码被暴力破解。我们通过继承HashedCredentialsMatcher,且使用Ehcache记录重试次数和超时时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String)token.getPrincipal();
//retry count + 1
Element element = passwordRetryCache.get(username);
if(element == null) {
element = new Element(username , new AtomicInteger(0));
passwordRetryCache.put(element);
}
AtomicInteger retryCount = (AtomicInteger)element.getObjectValue();
if(retryCount.incrementAndGet() > 5) {
//if retry count > 5 throw
throw new ExcessiveAttemptsException();
}
boolean matches = super.doCredentialsMatch(token, info);
if(matches) {
//clear retry count
passwordRetryCache.remove(username);
}
return matches;
}

Shiro中的授权

授权

​ 授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

关键对象

授权可简单理解为who对what(which)进行How操作

who,即主体(Subject),主体需要访问系统中的资源。

what,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。

How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。

授权流程

WmauNV.png

授权方式

  • 基于角色的访问控制

    • RBAC基于角色的访问控制(Role-Based Access Control) 是以角色为中心进行访问控制

      1
      2
      3
      if(subject.hasRole("admin")){
      //操作什么资源
      }
  • 基于资源的访问控制

    • RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制

      1
      2
      3
      4
      5
      6
      7
      if(subject.isPermission("user:update:01")){	//资源实例
      //对01用户具有修改权限
      }

      if(subject.isPermission("user:update:*")){ //资源类型
      //对01用户进行修改
      }

权限字符串

​ 权限字符串的规则是: 资源标识符: 操作 : 资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分隔符,权限字符串也可以使用*通配符。

​ 例子:

  • 用户创建权限: user : create ,或 user :create:*
  • 用户修改实例001的权限: user:update:001
  • 用户实例001的所有权限: user: * : 001

shiro中授权编程实现方式

  • 编程式

    1
    2
    3
    4
    5
    6
    Subject subject =  SecurityUtils.getSunject();
    if(subject.hasRole("admin")){
    //有权限
    } else {
    //无权限
    }
  • 注解式

    1
    2
    3
    4
    @RequiresRoles("admin")
    public void hello(){
    //有权限
    }
  • 标签式

    1
    2
    3
    4
    5
    6
    7
    <!-- JSP/GSP 标签: 在JSP/GSP页面通过相应的标签完成: -->

    <shiro:hasRole name="admin">
    <!-- 有权限 -->
    </shiro:hasRole>

    注意:Thymeleaf中使用shiro需要额外集成。

认证用户进行授权

在之前MD5的Test基础之上,增加用户授权。代码分布在com.shiro.authenticaor.TestCustomerMd5RealmAuthenticator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//.....s 

//认证用户进行授权
if(subject.isAuthenticated()){
//1.基于角色权限控制
System.out.println(subject.hasRole("admin"));

//基于多角色权限控制
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));

//是否具有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user"));
for (int i = 0; i < booleans.length; i++) {
System.out.println(booleans[i]);
}

System.out.println("===================================================");

//基于权限字符串的访问控制 资源标识符:操作:资源类型
System.out.println("权限: "+subject.isPermitted("user:update:01"));
System.out.println("权限: "+subject.isPermitted("product:create:02"));

//分别具有哪些权限
boolean[] permitted = subject.isPermitted("user:*:01","order:*:10");
for (boolean b : permitted){
System.out.println(b);
}

//同时具有哪些权限
boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01");
System.out.println(permittedAll);
}
Prev:
Shiro身份验证
Next:
Redis哨兵模式(未完)
catalog
catalog