韩源-末日生存手册 - token
type: channel
link: https://kerryhan.cn/index.php/tag/token
items: FastDFS分布式文件系统之防盗链权限控制
items: 基于过滤拦截技术实现的身份认证方案
items: token中的JWT加密解密算法分析
items: 彻底搞懂Session、Cookie、token的区别
items: Shiro授权功能详解
items: 单点登录原理及实现
items: springboot+mybatisPlus代码层级梳理

FastDFS分布式文件系统之防盗链权限控制
type: item
date: 2023-10-26T07:45:38+00:00
description: 问题所在FastDFS采用的是tracker和storage的模式。tracker负责负载均衡storage负责文件存储、文件同步、提供文件访问接口（也就是说用户要从storage直接获取文件）其实，storage的http端口是开着的，外界只要知道端口和文件路径信息就可以直接访问到文件。这种操作，对于文件而言是不安全的。解决方式在storage上配置访问规则，打开防盗链功能，首先要确保不能直接访问到文件。其次，限制访问文件时必须带有一个本地应用服务器生成的token，这个token需要有文件id、自己设置的密钥、时间戳三个部分组成。但是只有本地应用服务器保存密钥是不可行的，需要把密钥同时保存到storage上，用于token的解密验证工作。配置storage进入etc/fdfs，编辑http.conf文件编辑后的内容Javaresources下新建fdfs_cilent.conf文件#默认值为30s connect_timeout = 10 #默认值为30s network_timeout = 30 charset = UTF-8 #tracker的http端口 http.tracker_http_port = 8888 #服务器 tracker_server = 192.168.1.106:22122 #开启防盗链，防盗链密钥 http.anti_steal_token = true http.secret_key= 密钥主要的工具类文件/** * 上传下载等文件操作工具类 */ public class FastDFSClient { private static org.slf4j.Logger logger = LoggerFactory.getLogger(FastDFSClient.class); /*** * 初始化加载FastDFS的TrackerServer配置 */ static { try { String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath(); ClientGlobal.init(filePath); } catch (Exception e) { logger.error("FastDFS Client Init Fail!",e); } } public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, MyException { String fileName = "M00/00/00/wKgBa2U4dPSAO7fXAAC5PC_5iKw01.docx"; String groupName = "group1"; String sourceUrl = getSourceUrl(groupName,fileName); System.out.println(sourceUrl); } public static FastDFSFile getFastDFSFile(MultipartFile file,String author) { InputStream inputStream = null; //获取文件名称（带有后缀） String allFileName = file.getOriginalFilename(); //获取文件扩展名 String ext = ""; byte[] content = null; if (allFileName != null && !"".equals(allFileName)) { ext = (allFileName.substring(allFileName.lastIndexOf("."))).replaceAll("\\.",""); } //文件名称（不带后缀） String fileName = allFileName.replaceAll("." + ext, ""); try { inputStream = file.getInputStream(); //获取文件所有的字节内容 content = inputStream.readAllBytes(); //一定要关闭输入流，不然会报错 inputStream.close(); } catch (IOException e) { throw new CustomException("文件上传错误", ResultCode.UPLOAD_ERROR); } //创建文件实体类对象 FastDFSFile fastDFSFile…

基于过滤拦截技术实现的身份认证方案
type: item
date: 2022-03-20T04:58:00+00:00
description: 基于过滤拦截技术实现的身份认证方案系统基本流程用户发送请求过滤器过滤掉，访问需要保持登录状态的资源但是又没有携带token的请求拦截器拦截掉，访问需要具备相应权限的资源但是又没有对应权限的请求进入业务逻辑代码层级configMyFilterConfig：自定义过滤器的基础配置，对所有资源都进行过滤MyInterceptorConfig：自定义拦截器的基础配置，设置所需拦截的放行的URLcontrollerUserController：接口层entityUser：用户的实体对象。目前为了简化操作，不必新建权限表，所以在用户的实体对象里存放了list<String>集合，即用户的属性里就带着用户的所有权限信息filter：MyFilter：自定义过滤器，这块主要是判断用户请求有没有携带token，如果没有携带token，而且还访问的是需要携带token(即保持登录)的接口，那么就过滤掉用户的这次请求。但是，有的资源是属于开放资源，即不需要用户保持登录状态，因此，在这个模块，做了白名单列表，过滤器不对白名单列表进行过滤。interceptorMyInterceptor：自定义拦截器，这块主要是拦截需要权限的接口，获取用户的token，并且解析出用户权限信息，拿用户权限信息和接口所需权限进行比对，失败则拒绝，成功则让请求进行接口层。serviceUserService：接口所对应的具体业务逻辑，这块目前主要是有判断用户名密码，进而生成token，返回前端。utilJWTUtil：JWT工具类，可以用于生成JWT，解密JWT

token中的JWT加密解密算法分析
type: item
date: 2021-04-20T10:35:00+00:00
description: token中的JWT加密解密算法分析前言题目是token中的JWT，这边其实就能理解到，token是一个非常大的概念，翻译就是一个令牌的意思，一般用来表示经过验证之后得到的凭证，长度没有限制。token有很多种，可以是自己定义的，也可以是标准的。JWT属于token这个东西的一种、一个标准，整个JWT分为三段式（Header、Payload、Signature），具备自己的验证方式。JWTHeader（头部）header部分主要是存储JSON对象，描述了JWT的基本属性，主要有两个字段（"alg","typ"），alg属性表示Signature的加密算法，typ属性默认写为JWT。最后将header的JSON对象使用Base64URL进行编码转化，放在JWT的第一段。Payload（负载）Payload部分也是JSON对象，存放实际上需要传输的数据。JWT官方规定7个字段。iss：签发人exp：过期时间sub：主题aud：受众nbf：生效时间iat：签发时间jti：编号此处也可自定义添加字段，最终通过Base64URL转换为字符串。Signature（签名）Signature是对前两部分的一个签名，主要用于鉴别是否是原始数据，判断有没有被篡改，此处需要指定密钥（secret），解密时无需密钥。Base64URLheader和payload的编码格式均为Base64URL，这个算法类似于Base64，只是在Base64的基础上，对部分字符进行了进一步的编码。总结JWT不加密，Header、Payload只是通过编码生成字符串，Signature只是通过Header里指定的算法对Header、Payload进行加密，主要用于鉴别token有没有被篡改。JWT一旦签发token，中间无法废止token，除非更改服务器JWT逻辑，所以token有效期一般不宜过长。暂时存在的疑问，HMAC SHA256算法是如何实现加密需要密钥，但核对却不需要。源码package com.springboot.shiro.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import java.util.Date; /** * @author Johnny * @Date: 2020/9/18 11:31 * @Description: */ @Slf4j public class JWTUtil { /** * 生成token * @param userName * @param secret * @param role * @return */ public static String getToken(String userName ,String secret,String role){ String token = null; token = JWT.create() .withClaim("userName",userName) .withClaim("role",role) .withExpiresAt(new Date(System.currentTimeMillis()+5*60*10000)) .sign(Algorithm.HMAC256(secret)); return token; } /** * 解密token，并返回用户名 * @param token * @return */ @ExceptionHandler public static String getUserName(String token){ DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("userName").asString(); } /** * 解密token,并返回角色信息 * @param token * @return */ public static String getRole(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("role").asString(); } public static void main(String[] args) { String token = JWTUtil.getT…

彻底搞懂Session、Cookie、token的区别
type: item
date: 2021-04-20T08:53:06+00:00
description: 彻底搞懂Session、Cookie、token的区别今天纠正了最大的一个误区，session、cookie、token到底是些什么东西，是有什么作用，值得记录一下。前言HTTP协议是无状态的，什么是无状态呢，就是你的下一次请求和上一次请求是没有任何关系的，服务器也不知道是一个人请求了两遍接口，还是两个人分别请求了一次接口。这样的好处肯定是服务器不需要作额外的操作来分辨，降低了服务器负载，但是但是但是，如果要做一个系统，这个系统操作很多，第一步是用户登录，后续的操作都是基于用户登录的情况下进行的，那么服务器就没办法识别了。所以这种情况行不通，必须有其他解决办法来实现服务器分辨用户的能力。Session会话模式session字面意思就是会话，所谓会话就是要保持你和另一个人沟通的这种状态。服务器为了区分用户，会给用户一个session_id，这个session_id一般存在客户端（浏览器）的cookie中，每次请求的时候都把session_id传给后端，后端很具session_id从session里面进行寻找，找到了，就相当于知道用户了。服务器会把session存放在服务器上，但是如果在高并发的情况下，服务器的session会存很多信息，而且session一般都在内存里，很影响服务器性能，所以在高并发状况下，不建议使用session。另外，它也不支持分布式，因为分布式相当于分成多个后端，那这样的话，我的session只能保存在一个后端，用户访问其他节点接口时候，就会出现认证失败的情况。token模式token模式相比于session模式，会更容易支持高并发。用户一开始请求服务器进行登录认证，认证成功之后，服务器根据用户的id等不敏感信息生成一个加密后的字符串序列，也就是token，把token放在响应头里，返回给前端用户，以后每次的请求，用户都需要带着token。后端接收到token，进行解析，解析成功，就认为用户已经登录，而且也可以根据当时加密所加的那些字段，识别用户身份。cookiecookie一般称为cookies，类型为小型的文本文件，相当于一个浏览器的小型数据库，用于存储简单不敏感信息，内存很小，一般存放session_id。如果系统采用session会话模式，那么前端从cookie中取得session_id，再去访问服务器，这是正常流程。由于cookie内存非常小，所以后续浏览器衍生出了SessionStorage、localstorage等浏览器的小型数据库。SessionStorage里的数据关闭浏览器自动清除。

Shiro授权功能详解
type: item
date: 2021-04-10T06:57:00+00:00
description: 授权，也叫访问控制，即在应用中控制谁能访问哪些资源（如访问页面/编辑数据/页面操作等）。在授权中需了解的几个关键对象：主体（Subject）、资源（Resource）、权限（Permission）、角色（Role） 主体主体，即访问应用的用户，在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。资源在应用中用户可以访问的任何东西，比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。权限安全策略中的原子授权单位，通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源，如：访问用户列表页面查看/新增/修改/删除用户数据（即很多时候都是CRUD（增查改删）式权限控制）打印文档等等。。。如上可以看出，权限代表了用户有没有操作某个资源的权利，即反映在某个资源上的操作允不允许，不反映谁去执行这个操作。所以后续还需要把权限赋予给用户，即定义哪个用户允许在某个资源上做什么操作（权限），Shiro不会去做这件事情，而是由实现人员提供。Shiro支持粗粒度权限（如用户模块的所有权限）和细粒度权限（操作某个用户的权限，即实例级别的），后续部分介绍。角色角色代表了操作集合，可以理解为权限的集合，一般情况下我们会赋予用户角色而不是权限，即这样用户可以拥有一组权限，赋予权限时比较方便。典型的如：项目经理、技术总监、CTO、开发工程师等都是角色，不同的角色拥有一组不同的权限。隐式角色：即直接通过角色来验证用户有没有操作权限，如在应用中CTO、技术总监、开发工程师可以使用打印机，假设某天不允许开发工程师使用打印机，此时需要从应用中删除相应代码；再如在应用中CTO、技术总监可以查看用户、查看权限；突然有一天不允许技术总监查看用户、查看权限了，需要在相关代码中把技术总监角色从判断逻辑中删除掉；即粒度是以角色为单位进行访问控制的，粒度较粗；如果进行修改可能造成多处代码修改。显示角色：在程序中通过权限控制谁能访问某个资源，角色聚合一组权限集合；这样假设哪个角色不能访问某个资源，只需要从角色代表的权限集合中移除即可；无须修改多处代码；即粒度是以资源/实例为单位的；粒度较细。请google搜索“RBAC”和“RBAC新解”分别了解“基于角色的访问控制”“基于资源的访问控制(Resource-Based Access Control)”。授权方式Shiro支持三种方式的授权：编程式：通过写if/else授权代码块完成：Subject subject = SecurityUtils.getSubject(); if(subject.hasRole(“admin”)) { //有权限 } else { //无权限 }注解式：通过在执行的Java方法上放置相应的注解完成：@RequiresRoles("admin") public void hello() { //有权限 } 没有权限将抛出相应的异常；JSP/GSP标签：在JSP/GSP页面通过相应的标签完成：<shiro:hasRole name="admin"> <!— 有权限 —></shiro:hasRole> 后续部分将详细介绍如何使用。 授权基于角色的访问控制（隐式角色）1、在ini配置文件配置用户拥有的角色（shiro-role.ini）[users] zhang=123,role1,role2 wang=123,role1规则即：“用户名=密码,角色1，角色2”，如果需要在应用中判断用户是否有相应角色，就需要在相应的Realm中返回角色信息，也就是说Shiro不负责维护用户-角色信息，需要应用提供，Shiro只是提供相应的接口方便验证，后续会介绍如何动态的获取用户角色。2、测试用例（com.github.zhangkaitao.shiro.chapter3.RoleTest）@Test public void testHasRole() { login("classpath:shiro-role.ini", "zhang", "123"); //判断拥有角色：role1 Assert.assertTrue(subject().hasRole("role1")); //判断拥有角色：role1 and role2 Assert.assertTrue(subject().hasAllRoles(Arrays.asList("role1", "role2"))); //判断拥有角色：role1 and role2 and !role3 boolean[] result = subject().hasRoles(Arrays.asList("role1", "role2", "role3")); …

单点登录原理及实现
type: item
date: 2021-03-17T07:13:00+00:00
description: 一、单系统登录机制1、http无状态协议 web应用采用browser/server架构，http作为通信协议。http是无状态协议，浏览器的每一次请求，服务器会独立处理，不与之前或之后的请求产生关联，这个过程用下图说明，三次请求/响应对之间没有任何联系3c91a3bf-25d8-4b1f-8e4a-68535c51aaa8 但这也同时意味着，任何用户都能通过浏览器访问服务器资源，如果想保护服务器的某些资源，必须限制浏览器请求；要限制浏览器请求，必须鉴别浏览器请求，响应合法请求，忽略非法请求；要鉴别浏览器请求，必须清楚浏览器请求状态。既然http协议无状态，那就让服务器和浏览器共同维护一个状态吧！这就是会话机制2、会话机制 浏览器第一次请求服务器，服务器创建一个会话，并将会话的id作为响应的一部分发送给浏览器，浏览器存储会话id，并在后续第二次和第三次请求中带上会话id，服务器取得请求中的会话id就知道是不是同一个用户了，这个过程用下图说明，后续请求与第一次请求产生了关联8a9fb230-d506-4b19-b821-4001c68c4588 服务器在内存中保存会话对象，浏览器怎么保存会话id呢？你可能会想到两种方式请求参数cookie 将会话id作为每一个请求的参数，服务器接收请求自然能解析参数获得会话id，并借此判断是否来自同一会话，很明显，这种方式不靠谱。那就浏览器自己来维护这个会话id吧，每次发送http请求时浏览器自动发送会话id，cookie机制正好用来做这件事。cookie是浏览器用来存储少量数据的一种机制，数据以”key/value“形式存储，浏览器发送http请求时自动附带cookie信息 tomcat会话机制当然也实现了cookie，访问tomcat服务器时，浏览器中可以看到一个名为“JSESSIONID”的cookie，这就是tomcat会话机制维护的会话id，使用了cookie的请求响应过程如下图518293d9-64b2-459c-9d45-9f353c757d1f3、登录状态 有了会话机制，登录状态就好明白了，我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份，服务器拿到用户名密码去数据库比对，正确的话说明当前持有这个会话的用户是合法用户，应该将这个会话标记为“已授权”或者“已登录”等等之类的状态，既然是会话的状态，自然要保存在会话对象中，tomcat在会话对象中设置登录状态如下用户再次访问时，tomcat在会话对象中查看登录状态HttpSession session = request.getSession();session.setAttribute("isLogin", true);HttpSession session = request.getSession();session.getAttribute("isLogin"); 实现了登录状态的浏览器请求服务器模型如下图描述 每次请求受保护资源时都会检查会话对象中的登录状态，只有 isLogin=true 的会话才能访问，登录机制因此而实现。二、多系统的复杂性 web系统早已从久远的单系统发展成为如今由多系统组成的应用群，面对如此众多的系统，用户难道要一个一个登录、然后一个一个注销吗？就像下图描述的这样6dfbb0b1-46c0-4945-a3bf-5f060fa80710 web系统由单系统发展成多系统组成的应用群，复杂性应该由系统内部承担，而不是用户。无论web系统内部多么复杂，对用户而言，都是一个统一的整体，也就是说，用户访问web系统的整个应用群与访问单个系统一样，登录/注销只要一次就够了9fe14ab3-4254-447b-b850-0436e628c254 虽然单系统的登录解决方案很完美，但对于多系统应用群已经不再适用了，为什么呢？ 单系统登录解决方案的核心是cookie，cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的，这个限制就是cookie的域（通常对应网站的域名），浏览器发送http请求时会自动携带与该域匹配的cookie，而不是所有cookie4d58ccfa-0114-486d-bec2-c28f2f9eb513 既然这样，为什么不将web应用群中所有子系统的域名统一在一个顶级域名下，例如“*.baidu.com”，然后将它们的cookie域设置为“baidu.com”，这种做法理论上是可以的，甚至早期很多多系统登录就采用这种同域名共享cookie的方式。 然而，可行并不代表好，共享cookie的方式存在众多局限。首先，应用群域名得统一；其次，应用群各系统使用的技术（至少是web服务器）要相同，不然cookie的key值（tomcat为JSESSIONID）不同，无法维持会话…

springboot+mybatisPlus代码层级梳理
type: item
date: 2020-04-30T14:40:00+00:00
description: 代码层级梳理代码层级annotation层ExcelVoAttribut主要用于写@ExcelVoAttribute注解，可以更灵活的配置Excel数据表导出的一些基本配置LogDirection主要用于写@LogDirection注解，注解里包含一个字段（接口描述），通过接口添加这个注解，填写上接口的功能，当用户在访问接口时候，可以利用aop技术，把用户的操作记到日志里aspect层LogDrectionAspect主要定义了aop切点的位置和增强的方法/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Aspect @Component @Slf4j public class LogDirectionAspect { @Pointcut("@annotation(logDirection)") public void doLogDirection(LogDirection logDirection){ } @Around(value = "doLogDirection(logDirection)",argNames = "pjp,logDirection") public Object doBefore(ProceedingJoinPoint pjp,LogDirection logDirection) throws Throwable{ if (!"".equals(logDirection.direction())) { log.info(logDirection.direction()+"接口被调用"); return pjp.proceed(); } return "接口信息不规范"; } }@Pointcut定义切点，此处定义切点为使用@LogDirection的方法@Around定义增强的方法，通过around的参数ProceedingJoinPoint可以很好的控制程序能不能继续执行下一步，在权限判定发挥着比较重要的作用拓展：利用下面的这种方法，需要在xml文件中配置bean实例，当然也可以在方法中通过手动扫包的方法获得bean实例进行操作/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Aspect public class MyAdvice { /** * 配置切点 */ @Pointcut("execution(* com.soft1851.spring.web.dao..*.insert*(..))") public void pointCut() { } /** * 配置前置增强 */ @Before("MyAdvice.pointCut()") public void beforeMethod() { System.out.println("等待数据插入"); } /** * 配置后置增强 */ @AfterReturning("execution(* com.soft1851.spring.web.dao..*.*(..))") public void afterMethod(){ System.out.println("关闭数据库"); } }xml配置实例 <bean name="forumDao" class="com.soft1851.spring.web.dao.impl.ForumDaoImpl"/> <bean name="myAdvice" class="com.soft1851.spring.web.interceptor.MyAdvice"/> <aop:aspectj-autoproxy/>common层ResponseResult和ResultCode这两个类可以将响应体统一进行封装，将接口的响应体规范化，使前端获取数据也更加友好贴下代码，防止以后找不到ResponseResult类package com.soft1851.music.admin.common; import lombok.Data; import java.io.Serializable; /** * @author Johnny * @Date: 2020/4/21 19:52 * @Description: */ @Data public class ResponseResult implements Serializable { private static final long serialVersionUID = -3948389268046368059L; private Integer code; private String msg; private Obje…