对 Shiro 的理解与 Shiro + JWT

其他

这篇文章不会涉及 Session、Cache 等……下文是我对 Shiro 简单的理解,也许能让我这样的新手尽可能快地利用 Shiro 实现自己的需求……

样例代码仓库

详见 config/ShiroConfigsecurity/ 下的代码。

Shiro

搜索任何一篇关于 Shiro 的资料,几乎都会在文章前面列出 Authentication、Authorization……我们先放下这些,关注权限控制的核心。

身份验证

首先,Shiro 把身份验证抽象为 principals(身份)和 credentials(凭证)。具体下来其实就对应我们平时使用的 username 与 password。简单来讲,「身份验证」其实也就对应「登录」。

登陆(身份验证)完成后,Shiro 首先确定了你是我们系统中的人(否则,你的状态就是游客),然后 Shiro 会通过某种方式去查询该用户「能做什么事」。

授权

「某用户能做什么事」在 Shiro 中对应的是「授权」的概念。要实现「授权」,了解 Shiro 中的四个概念即可。

Subject(主体)、Resource(资源)、Permission(权限)、Role(角色)。

Subject 就是你通过 username 和 password 登录进来的用户。

Resource 就是你要操作(增删改访问)的任何东西。比如一篇文章、一首歌……

Permission 是 Shiro 中的原子单位,指我们对某个 Resource 进行增删改访的权限。比如播放周杰伦的新专辑(你不买可不能播放哦)、比如访问某篇付费文章……

Role 其实就是 Permission 的集合。一般在用户层面被称为:普通用户、VIP、管理员、超级管理员……

就这些

要使用 Shiro 的话,了解这俩概念就行了。一个是身份验证,需要 principals 和 credentials。对应我们通常意义上的 username 和 password。一个是授权,核心是 Subject(主体)、Resource(资源)、Permission(权限)、Role(角色)。

身份验证其实就是 Authentication、授权信息则通过 Authorization 得到。

我们到配置 Shiro 的核心代码里看一下。

Realm 样例

暂且抛开配置,Shiro 的核心只需要我们写一个自己的 Realm。

简单来说,Realm 配置的就是如何身份验证以及如何返回权限信息。虽然说的是「如何去……」,实际上我们只需要配置从哪获得用户的真实 credentials,从哪获得用户的权限信息即可。因为 Shiro 已经把身份验证抽象为「对比用户登录时输入的 username、password 和注册时存储的信息」。

也就是说,我们只需要写一个自己的 Realm,配置身份验证和授权数据的来源就行了!

看看 Realm 的模板:

Realm 模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyRealm extends AuthorizingRealm {
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如 checkRole,checkPermission 之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//……
}

/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
* 认证信息 (身份验证)
* Authentication 是用来验证用户身份
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//……
}
}

如上,doGetAuthorizationInfo()、doGetAuthenticationInfo() 俩方法……

我们看看这俩方法的简单实现。

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
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();

// 解密获得 username,用于和数据库进行对比
// 通过 jwt 的 token 获取 username
String email = JWTUtil.getEmailByToken(token);

if (email == null) {
throw new AuthenticationException("token invalid");
}

// 根据 username 看后台是否能查到
User user = userService.getUserByEmail(email);
if (user == null) {
throw new AuthenticationException("User didn't existed!");
}

// 验证 token
if (!JWTUtil.verify(token, email, user.getPassword())) {
throw new AuthenticationException("Username or password error");
}

return new SimpleAuthenticationInfo(token, token, "my_realm");
}

这里其实本来应该是一个简单的 username 与 password 的判断(我的 username 使用的是 email)。我的数据是保存在数据库里的,通过服务 userService 获取。

用户信息也可以不放在数据库,放内存或就保存到硬盘也是可以的嘛……

如果数据库保存的是加密后的信息(明文保存当然也行——如果你愿意),可想而知这里的判断逻辑大概就是这样:

1
2
3
4
5
6
7
8
9
// 伪代码
username = auth.getUserName();
pass = Pass.Encryp(auth.getPass());// 获取加密后的密码
//..
User user = userService.getUserByUserName(username);
if(user.getPass() == pass){
return true;
}
//……

但我这里用的是 jwt 控制权限。jwt 做的事情其实可以简单归结为:把用户名和密码加密为一个 token(字符串),不过这个 token 是可以解密的。我们可以通过给定的解密方式(提供的方法)逆获取 username 和 password。

授权信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

String email = JWTUtil.getEmailByToken(principals.toString());

User user = userService.getUserByEmail(email);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

// user 获取 role
String role = userService.getRoleByEmail(email);
simpleAuthorizationInfo.addRole(role);

// 权限判断
// 根据 role 获取 permissions
Set<String> permissions = new HashSet<>(userService.getPermissionsByRole(role));
simpleAuthorizationInfo.addStringPermissions(permissions);
//Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
//simpleAuthorizationInfo.addStringPermissions(permission);
return simpleAuthorizationInfo;
}

如上,先通过 user 获取 role,再通过 role 获取 permission。

这就是上面授权里提到的 Subject(主体)、Permission(权限)、Role(角色)。

而授权里的 Resource(资源),具体就可以是某个 Controller 里的 API,我们可以在 API 对应的方法上通过注解配置来控制。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
// 一个通过 Role 控制,一个通过 Permission 控制。
@GetMapping("/require_role")
@RequiresRoles("admin")
public ResponseBean requireRole() {
return new ResponseBean(200, "You are visiting require_role", null);
}

@GetMapping("/require_permission")
@RequiresPermissions(logical = Logical.AND, value = {"view", "edit"})
public ResponseBean requirePermission() {
return new ResponseBean(200, "You are visiting permission require edit,view", null);
}

从这里看 user、role、permission,也很容易看出它们应该是多对多的关系。

一个 user 可以有多个 role,一个 role 也可以有多个 permisson。

补充——Shiro + JWT

Realm

上面的 Realm 模板并不完整,还需要重写一个 supports() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}

/**
* 只有当需要检测用户权限的时候才会调用此方法,例如 checkRole,checkPermission 之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//……
}
//……
}

JWTToken 是自己定义的一个 token 类,即便不懂我们其实也能大概猜出这里 supports() 的作用:用自己定义的 JWTToken 来进行权限验证。

shiro + jwt 实现 RESTful API 认证方式

程序逻辑:

  1. POST 用户名与密码到 /login 进行登入,如果成功返回一个加密 token,失败的话直接返回 401 错误。
  2. 之后用户访问每一个需要权限的网址请求,必须在 header 中添加 Authorization 字段,例如 Authorization: token,token 为密钥。
  3. 后台会进行 token 的校验,如果不通过直接返回 401。

换种方式解释:

  1. 用户输入 用户名、密码

    PS:需要前端加密吗?前端 RSA 加密

  2. 加密后的密码 与 通过 用户名获取的密码对比

  3. 成功 返回 token,失败 返回

  4. header 中添加 Authorization 字段。例如 Authorization: token,token 为密钥。

配置

Shiro 怎么知道应该通过 token 来判断呢?

这个问题对应的是 Shiro 在 doGetAuthenticationInfo() 中传入的 auth 里的 Credential 为啥获取得到的是 token。(按上面的理解 Credential 不本该是密码吗……)

1
2
3
4
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
//……
}

是通过在 Shiro 的配置里注入自己实现的 JWTFilter 实现的。配置后就会放弃普通的用户名、密码鉴权方式而使用 token,即 JWT 来鉴权了。

这也是为什么 Shiro 要用 Credential 这个概念而不直接弄个 password……

类似于 Spring MVC 里通过 DispatcherServlet 来控制请求,我们可以通过自己配置的 Filter 来进行权限控制。

具体不谈了。