其他

这篇文章不会涉及 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 模板

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 简单实现

权限验证

  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 获取。

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

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

// 伪代码
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。

授权信息

  @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 对应的方法上通过注解配置来控制。

例如:

// 一个通过 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() 方法。

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 加密

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

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

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

配置

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

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

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
    String token = (String) auth.getCredentials();
		//...
}

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

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

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

具体不谈了。