Shiro

Shiro认证

1. 认证

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

2. Shiro 中认证的关键对象

  • Subject 主体:访问系统的用户,主体可以是用户、程序等等,进行认证的都称为主体;
  • Principal 身份信息:是主体(Subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)
  • Credential 凭证信息:是只有主体自己知道的安全信息,如密码、证书等。

3. 认证流程

img

4. 认证开发

4.1 模拟数据库中已存储用户信息【创建 shiro.ini 文件】

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

4.2 开发认证代码

复制代码

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 TestAuthenticator {

public static void main(String[] args) {
// 1. 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2. 给安全管理器设置 realm 【Realm 需要获取用户认证的数据源信息】
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// 3. SecurityUtils 设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 4. 获取关键对象 Subject 主体
Subject subject = SecurityUtils.getSubject();
// 5. 创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("wangwu", "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("认证失败: 密码错误~");
}
}
}
  • DisabledAccountException(帐号被禁用)
  • LockedAccountException(帐号被锁定)
  • ExcessiveAttemptsException(登录失败次数过多)
  • ExpiredCredentialsException(凭证过期)

4.3 自定义 Realm

  上边的程序使用的是 Shiro 自带的 IniRealm,IniRealm 从 shiro.ini 配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义 Realm。

1)Shiro 自定义 Realm

img

2)根据认证源码使用的是 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
public class SimpleAccountRealm extends AuthorizingRealm {
//.......省略
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = 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 = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}
}

3)自定义 Realm

 由于 Realm 相当于 Datasource 数据源,SecurityManager 进行安全认证需要通过 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
public class CustomerRealm extends AuthorizingRealm {

// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("====================");
return null;
}

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

4)使用自定义 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
public class TestCustomerRealm {

public static void main(String[] args) {
// 1.创建安全管理器
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

// 2.给安全管理器设置 realm 【数据库中获取用户名和密码】
defaultSecurityManager.setRealm(new CustomerRealm());

// 3.SecurityUtils 设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);

// 4.获取用户登录的主体
Subject subject = SecurityUtils.getSubject();

AuthenticationToken token = new UsernamePasswordToken("wangwu", "123");
try {
subject.login(token);
} catch (UnknownAccountException e) {
System.out.println("用户名不正确");
e.printStackTrace();
} catch (IncorrectCredentialsException e) {
System.out.println("用户密码不正确");
e.printStackTrace();
}
}

}

MD5+Salt

为什么需要MD5与盐

在TestCustomerRealm中的AuthenticationToken代表的是用户登录信息,在Shiro的认证过程中会与Realm的AuthenticationInfo(即数据库身份信息)相比对,如果不做任何加密处理。数据库信息与用户登录信息特别是密码完全一致,这样如果数据库泄露了,对应的用户信息就会受到极大威胁。

因而数据库并不会直接存有用户明文密码,而是在用户注册时将明文密码经过一系列加密算法获得的匿名密码存入数据库。当用户登录时会在业务层以相同算法算取匿名密码,再与数据库比对。MD5,Salt就是常用的加密算法。

注意的是,Shiro默认情况下是通过AuthenticatingRealm.class下的assertCredentialsMatch方法实现用户信息与数据库信息比对的,且比对方法为Strings.equals()。如果采用加密算法,数据库匿名密码与用户明文密码肯定对不上,因而我们需要更改默认比对方式。

Realm

因为是由Realm携带数据库信息,通过Realm的改造来更改默认比对方式。

1
2
3
4
5
6
7
// 设置 realm 使用的 hash 凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5"); // 设置使用MD5算法
// 2. 给安全管理器设置 realm 【Realm 需要获取用户认证的数据源信息】
CustomRealm customRealm = new CustomRealm();
customRealm.setCredentialsMatcher(credentialsMatcher);
securityManager.setRealm(customerMD5Realm);

这里还只是MD5,如果要加盐,不需要更改Realm,只需要再把带回来的AuthenticationInfo加上盐值就行了,因为Shiro会自动把盐值加在明文密码前再进行MD5计算的。

1
2
3
4
5
6
7
8
9
10
11
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String)authenticationToken.getPrincipal();
if("xry".equals(principal)){
// 参数1:返回数据库中的用户名 参数2:加盐后的匿名密码 参数3:注册时的随机盐 参数4:提供当前的 realm 名字,this.getName()
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
"xry","8a83592a02263bfe6752b2b5b03a4799",ByteSource.Util.bytes("X0*7ps"),this.getName());
return simpleAuthenticationInfo;
}
return null;
}

Hash散列

更深层次地,经过MD5和Salt后通过Hash散列再多次散列计算后就能得到更为复杂的密码。

需要对HashedCredentialsMatcher设置散列次数:

1
credentialsMatcher.setHashIterations(1024);     // 需要散列多少次

Shiro授权

1. 授权

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

2. 关键对象

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

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

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

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

3. 授权流程

img

4. 授权方式

  • 基于角色的访问控制【RBAC 基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制】
1
2
3
if(subject.hasRole("admin")){
//操作什么资源
}
  • 基于资源的访问控制【RBAC 基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制】
1
2
3
4
5
6
if(subject.isPermission("user:update:01")){ //资源实例
//对01用户进行修改
}
if(subject.isPermission("user:update:*")){ //资源类型
//对01用户进行修改
}

5. 权限字符串

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

例子:

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

6. 开发授权代码

自定义 Realm 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CustomerMD5Realm extends AuthorizingRealm {
/**
* 一个主体可以有多个身份,参数是集合 principals;但是只有一个主身份PrimaryPrincipal;
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String principal = (String) principalCollection.getPrimaryPrincipal();
System.out.println("principal:"+principal);

SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(Arrays.asList("user","admin"));
simpleAuthorizationInfo.addStringPermissions(Arrays.asList("user:update:*","admin:*:*","developer:*:01"));
return simpleAuthorizationInfo;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
System.out.println("principal = " + principal);
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class TestCustomerMD5Authenticator {

public static void main(String[] args) {
// 1. 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();

// 2. 给安全管理器设置 realm 【Realm 需要获取用户认证的数据源信息】
CustomerMD5Realm customerMD5Realm = new CustomerMD5Realm();
// 设置 realm 使用的 hash 凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5"); // 设置使用的 Hash 算法
credentialsMatcher.setHashIterations(1024); // 需要散列多少次
customerMD5Realm.setCredentialsMatcher(credentialsMatcher);
securityManager.setRealm(customerMD5Realm);

// 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("认证失败: 密码错误~");
}

// 授权
//hasRole,hasAllRoles等授权f都会触发Realm的doGetAuthorizationInfo。对于多参数授权方法还会多次触发Realm
if (subject.isAuthenticated()) {
//基于角色权限控制
System.out.println(subject.hasRole("super"));  // 走授权流程
// 基于多角色权限控制
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));  // 走授权流程
// 是否具有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "super"));  // 走授权流程
for (boolean aBoolean : booleans) {
System.out.println(aBoolean);
}
// 基于权限字符串的访问控制 资源标识符:操作:资源类型
System.out.println(subject.isPermitted("user:update:01"));  // 走授权流程
// 分别具有哪些权限
boolean[] permitted = subject.isPermitted("user:*:01", "order:*:02");  // 走授权流程
for (boolean b : permitted) {
System.out.println(b);
}
}
}
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!