JWT的权限控制与Shiro入门

Posted 橡皮筋儿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JWT的权限控制与Shiro入门相关的知识,希望对你有一定的参考价值。

一、前端权限控制

1.1介绍

在vue工程中,需要根据登录用户所拥有的权限信息动态的加载菜单列表(路由列表)

  1. 登录成功后获取用户信息,包含权限列表(菜单权限,按钮权限等)
  2. 根据用户的权限,去动态的渲染页面(根据路由名称和权限标识比较)
  3. 页面按钮权限通过自定义方法控制可见性

1.2具体

  1. 此时需要准备所有使用该系统的用户的等级:
  • saas管理员----拥有所有权限
  • 企业管理员----创建租户企业的权限
  • 普通用户-----被分配的权限
  1. 此时前端访问登录界面后,就开始渲染页面,使用了路由
  2. 路由首先把公共的组件路由出来,然后调用前置构字函数(beforeEach),获取用户的信息存储起来。
  3. 最后判断当前的模块路由是否具有访问权限,来决定是否渲染此路由。

二、有状态服务和无状态服务

对服务器程序来说,究竟是有状态服务,还是无状态服务,其判断依据——两个来自相同发起者的请求在服务器端是否具备上下文关系。

2.1无状态服务

无状态服务对于客户端的单次请求,不会依赖其他请求的数据。即:处理请求的所需信息全部包含在该请求里。

如:cookie保存token的方式传输数据,对于服务端来讲,每次只是验证token是否合法,是否是我这个服务端颁发出的内容,从而辨别是否有权限,而不会保存用户的信息。

2.2有状态服务

有些数据会被记录在服务端,先后的请求是有关联的

当客户端登录后,服务端会颁发一个sessionId,此时在服务端就存储了该sessionId对应的session信息。客户端一般把返回的sessionId存储在cookie中。从而将http的无状态服务变相转换为有状态服务。

三、基于JWT的API鉴权

3.1JWT(JSON Web Token)

3.1.1 介绍

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

3.1.2JWT的构成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

1.header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

{
  \'typ\': \'JWT\',
  \'alg\': \'HS256\'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

2.playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

定义一个payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

然后将其进行base64加密,得到Jwt的第二部分。

3.signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

注意

secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

3.1.3JWT与Session的差异

  1. Session是在服务器端的,而JWT是在客户端的。
  2. Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
  3. Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。

3.1.4 工作流程

-每一次请求都需要token -Token应该放在请求header中 -我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin: *

3.1.5 用Token的好处

  • 无状态和可扩展性:Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。
  • 安全:Token不是Cookie。(The token, not a cookie.)每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。
    CSRF(Cross-site request forgery),中文名称:跨站请求伪造。CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。
  • token在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。

在spring拦截器中,进行拦截

四、Shiro安全框架

4.1介绍

shiro 是一个功能强大和易于使用的Java安全框架,为开发人员提供一个直观而全面的解决方案的认证,授权,加密,会话管理。

4.2 作用

  • Authentication:验证用户核实身份
  • Authorization:对用户进行访问控制:如判断用户是否被允许做某件事
  • SessionManager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储

4.3 特点

Web Support:Web支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

SSO:单点登录功能

4.4 快速入门

1.导入依赖

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2.创建配置文件

[users]
#模拟从数据库查询用户
#数据格式: 用户名=密码
zhangsan=123456
lisi=1234

3.测试login

 /**
     * 测试用户认证
     *  认证: 用户登录
     *  1.通过配置文件创建SecurityManagerFactory
     *  2.通过工厂获取securityManager
     *  3.将securityManager绑定到当前运行环境
     *  4.从当前运行环境中构造subject
     *  5.构造shiro登录的数据
     *  6.登录
     */
@Test
public void testLogin(){
    //1.获取工厂
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-1.ini");
    //2.获取安全管理
    SecurityManager securityManager = factory.getInstance();
    //3.绑定
    SecurityUtils.setSecurityManager(securityManager);
    //4.构造subject
    Subject subject = SecurityUtils.getSubject();
    //5.构造数据
    String username = "zhangsan";
    String password = "123456";
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //6,登录
    subject.login(token);
    //判断是否登录成功
    System.out.println(subject.isAuthenticated());
    System.out.println(subject.getPrincipal());
}

4.测试用户权限

[users]
#数据格式: 用户名=密码,角色名列表
zhangsan=123456,role1,role2
lisi=1234,role2
[roles]
#角色
#角色名=权限列表
role1=user:save,user:update
role2=user:find
/**
 * 测试用户权限
 */
@Test
public void testAble(){
    //1.获取工厂
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-2.ini");
    //2.获取安全管理
    SecurityManager securityManager = factory.getInstance();
    //3.绑定
    SecurityUtils.setSecurityManager(securityManager);
    //4.构造subject
    Subject subject = SecurityUtils.getSubject();
    //5.构造数据
    String username = "lisi";
    String password = "1234";
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //6,登录
    subject.login(token);
    //登录成功
    if (subject.isAuthenticated()){
        //查看是否有role1角色
        System.out.println(subject.hasRole("role2"));
        //是否具有权限
        System.out.println(subject.isPermitted("user:find"));
    }
}
//结果:true
//    false

4.5使用

1.导入maven坐标

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.7.25</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.2</version>
    </dependency>
</dependencies>

2.创建ini配置文件

其中指定编写的realm域的位置,并且将realm绑定到SecurityManager

[main]
#自定写的realm域,全包名
permReam=gyb.shiro.PermissionRealm
#注册realm到SecurityManager
securityManager.realms=$permReam

3.编写PermissionRealm类,继承AuthorizingRealm,重写方法

package gyb.shiro;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: 郜宇博
 * @Date: 2021/9/19 16:30
 */
public class PermissionRealm extends AuthorizingRealm {
    /**
     * 一般重写setName方法
     */
    public void setName(){
        super.setName("permissionRealm");
    }
    /**
     * 重写抽象doGetAuthorizationInfo:授权(获取到用户的授权数据)
     * 目的:根据认证数据获取用户的权限信息
     * principals:包含已认证安全数据
     * AuthorizationInfo 授权数据
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("执行授权方法");
        //1.获取安全数据
        //2.通过安全数据内的username查询数据库,获取到权限和角色
        //模拟查数据库的内容
        List<String> perms = new ArrayList<String>();
        perms.add("user:save");
        perms.add("user:update");
        List<String> roles = new ArrayList<String>();
        roles.add("role1");
        roles.add("role2");
        //3.构造返回
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //4.设置权限、角色集合
        info.addStringPermissions(perms);
        info.addRoles(roles);
        return info;
    }

    /**
     * doGetAuthenticationInfo :认证(根据用户名密码登录,将用户数据(安全数据)保存)
     * 目的:比较用户名和密码是否和数据库中的一致,并将安全数据存入到shiro中进行保管
     * AuthenticationToken:登录时构造的token
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行认证方法");
        //1.构造upToken
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        //2.获取输入的用户名密码
        String username = upToken.getUsername();
        String password = new String(upToken.getPassword());
        //3.与数据库比较
        if("123456".equals(password)){
            //一致,向shiro存入安全数据
            //三个参数:1.安全数据,2.密码,3,当前realm域名称
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,password,this.getName());
            return info;
        }else {
            throw new RuntimeException("用户名或密码错误");
        }
    }
}

4.使用

/**
 * 通过认证授权方法进行判断
 */
@Test
public void testShiro(){
    //1.获取工厂
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-3.ini");
    //2.获取安全管理
    SecurityManager securityManager = factory.getInstance();
    //3.绑定
    SecurityUtils.setSecurityManager(securityManager);
    //4.构造subject
    Subject subject = SecurityUtils.getSubject();
    //5.构造数据
    String username = "zhangsan";
    String password = "123456";
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //6,执行认证方法(登录),此时自动找到配置的Shiro类(在ini文件中),然后执行认证方法
    subject.login(token);
    //7.执行授权方法
    System.out.println(subject.hasRole("role1"));
    System.out.println(subject.isPermitted("user:save"));
}

4.6工作流程

1.认证

  1. subject调用login方法,传递token,会自动委任给SecurityManager
  2. SecurityManager中使用认证器
  3. 认证器找到了所有的realm域,此时就找到了自己写好的。
  4. 然后就可以执行自己实现的realm域

2.授权

首先调用isPermitted或hasRole方法,然后会委任给SecurityManager。

SecurityManager中使用授权器

以上是关于JWT的权限控制与Shiro入门的主要内容,如果未能解决你的问题,请参考以下文章

Shiro+JWT 实现权限管理--Shiro

Shiro+JWT 实现权限管理--Shiro

Shiro权限控制框架入门1:Shiro的认证流程以及基本概念介绍

Shiro + JWT权限验证

Shiro + JWT权限验证

Shiro + JWT权限验证