韩源-末日生存手册 - public
type: channel
link: https://kerryhan.cn/index.php/tag/public/
items: Levenshtein距离计算
items: FastDFS分布式文件系统之防盗链权限控制
items: Shiro授权功能详解
items: shiro框架概念及原理
items: 单点登录原理及实现
items: Aop面向切面编程

Levenshtein距离计算
type: item
date: 2023-11-30T06:32:00+00:00
description: 概念莱文斯坦距离(Levenshtein distance)是编辑距离的一种。它由俄罗斯科学家 弗拉基米尔·莱文斯坦在1965年提出。两个字符串之间的莱文斯坦距离是将一个单词更改为另一个单词所需的最少编辑操作次数。莱文斯坦距离允许的编辑操作包括单个字符的 替换/插入/删除。示例"Saturday" 和 "Sundays" 之间的 Levenshtein 距离是 4 1.) Saturday → Sturday // 删除第一个 a 2.) Sturday → Surday // 删除第一个 t 3.) Surday → Sunday // 替换 r 为 n 4.) Sunday → Sundays // 结尾添加 s计算方法莱文斯坦距离本质上可以理解为一个动态规划问题，其状态转移方程为：解释一下状态转移方程：方程第一行：如果 min(i,j) = 0（即当前两个字符串至少有一个长度为0，即是空字符串）时，则此时的莱文斯坦距离就是两字符串的长度的最大值。这个很好理解，不赘述了方程第二行：如果min(i,j) != 0，则取如下三个数字的最小值：2.1. 将a字符串的长度i减小1，计算其与长度为j的b字符串的莱文斯坦距离，并将其结果再加1，这其实等价于将当前a字符串的i位置的字符删除了；2.2. 将b字符串的长度j减小1，计算其与长度为i的a字符串的莱文斯坦距离，并将其结果再加1，这其实等价于在当前a字符串的i位置后面插入了一个字符（即插入了b字符串j位置的那个字符）；2.3. 将str_a、str_b字符串的长度同时减小1，计算两者的剩余字符串的莱文斯坦距离，此时如果str_a的i位置的字符和str_b的j位置的字符不同，则结果再加1，这其实等价于将str_a的i位置的字符改为str_b的j位置的那个字符；计算过程Java代码public class LevenshteinUtil { /** * 编辑距离算法，（用于计算距离相近的字符串） * * @param word1 * @param word2 * @return */ public static int minDistance(String word1, String word2) { int n = word1.length(); int m = word2.length(); if (n * m == 0) return n + m; int[][] d = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++) { d[i][0] = i; } for (int j = 0; j < m + 1; j++) { d[0][j] = j; } for (int i = 1; i < n + 1; i++) { for (int j = 1; j < m + 1; j++) { int left = d[i - 1][j] + 1; int down = d[i][j - 1] + 1; int left_down = d[i - 1][j - 1]; if (word1.charAt(i - 1) != word2.charAt(j - 1)) left_down += 1; d[i][j] = Math.min(left, Math.min(down, left_down)); } } return d[n][m]; } /** * 编辑距离算法，（用于计算距离相近的标签） * * @param tagList1 * @param tagList2 * @return */ public static int minDistance(List<String> tagList1, List<String> tagList2) { int n = tagList1.size(); int m = tagList2.size(); if (n * m == 0) return n + m; int[][] d = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++) { d[i][0] = i; } for (int j = 0; j < m + 1; j++) { d[0][j] = j; } for (int i = 1; i < n + 1; i++) { for (int j = 1; j < m + 1; j++) { int left = d[i - 1][j] + 1; int down = d[i][j - 1] + 1; int left_down = d[i - 1][j - 1]; if (!Objects.equals(tagList1…

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…

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")); …

shiro框架概念及原理
type: item
date: 2021-04-05T00:12:00+00:00
description: 一 shiro的简介shiro是apache的一个开源框架，是一个权限管理的框架，实现 用户认证、用户授权。二 shiro的架构图authenticator:认证器，主体进行认证最终通过authenticator进行authorizer:授权器，主体进行授权最终通过authorizer进行sessionManager:web应用中一般是用web容器对session进行管理，shiro也提供了一套session管理方式SessionDao:通过SessionDao管理session数据，以下是SessionDao的方法public interface SessionDAO {/*如DefaultSessionManager在创建完session后会调用该方法； 如保存到关系数据库/文件系统/NoSQL数据库；即可以实现会话的持久化； 返回会话ID；主要此处返回的ID.equals(session.getId())； */ Serializable create(Session session); //根据会话ID获取会话 Session readSession(Serializable sessionId) throws UnknownSessionException; //更新会话；如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用 void update(Session session) throws UnknownSessionException; //删除会话；当会话过期/会话停止（如用户退出时）会调用 void delete(Session session); //获取当前所有活跃用户，如果用户量多此方法影响性能 Collection<Session> getActiveSessions();cache Manager：缓存管理器，主要对session和授权数据进行缓存，比如将授权数据通过cacheManager进行缓存管理，和ehcache整合对缓存数据进行管理。三个核心组件:Subject, SecurityManager 和 Realms.Subject:即"当前操作用户"。但是，在Shiro中，Subject这一概念并不仅仅指人，也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着"当前跟软件交互的东西"。但考虑到大多数目的和用途，你可以把它认为是Shiro的"用户"概念。Subject代表了当前用户的安全操作，SecurityManager则管理所有用户的安全操作。SecurityManager:它是Shiro框架的核心，典型的Facade模式，Shiro通过SecurityManager来管理内部组件实例，并通过它来提供安全管理的各种服务。Realm: Realm充当了Shiro与应用安全数据间的"桥梁"或者"连接器"。也就是说，当对用户执行认证(登录)和授权(访问控制)验证时，Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲，Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节，并在需要时将相关数据提供给Shiro。当配置Shiro时，你必须至少指定一个Realm，用于认证和(或)授权。配置多个Realm是可以的，但是至少需要一个。Shiro内置了可以连接大量安全数据源(又名目录)的Realm，如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求，你还可以插入代表自定义数据源的自己的Realm实现。三 shiro提供的功能Authentication：用户认证（登录）Authorization：权限控制Session Management：会话管理Cryptography：数据加密Web Support：支持web的APICaching：缓存Concurrency：支持多线程应用程序Testing：测试的支持“Run As”：假设一个用户为另一个用户的身份“Remember Me”：在Session中保存用户身份

单点登录原理及实现
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）不同，无法维持会话…

Aop面向切面编程
type: item
date: 2020-04-06T16:48:00+00:00
description: 什么是面向切面编程官方：在软件业，AOP为Aspect Oriented Programming的缩写，意为：面向切面编程，通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续，是软件开发中的一个热点，也是Spring框架中的一个重要内容，是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离，从而使得业务逻辑各部分之间的耦合度降低，提高程序的可重用性，同时提高了开发的效率个人理解：面向切面编程旨在较少冗余代码，使用的是一种代码横向抽取（举例：不同的方法拥有三句同样的代码，那这个代码就可以抽取出来，但使用父类、接口、继承、实现会极大的提高代码的耦合度，因此我们采用面向切面编程，把这三句代码全部抽取到一个方法当中）xml配置版实例代码接口：public interface Hello { void sayHello(); } 实现类public class HelloImpl implements Hello { @Override public void sayHello() { System.out.println("hello"); } }增强（相当于抽取出来的部分代码）public class BuildHello { void beforeBuilder(){ System.out.println("start..."); } void afterBuilder(){ System.out.println("ending..."); } } xml中bean实例的配置 <bean id="hello" class="com.soft1851.spring.aop.hello.HelloImpl"/> <bean id="beforeAdvice" class="com.soft1851.spring.aop.hello.BuildHello"/> <aop:config > <aop:aspect id="before" ref="beforeAdvice"> <aop:pointcut id="beforeCut" expression="execution(* com.soft1851.spring.aop.hello..*.*(..))"/> <aop:before method="beforeBuilder" pointcut-ref="beforeCut"/> </aop:aspect> <aop:aspect id="after" ref="beforeAdvice"> <aop:pointcut id="afterCut" expression="execution(* com.soft1851.spring.aop.hello..*.*(..))"/> <aop:after-returning method="afterBuilder" pointcut-ref="afterCut"/> </aop:aspect> </aop:config>注意要点：aop-config是配置整体的切面编程，而aop-aspect是设置切点和增强的容器，aop-pointcun用于设置切点，aop:before用于设置前置增强，aop:after-returning用于设置后置增强注解版实例代码dao底层代码@Repository public class ForumDaoImpl implements ForumDao { private JdbcTemplate jdbcTemplate; public ForumDaoImpl(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public List<Forum> selectAll() { String sql = "SELECT * FROM t_forum "; System.out.println(jdbcTemplate); return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Forum.class)); } @Override public int insert(Forum forum) { System.out.println("开始插入数据"); String sql = "INSERT INTO t_forum VALUES (Null,?)"; Object[] args = {forum.getForumName()}; return jdbcTemplate.update(sql, args); } @Override publ…