JWT的权限控制与Shiro入门
一、前端权限控制
1.1介绍
在vue工程中,需要根据登录用户所拥有的权限信息,动态的加载菜单列表(路由列表)
- 登录成功后获取用户信息,包含权限列表(菜单权限,按钮权限等)
- 根据用户的权限,去动态的渲染页面(根据路由名称和权限标识比较)
- 页面按钮权限通过自定义方法控制可见性
1.2具体
- 此时需要准备所有使用该系统的用户的等级:
- saas管理员----拥有所有权限
- 企业管理员----创建租户企业的权限
- 普通用户-----被分配的权限
- 此时前端访问登录界面后,就开始渲染页面,使用了路由
- 路由首先把公共的组件路由出来,然后调用前置构字函数(beforeEach),获取用户的信息存储起来。
- 最后判断当前的模块路由是否具有访问权限,来决定是否渲染此路由。
二、有状态服务和无状态服务
对服务器程序来说,究竟是有状态服务,还是无状态服务,其判断依据——两个来自相同发起者的请求在服务器端是否具备上下文关系。
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的差异
- Session是在服务器端的,而JWT是在客户端的。
- Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
- 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.认证
- subject调用login方法,传递token,会自动委任给SecurityManager
- 在SecurityManager中使用认证器
- 认证器找到了所有的realm域,此时就找到了自己写好的。
- 然后就可以执行自己实现的realm域
2.授权
首先调用isPermitted或hasRole方法,然后会委任给SecurityManager。
在SecurityManager中使用授权器