shiro 系列

shiro-00-overview

Shiro-01-shiro 是什么?

Shiro-02-shiro 的架构设计详解

Shiro-03-5 分钟入门 shiro 安全框架实战笔记

Shiro-04-Authentication 身份验证

Shiro-05-Authorization 授权

Shiro-06-Realms 领域

Shiro-07-Session Management 会话管理

Shiro-08-Cryptography 编码加密

Shiro-09-web 整合

Shiro-10-caching 缓存

Shiro-11-test 测试

Shiro-12-subject 主体

Shiro-20-shiro 整合 spring 实战及源码详解

Shiro-21-shiro 整合 springmvc 实战及源码详解

Shiro-22-shiro 整合 springboot 实战

Shiro-30-手写实现 shiro

Shiro-31-从零手写 shiro 权限校验框架 (1) 基础功能

序言

相信大家对于 shiro 已经有了最基本的认识,后续我们将尝试和大家一起手写一个自己的 shiro 权限校验框架,加深对于 shiro 的理解。

主要分成下面几个步骤:

(1)基本功能的实现

(2)整合 web

(3)整合 spring

(4)整合 springboot

基本功能的实现

本篇主要目标在于实现最基本的功能,也就是我们在 shiro 入门中使用的例子。

先和老马一起回顾一下:

  [java]
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
public static void main(String[] args) { //1. SecurityManager Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject(); //2. session Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } //3. 登录 if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); currentUser.login(token); } //4. 授权 log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } //5. 登出 currentUser.logout(); }

核心组件

shiro 的详细架构如下。

详细架构

我们在实现的时候,会做很多简化,以方便大家学习理解。

我们重点关注下面几个部分:

(1)subject 主题

(2)SecurityManager 安全管理器

(3)session

(4)login 与 logout

(5)hasRole、isPermitted 授权校验

接口定义

有了上面的目标之后,我们来定义一下对应的接口

subject

shiro 的核心思想在于面向 subject 编程,这样所有的操作看起来更加自然。

  [java]
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
60
import com.github.houbb.orihs.api.session.Session; import com.github.houbb.orihs.api.verify.VerifyContext; /** * @author binbin.hou * @since 0.0.1 */ public interface Subject { /** * 主题标识 * @return 标识 * @since 0.0.1 */ Object id(); /** * 当前的 session 信息 * @return session * @since 0.0.1 */ Session session(); /** * 是否已经验证过身份 * @return 是否 * @since 0.0.1 */ boolean authed(); /** * 登陆 * @param verifyContext 验证信息 * @since 0.0.1 */ void login(final VerifyContext verifyContext); /** * 登出 * @since 0.0.1 */ void logout(); /** * 是否拥有角色 * @param roleCode 角色标识 * @return 结果 * @since 0.0.1 */ boolean hasRole(final String roleCode); /** * 是否拥有权限 * @param permissionCode 权限 * @return 结果 * @since 0.0.1 */ boolean hasPermission(final String permissionCode); }

Session

对于 session 实际上可以沿用 HttpSessin,不过这里为了简单,直接定义了一个 session 接口。

  [java]
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
/** * session 接口定义 * @author binbin.hou * @since 0.0.1 */ public interface Session { /** * 设置属性 * @param key 键 * @param value 值 * @return this * @since 0.0.1 */ Session attr(final Object key, final Object value); /** * 获取属性 * @param key 键 * @return 结果 * @since 0.0.1 */ Object attr(final Object key); /** * 删除属性 * @param key 键 * @return 结果 * @since 0.0.1 */ Object removeAttr(final Object key); }

SecurityManager

安全管理器只有两个核心方法,一个是登录,一个是登出。

  [java]
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
package com.github.houbb.orihs.api.manager; import com.github.houbb.orihs.api.auth.Auth; import com.github.houbb.orihs.api.auth.AuthContext; import com.github.houbb.orihs.api.session.Session; import com.github.houbb.orihs.api.subject.Subject; import com.github.houbb.orihs.api.verify.VerifyContext; /** * 安全管理类 * @author binbin.hou * @since 0.0.1 */ public interface SecurityManager { /** * 登陆 * @param subject 主题 * @param verifyContext 验证信息 * @since 0.0.1 */ void login(final Subject subject, final VerifyContext verifyContext); /** * 登出 * @param subject 主题 * @since 0.0.1 */ void logout(final Subject subject); }

登录校验

个人感觉 shiro 的接口命名过于官方,不利于记忆,登录和授权的英文命名过于近似。

这里为了偷懒就给改了一个简单的名字。

  [java]
1
2
3
4
5
6
7
8
9
10
11
public interface Verify { /** * 认证 * @param context 上下文 * @return 结果 * @since 0.0.1 */ VerifyResult verify(final VerifyContext context); }

VerifyContext 用于存放登录信息:

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface VerifyContext { /** * 唯一标识 * @return 标识 * @since 0.0.1 */ Object id(); /** * 密码 * @return 密码 * @since 0.0.1 */ Object password(); }

VerifyResult 对应校验的结果,便于后期拓展。

  [java]
1
2
3
4
5
6
7
8
9
10
public interface VerifyResult { /** * 用户标识 * @return 标识 * @since 0.0.1 */ Object id(); }

授权校验

在登录成功之后,就是授权了。

  [java]
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
package com.github.houbb.orihs.api.auth; import java.util.List; /** * 授权 * @author binbin.hou * @since 0.0.1 */ public interface Auth { /** * 是否拥有角色 * @param role 角色 * @param authContext 认证上下文 * @return 结果 * @since 0.0.1 */ boolean hasRole(final Role role, final AuthContext authContext); /** * 是否拥有权限 * @param permission 权限 * @param authContext 认证上下文 * @return 结果 * @since 0.0.1 */ boolean hasPermission(final Permission permission, final AuthContext authContext); /** * 角色列表 * @param authContext 上下文 * @return 角色列表 * @since 0.0.1 */ List<Role> roles(final AuthContext authContext); /** * 权限列表 * @param authContext 上下文 * @return 权限列表 * @since 0.0.1 */ List<Permission> permissions(final AuthContext authContext); }

好的,定义好了接口,我们就可以开始实现了。

session 实现

session 的实现最简单的可以直接实现一个 map。

  [java]
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
60
61
62
63
import com.github.houbb.orihs.api.session.Session; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * session 接口定义 * * @author binbin.hou * @since 0.0.1 */ public class DefaultSession implements Session { /** * 属性 map * * @since 0.0.1 */ private Map<Object, Object> attrMap; public DefaultSession() { this.attrMap = new ConcurrentHashMap<>(); } /** * 设置属性 * * @param key 键 * @param value 值 * @return this * @since 0.0.1 */ @Override public Session attr(final Object key, final Object value) { attrMap.put(key, value); return this; } /** * 获取属性 * * @param key 键 * @return 结果 * @since 0.0.1 */ @Override public Object attr(final Object key) { return attrMap.get(key); } /** * 删除属性 * * @param key 键 * @return 结果 * @since 0.0.1 */ @Override public Object removeAttr(final Object key) { return attrMap.remove(key); } }

SecurityManager 实现

其中 auth 和 verify 是我们前面的登录和授权实现类。

登录

  [java]
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
/** * 登陆 * * @param subject 上下文 * @param verifyContext 验证上下文 * @since 0.0.1 */ @Override public void login(final Subject subject, VerifyContext verifyContext) { //0. 参数校验 ArgUtil.notNull(verify, "verify 验证实现"); ArgUtil.notNull(auth, "auth 授权实现"); //1. 验证 VerifyResult verifyResult = this.verify.verify(verifyContext); //2. 更新授权上下文 DefaultAuthContext authContext = new DefaultAuthContext(); authContext.id(verifyResult.id()); //3. 更新主题信息 DelegatingSubject delegatingSubject = (DelegatingSubject) subject; delegatingSubject.auth(auth); delegatingSubject.authContext(authContext); delegatingSubject.authed(true); //4. 重新绑定到当前线程 SecurityUtils.setSubject(delegatingSubject); }

登出

登出的时候会把相关的信息全部清空。

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** * 登出 * * @param subject 主题 * @since 0.0.1 */ @Override public void logout(final Subject subject) { // 信息清空 this.verify = null; this.auth = null; DelegatingSubject delegatingSubject = (DelegatingSubject) subject; delegatingSubject.authContext(null); delegatingSubject.authed(false); delegatingSubject.auth(null); delegatingSubject.session(null); SecurityUtils.clearSubject(); }

Subject 实现

主题作为 shiro 的核心部分,实现要复杂一点:

  [java]
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import com.github.houbb.orihs.api.auth.Auth; import com.github.houbb.orihs.api.auth.AuthContext; import com.github.houbb.orihs.api.auth.Permission; import com.github.houbb.orihs.api.auth.Role; import com.github.houbb.orihs.api.manager.SecurityManager; import com.github.houbb.orihs.api.session.Session; import com.github.houbb.orihs.api.subject.Subject; import com.github.houbb.orihs.api.verify.VerifyContext; import com.github.houbb.orihs.core.auth.DefaultPermission; import com.github.houbb.orihs.core.auth.DefaultRole; import com.github.houbb.orihs.core.auth.NoneAuth; import com.github.houbb.orihs.core.exception.OrihsException; import com.github.houbb.orihs.core.exception.OrihsRespCode; import com.github.houbb.orihs.core.session.DefaultSession; import com.github.houbb.orihs.core.util.SecurityUtils; /** * 默认主题 * @author binbin.hou * @since 0.0.1 */ public class DelegatingSubject implements Subject { /** * 是否已经验证过 * @since 0.0.1 */ private boolean authed = false; /** * session 信息 * @since 0.0.1 */ private Session session = new DefaultSession(); /** * 权限验证 * @since 0.0.1 */ private Auth auth = new NoneAuth(); /** * 授权上下文 * @since 0.0.1 */ private AuthContext authContext = null; @Override public boolean authed() { return authed; } public DelegatingSubject authed(boolean authed) { this.authed = authed; return this; } @Override public Object id() { assertAuthed(); return this.authContext.id(); } @Override public Session session() { return session; } public DelegatingSubject session(Session session) { this.session = session; return this; } public Auth auth() { return auth; } public DelegatingSubject auth(Auth auth) { this.auth = auth; return this; } public AuthContext authContext() { return authContext; } public DelegatingSubject authContext(AuthContext authContext) { this.authContext = authContext; return this; } @Override public void login(VerifyContext verifyContext) { SecurityManager securityManager = SecurityUtils.getSecurityManagerAndAssert(); securityManager.login(this, verifyContext); } @Override public void logout() { SecurityManager securityManager = SecurityUtils.getSecurityManagerAndAssert(); securityManager.logout(this); } @Override public boolean hasRole(String roleCode) { assertAuthed(); Role role = new DefaultRole(roleCode); return this.auth.hasRole(role, this.authContext); } @Override public boolean hasPermission(String permissionCode) { assertAuthed(); Permission permission = new DefaultPermission(permissionCode); return this.auth.hasPermission(permission, this.authContext); } /** * 断言已经授权 * @since 0.0.1 */ private void assertAuthed() { if(!authed) { throw new OrihsException(OrihsRespCode.VERIFY_USER_NOT_LOGIN); } } }

如何实现面向 subject 的?

实际上 shiro 设计比较巧妙的一点就是,如何实现以 subject 为主题的?

因为登录/登出实际上的实现都在 SecurityManager 中。

这里技巧就在于使用了 ThreadLocal 保存了 SecurityManager 信息。

  [java]
1
2
3
4
5
@Override public void login(VerifyContext verifyContext) { SecurityManager securityManager = SecurityUtils.getSecurityManagerAndAssert(); securityManager.login(this, verifyContext); }

Subject 在登录的时候首先会获取当前线程的 SecurityManager,然后调用其中的 login 方法进行登录校验。

  [java]
1
2
//4. 重新绑定到当前线程 SecurityUtils.setSubject(delegatingSubject);

登录完成后,会把 subject 信息重新设置到当前线程中,这样可以方便在任何地方使用。

权限校验

权限校验的实现其实比较简单:

  [java]
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
import com.github.houbb.heaven.util.util.CollectionUtil; import com.github.houbb.orihs.api.auth.Auth; import com.github.houbb.orihs.api.auth.AuthContext; import com.github.houbb.orihs.api.auth.Permission; import com.github.houbb.orihs.api.auth.Role; import com.github.houbb.orihs.core.util.CodeUtil; import java.util.List; /** * 抽象的验证策略 * @author binbin.hou * @since 0.0.1 */ public abstract class AbstractAuth implements Auth { @Override public boolean hasRole(Role role, AuthContext authContext) { List<Role> roles = this.roles(authContext); return CodeUtil.hasCode(roles, role); } @Override public boolean hasPermission(Permission permission, AuthContext authContext) { List<Permission> permissions = this.permissions(authContext); return CodeUtil.hasCode(permissions, permission); } }

只需要具体的实现类,提供对对应的角色编码之后,我们统一判断即可。

测试

好了,写了这么多,我们可以验证一下效果了。

测试代码

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) { //1. 构建并且设置 SecurityManager Verify verify = new FooVerify(); Auth auth = new FooAuth(); SecurityManager securityManager = new DefaultSecurityManager(verify, auth); SecurityUtils.setSecurityManager(securityManager); //2. 获取 Subject Subject subject = SecurityUtils.getSubject(); Session session = subject.session(); session.attr("someKey", "aValue"); //3. 登录 VerifyContext verifyContext = new DefaultVerifyContext(RoleConst.ROLE_ADMIN, RoleConst.ROLE_ADMIN); subject.login(verifyContext); if(subject.authed()) { System.out.println("已经登录,当前用户:" + subject.id()); System.out.println("是否拥有角色:" + subject.hasRole(RoleConst.ROLE_ADMIN)); System.out.println("是否拥有权限:" + subject.hasPermission(RoleConst.ROLE_ADMIN)); System.out.println(subject.session().attr("someKey")); } subject.logout(); System.out.println(subject.authed()); }

测试实现

为了验证,我们实现了最简单的验证和授权实现。

登录

  [java]
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
import com.github.houbb.orihs.api.verify.Verify; import com.github.houbb.orihs.api.verify.VerifyContext; import com.github.houbb.orihs.api.verify.VerifyResult; import com.github.houbb.orihs.core.constant.RoleConst; import com.github.houbb.orihs.core.exception.OrihsException; import com.github.houbb.orihs.core.exception.OrihsRespCode; /** * @author binbin.hou * @since 0.0.1 */ public class FooVerify implements Verify { @Override public VerifyResult verify(VerifyContext context) { final String id = (String) context.id(); final String password = (String) context.password(); if(RoleConst.ROLE_ADMIN.equals(id)) { if(RoleConst.ROLE_ADMIN.equals(password)) { return DefaultVerifyResult.newInstance().id(id); } throw new OrihsException(OrihsRespCode.VERIFY_ERROR_ACCT_PWD); } if(RoleConst.ROLE_GUEST.equals(context.id())) { if(RoleConst.ROLE_GUEST.equals(password)) { return DefaultVerifyResult.newInstance().id(id); } throw new OrihsException(OrihsRespCode.VERIFY_ERROR_ACCT_PWD); } throw new OrihsException(OrihsRespCode.VERIFY_UNKNOWN_ACCT); } }

授权

  [java]
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
import com.github.houbb.orihs.api.auth.AuthContext; import com.github.houbb.orihs.api.auth.Permission; import com.github.houbb.orihs.api.auth.Role; import com.github.houbb.orihs.core.constant.RoleConst; import com.github.houbb.orihs.core.util.PermissionUtil; import com.github.houbb.orihs.core.util.RoleUtil; import java.util.List; /** * 测试版本 * @author binbin.hou * @since 0.0.1 */ public class FooAuth extends AbstractAuth { @Override public List<Role> roles(AuthContext authContext) { if(RoleConst.ROLE_ADMIN.equals(authContext.id())) { return RoleUtil.roles(RoleConst.ROLE_ADMIN); } return RoleUtil.roles(RoleConst.ROLE_GUEST); } @Override public List<Permission> permissions(AuthContext authContext) { if(RoleConst.ROLE_ADMIN.equals(authContext.id())) { return PermissionUtil.permissions(RoleConst.ROLE_ADMIN); } return PermissionUtil.permissions(RoleConst.ROLE_GUEST); } }

测试日志

测试日志如下:

  [plaintext]
1
2
3
4
5
已经登录,当前用户:admin 是否拥有角色:true 是否拥有权限:true aValue false

是不是和 shiro 入门教程比较,有那么一点儿味道了呢?

不过和真正的 shiro 相比,我们的功能还是缺少了很多,算是迈出了第一步。

小结

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次相遇。

参考资料

10 Minute Tutorial on Apache Shiro

https://shiro.apache.org/reference.html

https://shiro.apache.org/session-management.html