-
使用先来看下如何在 vue 项目中应用的import html2canvas from "html2canvas";// 生成快照const convertToImage = (container, options = {}) => { // 设置放大倍数 const scale = window.devicePixelRatio; // 传入节点原始宽高 const _width = container.offsetWidth; const _height = container.offsetHeight; let { width, height } = options; width = width || _width; height = height || _height; // html2canvas配置项 const ops = { scale, // width, // height, useCORS: true, allowTaint: false, ...options }; return html2canvas(container, ops).then(canvas => { // 返回图片的二进制数据 return canvas.toDataURL("image/png"); });}// 调用函数,取到截图的二进制数据,对图片进行处理(保存本地、展示等)const imgBlobData = await convertToImage(element);仅此而已~~~遇到的问题如果只是这样就结束了,那这也太简单了吧,但是人无完人,再美的东西也会有瑕疵,下面列举一些 html2canvas 的问题及解决办法1、图片跨域解决方案:设置配置项 allowTaint: falsecanvas 的 CanvasRenderingContext2D 属于浏览器的对象,如果渲染过跨域资源,浏览器就认定 canvas 已经被污染了 Taint:污点设置配置项 useCORS: false表示允许跨域资源共享,注意不能与 allowTaint 同时配置为 trueimg 标签中添加 crossOrigin = "anonymous"anonymous:如果使用这个值的话就会在请求 header 中带上 Origin 属性,但请求不会带上 cookie 和客户端 ssl 证书等其他的一些认证信息图片服务器配置 Access-Control-Allow-Origin: *重要的配置项,是跨域问题的根本源泉,需要后端配合2、截图锯齿解决方案:根据设备像素比进行缩放// 设置放大倍数const scale = window.devicePixelRatio;3、截图不全解决方案:截图之前将页面滚动到顶部document.body.scrollTop = document.documentElement.scrollTop = 0;const imgBlobData = await convertToImage(element);4、对 css3 支持不好html2canvas 暂不支持的 CSS 样式属性:background-blend-mode、background-clip: text、box-decoration-break、repeating-linear-gradient()、font-variant-ligatures、mix-blend-mode、writing-mode、writing-mode、border-image、box-shadow、filter、zoom、transform解决方案:对于一些必要的样式,可以选择使用图片做兜底实现box-shadow 可以参考 这个pr,修改源码解决,但是,实际效果也不是太理想……5、svg 标签问题原因:vue-lottie 动画库渲染的标签是 svg(也可能是你自己写的 svg 标签)html2canvas 对于 svg 标签的支持也不尽人意,解决办法同样是用图片做兜底在项目中,我们是用 svg 做动画,截图的时候把动画换成一张静态图,这样只要设置要静态图的样式,截图效果还是可以接受的6、其他建议:在页面开发前尽量跟产品确认好这个页面到底要不要截图,如果需要截图,那么搬砖的时候就要注意不要使用以上 css3 特性了,否则,就后期就只能含着眼泪、咬着牙修 bug 了原文链接:https://www.jianshu.com/p/e74dab30ea2c
-
注释介绍 redis与Spring Cache的整合请参看上章 @Cacheable @Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 @Cacheable 作用和配置方法 参数 解释 example value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如: @Cacheable(value=”mycache”) @Cacheable(value={”cache1”,”cache2”} key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @Cacheable(value=”testcache”,key=”#userName”) condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @Cacheable(value=”testcache”,condition=”#userName.length()>2”) 实例 @Cacheable(value=”accountCache”),这个注释的意思是,当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key 就是参数 userName,value 就是 Account 对象。“accountCache”缓存是在 spring*.xml 中定义的名称。 @Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 System.out.println("real query account."+userName); return getFromDB(userName); } @CachePut @CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用 @CachePut 作用和配置方法 参数 解释 example value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 @CachePut(value=”my cache”) key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @CachePut(value=”testcache”,key=”#userName”) condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @CachePut(value=”testcache”,condition=”#userName.length()>2”) 实例 @CachePut 注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。 @CachePut(value="accountCache",key="#account.getName()")// 更新accountCache 缓存 public Account updateAccount(Account account) { return updateDB(account); } @CacheEvict @CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空 @CacheEvict 作用和配置方法 参数 解释 example value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 @CacheEvict(value=”my cache”) key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @CacheEvict(value=”testcache”,key=”#userName”) condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @CacheEvict(value=”testcache”,condition=”#userName.length()>2”) allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 @CachEvict(value=”testcache”,allEntries=true) beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 @CachEvict(value=”testcache”,beforeInvocation=true) 实例 @CacheEvict(value="accountCache",key="#account.getName()")// 清空accountCache 缓存 public void updateAccount(Account account) { updateDB(account); } @CacheEvict(value="accountCache",allEntries=true)// 清空accountCache 缓存 public void reload() { reloadAll() } @Cacheable(value="accountCache",condition="#userName.length() <=4")// 缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 return getFromDB(userName); } @CacheConfig 所有的@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了, 所以,有了@CacheConfig这个配置,@CacheConfig is a class-level annotation that allows to share the cache names,如果你在你的方法写别的名字,那么依然以方法的名字为准。 @CacheConfig("books") public class BookRepositoryImpl implements BookRepository { @Cacheable public Book findBook(ISBN isbn) {...} } 条件缓存 下面提供一些常用的条件缓存 //@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; @Cacheable(value = "user", key = "#id", condition = "#id lt 10") public User conditionFindById(final Long id) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'") public User conditionSave(final User user) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反) @CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'") public User conditionSave2(final User user) //@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存; @CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'") public User conditionDelete(final User user) @Caching 有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;此时就需要@Caching组合多个注解标签了。 @Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) public User save(User user) { 自定义缓存注解 比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如: @Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface UserSaveCache { } 这样我们在方法上使用如下代码即可,整个代码显得比较干净。 @UserSaveCache public User save(User user) 扩展 比如findByUsername时,不应该只放username–>user,应该连同id—>user和email—>user一起放入;这样下次如果按照id查找直接从缓存中就命中了 @Caching( cacheable = { @Cacheable(value = "user", key = "#username") }, put = { @CachePut(value = "user", key = "#result.id", condition = "#result != null"), @CachePut(value = "user", key = "#result.email", condition = "#result != null") } ) public User findByUsername(final String username) { System.out.println("cache miss, invoke find by username, username:" + username); for (User user : users) { if (user.getUsername().equals(username)) { return user; } } return null; } 其实对于:id—>user;username—->user;email—>user;更好的方式可能是:id—>user;username—>id;email—>id;保证user只存一份;如: @CachePut(value="cacheName", key="#user.username", cacheValue="#user.username") public void save(User user) @Cacheable(value="cacheName", key="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())") public User findByUsername(String username) SpEL上下文数据 Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档: 名称 位置 描述 示例 methodName root对象 当前被调用的方法名 root.methodName method root对象 当前被调用的方法 root.method.name target root对象 当前被调用的目标对象 root.target targetClass root对象 当前被调用的目标对象类 root.targetClass args root对象 当前被调用的方法的参数列表 root.args[0] caches root对象 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache root.caches[0].name argument name 执行上下文 当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数 user.id result 执行上下文 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache evict’的beforeInvocation=false) result @CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true) public void conditionUpdate(User user) ———————————————— 原文链接:https://blog.csdn.net/whatlookingfor/article/details/51833378/
-
一、介绍 spring cache 是spring3版本之后引入的一项技术,可以简化对于缓存层的操作,spring cache与springcloud stream类似,都是基于抽象层,可以任意切换其实现。 其核心是CacheManager、Cache这两个接口,所有由spring整合的cache都要实现这两个接口、Redis的实现类则是 RedisCache 和 RedisManager。 二、使用 1、查询 需要导入的依赖 org.springframework.bootgroupId> spring-boot-starter-cacheartifactId>dependency> org.springframework.bootgroupId> spring-boot-starter-data-redisartifactId>dependency> 编写对于cache的配置 @EnableCaching@SpringBootConfigurationpublic class CacheConfig { @Autowired private RedisConnectionFactory connectionFactory; @Bean // 如果有多个CacheManager的话需要使用@Primary直接指定那个是默认的 public RedisCacheManager cacheManager() { RedisSerializer redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); // 防止在序列化的过程中丢失对象的属性 om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 开启实体类和json的类型转换 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题) RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) // 不缓存空值 .disableCachingNullValues() // 1分钟过期 .entryTtl(Duration.ofMinutes(1)) ; RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); return cacheManager; }} 进行以上配置即可使用springboot cache了,还有一个key的生成策略的配置(可选) @Beanpublic KeyGenerator keyGenerator() { return (target, method, params) -> { StringBuffer key = new StringBuffer(); key.append(target.getClass().getSimpleName() + "#" + method.getName() + "("); for (Object args : params) { key.append(args + ","); } key.deleteCharAt(key.length() - 1); key.append(")"); return key.toString(); };} 注意:如果配置了KeyGenerator ,在进行缓存的时候如果不指定key的话,最后会把生成的key缓存起来,如果同时配置了KeyGenerator 和key则优先使用key。推荐:深入SpringBoot核心注解原理 在controller或者service的类上面添加 @CacheConfig ,注解里面的参数详情见下表: 在标有@CacheConfig的类里面编写一个查询单个对象的方法并添加 @Cacheable注解 @Cacheable(key = "#id", unless = "#result == null") @PatchMapping("/course/{id}")public Course courseInfo(@PathVariable Integer id) { log.info("进来了 .. "); return courseService.getCourseInfo(id);} 执行完该方法后,执行结果将会被缓存到Redis: @Cacheable注解中参数详情见下表: 2、修改 编写一个修改的方法,参数传对象,返回值也改成这个对象 @PutMapping("/course")public Course modifyCoruse(@RequestBody Course course) { courseService.updateCourse(course); return course;} 在方法上面添加 @CachePut(key = "#course.id") 注解,这个注解表示将方法的返回值更新到缓存中,注解中的参数和 @Cacheable 中的一样,这里就略过了。扩展:SpringBoot内容聚合 3、删除 编写删除方法,在方法上添加@CacheEvict 注解 @CacheEvict(key = "#id")@DeleteMapping("/course/{id}")public void removeCourse(@PathVariable Integer id) { courseService.remove(id);} @CacheEvict 的参数信息见下表: 三、 基于代码的Cache的使用 因为我们有配置的CacheManager,所以可以利用RedisCacheManager对象去手动操作cache,首先将CacheManager注入进来: @Resource private CacheManager cacheManager;@PatchMapping("/course2/{id}")public Course course2(@PathVariable Integer id) { // 获取指定命名空间的cache Cache cache = cacheManager.getCache("course"); // 通过key获取对应的value Cache.ValueWrapper wrapper = cache.get(2); if (wrapper == null) { // 查询数据库 Course course = courseService.getCourseInfo(id); // 加入缓存 cache.put(course.getId(), course); return course; } else { // 将缓存的结果返回 // 因为配置了enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // 所以在进行强转的时候不会报错 return (Course) wrapper.get(); ————————————————原文链接:https://blog.csdn.net/weixin_34476847/article/details/113050518
-
1.免费接口https://007.qq.com/product.html?ADTAG=index.block(这个是腾讯的验证码,目前是免费的,2018-09-13)2.登录注册(自己按照提示注册一个就好了,他上面也有比较完整的接入文档)3.AppID和App Secret Key(注册完成后,你会有一个AppID和App Secret Key,这个一定要记住,后面要用到,忘记了也可以登录自己的账户可以查询到)4.前端部分 --> 引入js文件 --> 激活验证码 (点击按钮,会自动激活验证码,data-cbfn:回调函数) --> 回调函数 (这里,发起的Ajax请求,主要是要在后端再做一次验证)5.后端部分 (基于.net,参考文档上有其他方式的) --> 服务器接入的url https://ssl.captcha.qq.com/ticket/verify --> 必填参数 --> 后端发起请求 --> 返回数据格式 (再在后台做二次验证,我的理解是主要是要验证当前的用户是否是请求的用户,以及前端的信息是否有做过其他更改,这样会更可靠)原文链接:https://www.cnblogs.com/LiCoco/p/9642176.html
-
shiro使用ehcache缓存 pom添加依赖 org.apache.shiro shiro-ehcache 1.4.0 在ShiroConfig中添加缓存管理器 /** * shiro缓存管理器; * 需要添加到securityManager中 * @return */ @Bean public EhCacheManager ehCacheManager(){ EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml"); return cacheManager; } 将缓存管理器添加到SecurityManager中 /** * 配置核心安全事务管理器 * @param shiroRealm * @return */ @Bean(name="securityManager") public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //设置自定义realm. securityManager.setRealm(shiroRealm()); //配置记住我 参考博客: securityManager.setRememberMeManager(rememberMeManager()); //配置 ehcache缓存管理器 参考博客: securityManager.setCacheManager(ehCacheManager()); //配置自定义session管理,使用redis 参考博客: //securityManager.setSessionManager(sessionManager()); return securityManager; } 修改shiroRealm开启缓存 /** * 身份认证realm; (这个需要自己写,账号密码校验;权限等) * @return */ @Bean public ShiroRealm shiroRealm(){ ShiroRealm shiroRealm = new ShiroRealm(); shiroRealm.setCachingEnabled(true); //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false shiroRealm.setAuthenticationCachingEnabled(true); //缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置 shiroRealm.setAuthenticationCacheName("authenticationCache"); //启用授权缓存,即缓存AuthorizationInfo信息,默认false shiroRealm.setAuthorizationCachingEnabled(true); //缓存AuthorizationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置 shiroRealm.setAuthorizationCacheName("authorizationCache"); return shiroRealm; } 在config下添加ehcache-shiro.xml name:缓存名称。 maxElementsOnDisk:硬盘最大缓存个数。0表示不限制 maxEntriesLocalHeap:指定允许在内存中存放元素的最大数量,0表示不限制。 maxBytesLocalDisk:指定当前缓存能够使用的硬盘的最大字节数,其值可以是数字加单位,单位可以是K、M或者G,不区分大小写, 如:30G。当在CacheManager级别指定了该属性后,Cache级别也可以用百分比来表示, 如:60%,表示最多使用CacheManager级别指定硬盘容量的60%。该属性也可以在运行期指定。当指定了该属性后会隐式的使当前Cache的overflowToDisk为true。 maxEntriesInCache:指定缓存中允许存放元素的最大数量。这个属性也可以在运行期动态修改。但是这个属性只对Terracotta分布式缓存有用。 maxBytesLocalHeap:指定当前缓存能够使用的堆内存的最大字节数,其值的设置规则跟maxBytesLocalDisk是一样的。 maxBytesLocalOffHeap:指定当前Cache允许使用的非堆内存的最大字节数。当指定了该属性后,会使当前Cache的overflowToOffHeap的值变为true, 如果我们需要关闭overflowToOffHeap,那么我们需要显示的指定overflowToOffHeap的值为false。 overflowToDisk:boolean类型,默认为false。当内存里面的缓存已经达到预设的上限时是否允许将按驱除策略驱除的元素保存在硬盘上,默认是LRU(最近最少使用)。 当指定为false的时候表示缓存信息不会保存到磁盘上,只会保存在内存中。 该属性现在已经废弃,推荐使用cache元素的子元素persistence来代替,如:。 diskSpoolBufferSizeMB:当往磁盘上写入缓存信息时缓冲区的大小,单位是MB,默认是30。 overflowToOffHeap:boolean类型,默认为false。表示是否允许Cache使用非堆内存进行存储,非堆内存是不受Java GC影响的。该属性只对企业版Ehcache有用。 copyOnRead:当指定该属性为true时,我们在从Cache中读数据时取到的是Cache中对应元素的一个copy副本,而不是对应的一个引用。默认为false。 copyOnWrite:当指定该属性为true时,我们在往Cache中写入数据时用的是原对象的一个copy副本,而不是对应的一个引用。默认为false。 timeToIdleSeconds:单位是秒,表示一个元素所允许闲置的最大时间,也就是说一个元素在不被请求的情况下允许在缓存中待的最大时间。默认是0,表示不限制。 timeToLiveSeconds:单位是秒,表示无论一个元素闲置与否,其允许在Cache中存在的最大时间。默认是0,表示不限制。 eternal:boolean类型,表示是否永恒,默认为false。如果设为true,将忽略timeToIdleSeconds和timeToLiveSeconds,Cache内的元素永远都不会过期,也就不会因为元素的过期而被清除了。 diskExpiryThreadIntervalSeconds :单位是秒,表示多久检查元素是否过期的线程多久运行一次,默认是120秒。 clearOnFlush:boolean类型。表示在调用Cache的flush方法时是否要清空MemoryStore。默认为true。 diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false. maxElementsInMemory:缓存最大数目 memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。 memoryStoreEvictionPolicy: Ehcache的三种清空策略; FIFO,first in first out,这个是大家最熟的,先进先出。 LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。 LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。 maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="0" timeToLiveSeconds="0" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="0" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="0" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> 在ShiroRealm的doGetAuthorizationInfo方法中添加日志,或者直接看控制台sql打印也行。 启动项目,使用admin登录,访问http://localhost:9090/userInfo/view 第一次访问,查看控制台,有sql查询,再次访问将不再打印查询数据库的日志,证明缓存生效了。上面的缓存配置时间配置为永久,请根据需求自己更改值来进行测试。 关于在ShiroRealm中对 用户信息 角色信息 权限信息的查询,如果需要添加缓存 请自行处理。 如果用户的权限发生改变怎么办? 上面已经启用了缓存,第一次请求走数据库查询,后续请求将直接查询ehcache缓存,假如这个时候在权限控制台分配了某个权限给某个角色,那么拥有这个角色的所有用户在下次请求之前都需要从数据库查询最新的权限信息。下面开始进行在权限发生改变时,该如何做: 在ShiroRealm中添加以下方法(清理缓存) /** * 重写方法,清除当前用户的的 授权缓存 * @param principals */ @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } /** * 重写方法,清除当前用户的 认证缓存 * @param principals */ @Override public void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); } /** * 自定义方法:清除所有 授权缓存 */ public void clearAllCachedAuthorizationInfo() { getAuthorizationCache().clear(); } /** * 自定义方法:清除所有 认证缓存 */ public void clearAllCachedAuthenticationInfo() { getAuthenticationCache().clear(); } /** * 自定义方法:清除所有的 认证缓存 和 授权缓存 */ public void clearAllCache() { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } 在shiroConfig中添加以下bean /** * 让某个实例的某个方法的返回值注入为Bean的实例 * Spring静态注入 * @return */ @Bean public MethodInvokingFactoryBean getMethodInvokingFactoryBean(){ MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean(); factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager"); factoryBean.setArguments(new Object[]{securityManager()}); return factoryBean; } 在UserController中添加下面两个方法 /** * 给admin用户添加 userInfo:del 权限 * @param model * @return */ @RequestMapping(value = "/addPermission",method = RequestMethod.GET) @ResponseBody public String addPermission(Model model) { //在sys_role_permission 表中 将 删除的权限 关联到admin用户所在的角色 roleMapper.addPermission(1,3); //添加成功之后 清除缓存 DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager)SecurityUtils.getSecurityManager(); ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next(); //清除权限 相关的缓存 shiroRealm.clearAllCache(); return "给admin用户添加 userInfo:del 权限成功"; } /** * 删除admin用户 userInfo:del 权限 * @param model * @return */ @RequestMapping(value = "/delPermission",method = RequestMethod.GET) @ResponseBody public String delPermission(Model model) { //在sys_role_permission 表中 将 删除的权限 关联到admin用户所在的角色 roleMapper.delPermission(1,3); //添加成功之后 清除缓存 DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager)SecurityUtils.getSecurityManager(); ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next(); //清除权限 相关的缓存 shiroRealm.clearAllCache(); return "删除admin用户userInfo:del 权限成功"; } 注意:在添加权限 或者 删除权限之后 都有调用shiroRealm.clearAllCache();来清除所有的缓存。 ———————————————— 原文链接:https://blog.csdn.net/qq_34021712/article/details/80309246
-
SpringBoot搭建的应用,一直工作得好好的,突然发现上传文件失败,提示org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not valid目录非法,实际查看目录,结果还真没有,下面就这个问题的表现,分析下SpringBoot针对文件上传的处理过程I. 问题分析0. 堆栈分析问题定位,最佳的辅助手段就是堆栈分析,首先捞出核心的堆栈信息org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not valid at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.handleParseFailure(StandardMultipartHttpServletRequest.java:122) at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:113) at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.(StandardMultipartHttpServletRequest.java:86) at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:93) at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1128) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:960) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877) at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)从堆栈内容来看,问题比较清晰,目录非法,根据path路径,进入目录,结果发现,没有这个目录,那么问题的关键就是没有目录为什么会导致异常了,这个目录到底有啥用先简单描述下上面的原因,上传的文件会缓存到本地磁盘,而缓存的路径就是上面的/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT,接着引入的疑问就是:为什么上传的文件要缓存到本地为什么临时目录会不存在什么地方实现文件缓存1. 场景模拟要确认上面的问题,最直观的方法就是撸源码,直接看代码就有点蛋疼了,接下来采用debug方式来层层剥离,看下根源再哪里。首先是搭建一个简单的测试项目,进行场景复现, 首先创建一个接收文件上传的Controller,如下@RestController @RequestMapping(path = "/file") public class FileUploadRest { /** * 保存上传的文件 * * @param file * @return */ private String saveFileToLocal(MultipartFile file) { try { String name = "/tmp/out_" + System.currentTimeMillis() + file.getName(); FileOutputStream writer = new FileOutputStream(new File(name)); writer.write(file.getBytes()); writer.flush(); writer.close(); return name; } catch (Exception e) { e.printStackTrace(); return e.getMessage(); } } @PostMapping(path = "upload") public String upload(@RequestParam("file") MultipartFile file) { String ans = saveFileToLocal(file); return ans; } }其次就是使用curl来上传文件curl http://127.0.0.1:8080/file/upload -F "file=@/Users/user/Desktop/demo.jpg" -v然后在接收文件上传的方法中开启断点,注意下面红框中的 location, 就是文件上传的临时目录2. 源码定位上面的截图可以确认确实将上传的文件保存到了临时目录,验证方式就是进入那个目录进行查看,会看到一个tmp文件,接下来我们需要确定的是在什么地方,实现将数据缓存到本地的。左边红框是这次请求的完整链路,我们可以通过逆推链路,去定位可能实现文件缓存的地方如果对spring和tomcat的源码不熟的话,也没什么特别的好办法,从上面的链路中,多打一些断点,采用传说中的二分定位方法来缩小范围。通过最开始的request对象和后面的request对象分析,发现一个可以作为参考标准的就是上图中右边红框的request#parts属性;开始是null,文件保存之后则会有数据所以关键就是org.springframework.web.filter.HiddenHttpMethodFilter#doFilterInternal 中的 String paramValue = request.getParameter(this.methodParam); 这一行代码到这里在单步进去,主要的焦点将集中在 org.apache.catalina.connector.Request#parseParts进入上面方法的逻辑,很容易找到具体的实现位置 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest,这个方法的实现比较有意思,有必要贴出来看一下public List parseRequest(RequestContext ctx) throws FileUploadException { List items = new ArrayList<>(); boolean successful = false; try { FileItemIterator iter = getItemIterator(ctx); // 注意这里,文件工厂类,里面保存了临时目录的地址 // 这个对象首次是在 org.apache.catalina.connector.Request#parseParts 方法的 FileItemFactory fac = getFileItemFactory(); if (fac == null) { throw new NullPointerException("No FileItemFactory has been set."); } while (iter.hasNext()) { final FileItemStream item = iter.next(); // Don't use getName() here to prevent an InvalidFileNameException. final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name; // 创建一个临时文件对象 FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName); items.add(fileItem); try { // 流的拷贝,这块代码也挺有意思,将输入流数据写入输出流 // 后面会贴出源码,看下开源大佬们的玩法,和我们自己写的有啥区别 Streams.copy(item.openStream(), fileItem.getOutputStream(), true); } catch (FileUploadIOException e) { throw (FileUploadException) e.getCause(); } catch (IOException e) { throw new IOFileUploadException(String.format("Processing of %s request failed. %s", MULTIPART_FORM_DATA, e.getMessage()), e); } final FileItemHeaders fih = item.getHeaders(); fileItem.setHeaders(fih); } successful = true; return items; } catch (FileUploadIOException e) { throw (FileUploadException) e.getCause(); } catch (IOException e) { throw new FileUploadException(e.getMessage(), e); } finally { if (!successful) { for (FileItem fileItem : items) { try { fileItem.delete(); } catch (Exception ignored) { // ignored TODO perhaps add to tracker delete failure list somehow? } } } } }核心代码就两点,一个是文件工厂类,一个是流的拷贝;前者定义了我们的临时文件目录,也是我们解决前面问题的关键,换一个我自定义的目录永不删除,不就可以避免上面的问题了么;后面一个则是数据复用方面的首先看下FileItemFactory的实例化位置,在org.apache.catalina.connector.Request#parseParts中具体的location实例化代码为// TEMPDIR = "javax.servlet.context.tempdir"; location = ((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR));3. 问题reviewa. 解决问题到上面,基本上就捞到了最终的问题,先看如何解决这个问题方法1应用重启方法2增加服务配置,自定义baseDirserver.tomcat.basedir=/tmp/tomcat方法3注入bean,手动配置临时目录@Bean MultipartConfigElement multipartConfigElement() { MultipartConfigFactory factory = new MultipartConfigFactory(); factory.setLocation("/tmp/tomcat"); return factory.createMultipartConfig(); }方法4配置不删除tmp目录下的tomcatvim /usr/lib/tmpfiles.d/tmp.conf # 添加一行 x /tmp/tomcat.*b. 流拷贝tomcat中实现流的拷贝代码如下,org.apache.tomcat.util.http.fileupload.util.Streams#copy(java.io.InputStream, java.io.OutputStream, boolean, byte[]) , 看下面的实现,直观影响就是写得真特么严谨public static long copy(InputStream inputStream, OutputStream outputStream, boolean closeOutputStream, byte[] buffer) throws IOException { OutputStream out = outputStream; InputStream in = inputStream; try { long total = 0; for (;;) { int res = in.read(buffer); if (res == -1) { break; } if (res > 0) { total += res; if (out != null) { out.write(buffer, 0, res); } } } if (out != null) { if (closeOutputStream) { out.close(); } else { out.flush(); } out = null; } in.close(); in = null; return total; } finally { IOUtils.closeQuietly(in); if (closeOutputStream) { IOUtils.closeQuietly(out); } } }c. 自问自答前面提出了几个问题,现在给一个简单的回答,因为篇幅问题,后面会单开一文,进行详细说明什么地方缓存文件上面的定位过程给出答案,具体实现逻辑在 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest为什么目录会不存在springboot启动时会创建一个/tmp/tomcat.*/work/Tomcat/localhost/ROOT的临时目录作为文件上传的临时目录,但是该目录会在n天之后被系统自动清理掉,这个清理是由linux操作系统完成的,具体的配置如下 vim /usr/lib/tmpfiles.d/tmp.conf# This file is part of systemd. # # systemd is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation; either version 2.1 of the License, or # (at your option) any later version. # See tmpfiles.d(5) for details # Clear tmp directories separately, to make them easier to override v /tmp 1777 root root 10d v /var/tmp 1777 root root 30d # Exclude namespace mountpoints created with PrivateTmp=yes x /tmp/systemd-private-%b-* X /tmp/systemd-private-%b-*/tmp x /var/tmp/systemd-private-%b-* X /var/tmp/systemd-private-%b-*/tmp为什么要缓存文件因为流取一次消费之后,后面无法再从流中获取数据,所以缓存方便后续复。原文链接:https://www.cnblogs.com/yihuihui/p/10372887.html
-
RSA加密算法简介SA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到目前为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。RSA加密的java实现实现的思路,由RSA随机生成一对公钥和私钥,公钥方到客户端,私钥放到服务端,发送数据的时候由公钥对传输数据进行加密,然后发送给服务端,服务端用私钥才能对数据进行解密.下面是代码实现的例子package com.yihur.demoimport org.apache.commons.codec.binary.Base64;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.crypto.BadPaddingException;import javax.crypto.Cipher;import javax.crypto.IllegalBlockSizeException;import javax.crypto.NoSuchPaddingException;import java.nio.charset.StandardCharsets;import java.security.*;import java.security.interfaces.RSAPrivateKey;import java.security.interfaces.RSAPublicKey;import java.security.spec.InvalidKeySpecException;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;import java.util.HashMap;import java.util.Map;/** * @author yihur * @description RSA加密 * @date 2019/4/3 */public class MyRSAencryptionMethod { private static Logger logger = LoggerFactory.getLogger(MyRSAencryptionMethod .class); /** * 用于封装随机产生的公钥与私钥 * * @author yihur * @date 2019/4/4 * @param * @return */ private static Map keyMap = new HashMap<>(); /** * 测试方法 * * @param args * @return void * * * 前端用crypto-js进行加密, * npm i jsencrypt, * 然后页面头引入import JSEncrypt from 'jsencrypt'; * const encrypt = new JSEncrypt(); * encrypt.setPublicKey('你的公钥'); * password = encrypt.encrypt(‘你的密码’);// 加密后的字符串 * @author yihur * @date 2019/4/4 */ public static void main(String[] args) { //生成公钥和私钥 genKeyPair(); //加密字符串 String message = "df723820"; System.out.println("随机生成的公钥为:" + keyMap.get(0)); System.out.println("随机生成的私钥为:" + keyMap.get(1)); String messageEn = encrypt(message, keyMap.get(0)); System.out.println("加密后的字符串为:" + messageEn); String messageDe = decrypt(messageEn, keyMap.get(1)); System.out.println("还原后的字符串为:" + messageDe); } /** * 随机生成密钥对 * * @param * @return void * @author yihur * @date 2019/4/4 */ public static void genKeyPair() { // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 KeyPairGenerator keyPairGen = null; try { keyPairGen = KeyPairGenerator.getInstance("RSA"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); logger.info(e.getMessage()); } // 初始化密钥对生成器,密钥大小为96-1024位 assert keyPairGen != null; keyPairGen.initialize(1024, new SecureRandom()); // 生成一个密钥对,保存在keyPair中 KeyPair keyPair = keyPairGen.generateKeyPair(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到私钥 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 得到公钥 String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded())); // 得到私钥字符串 String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded()))); // 将公钥和私钥保存到Map keyMap.put(0, publicKeyString); //0表示公钥 keyMap.put(1, privateKeyString); //1表示私钥 } /** * RSA公钥加密 * * @param str 加密字符串 * @param publicKey 公钥 * @return 密文 */ public static String encrypt(String str, String publicKey) { //base64编码的公钥 byte[] decoded = Base64.decodeBase64(publicKey); RSAPublicKey pubKey = null; String outStr = null; try { pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, pubKey); outStr = Base64.encodeBase64String(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8))); } catch (InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException | InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException e) { e.printStackTrace(); logger.info(e.getMessage()); } //RSA加密 return outStr; } /** * RSA私钥解密 * * @param str 加密字符串 * @param privateKey 私钥 * @return 铭文 */ public static String decrypt(String str, String privateKey) { //64位解码加密后的字符串 byte[] inputByte = Base64.decodeBase64(str.getBytes(StandardCharsets.UTF_8)); //base64编码的私钥 byte[] decoded = Base64.decodeBase64(privateKey); RSAPrivateKey priKey = null; //RSA解密 Cipher cipher = null; String outStr = null; try { priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded)); cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, priKey); outStr = new String(cipher.doFinal(inputByte)); } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidKeyException e) { e.printStackTrace(); logger.info(e.getMessage()); } return outStr; }}RSA加密的前端用法前端用crypto-js进行加密,npm i jsencrypt,然后页面头引入import JSEncrypt from 'jsencrypt';const encrypt = new JSEncrypt();encrypt.setPublicKey('你的公钥');password = encrypt.encrypt(‘你的密码’);// 加密后的字符串后续在实际应用中RSA加密也还是远远不够,一般还会加入MD5加密的方式,以及加密验证,token等等方式作为请求连接的校验,比如后端加密一个MD5字符串,给前端之后,前端用特定组合加上传输数据返回一个RSA加密的字符串,后端接收后解密,然后和自身的字符串进行对比,以确认数据来源的准确性.原文链接:https://www.jianshu.com/p/d1e303b976aa
-
sourceMap的作用 通常,js代码出错,控制台会提示第几行第几列代码出错。但是webpack打包压缩后的代码,都被压缩到了一行,变量也变成了a,b,c,d。代码出错,控制台就没法正确的提示错误位置。 sourceMap就可以解决这个问题。sourceMap就是一个信息文件,里面储存着打包前的位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。具体如何编码,可以看下阮一峰的文章。 有了它,出错的时候,浏览器控制台将直接显示原始代码出错的位置,而不是转换后的代码,点击出错信息将直接跳转到原始代码位置。 sourceMap的调试 Console直接点击错误提示跳转到源码(如上图所示) 一般浏览器会默认开启sourceMap调试,如果控制台不能显示源码错误位置,则需要打开配置。 打开浏览器的这个配置后,控制台的Console面板的错误提示,直接点击就会跳转到对应的源文件出错位置。 Sources面板找到源文件,进行断点调试 如果没Console没报错,但是页面显示不正确。可以点击控制台的Sources面板,源文件都在 webpack:// 目录下,或者直接搜索文件,打开源文件后进行断点调试。 按需加载的路由,只有页面加载了,源文件才会在这个目录下显示。 webpack配置 vue-cli3的vue.config.js文件: module.exports = { productionSourceMap: false, //默认是true } 打包后的文件: 对于打包后的sourceMap,webpack提供多种类型的配置。 其中,开发环境使用: eval:会把每个模块封装到 eval 里包裹起来执行,并且会在末尾追加map文件的地址。map文件映射到转换后的代码(可执行的js文件),而不是映射到原始代码(vue文件),所以不能正确的显示错误行数。 eval-source-map: 和 eval 类似,为每个模块生成原始的sourceMap,map文件会以dataURL的形式添加到js中(类似于图片的base64形式)。原始的sourceMap可以正确提示错误行数。 eval-cheap-source-map: 跟eval-source-map相同,唯一不同的就是增加了cheap,cheap是指忽略了列信息(绝大部分时候列信息对于错误提示没啥用,只需要提示行数就行)。 cheap-module-eval-source-map: 与eval-cheap-source-map相同,但是包含了不同loader模块之间的sourceMap。例如借助babel编译ES6,如果生成不包含loader的sourcemap,此时debug到的将是编译后的代码,而非原始代码。 生产环境使用: source-map: map文件包含完整的原始代码,但是打包会很慢。打包后的js最后一行是map文件地址的注释 hidden-source-map:与 sourceMap 相同,也生成map文件,但是打包后的js最后没有map文件地址的引用。浏览器不会主动去请求map文件,一般用于网站错误分析,需要让错误分析工具按名称匹配到map文件。 nosources-source-map:生成的map文件不包含源码,但是会正确提示错误的行数。另外项目的目录结构和文件名称会暴露在Sources面板 特定环境用(针对一些第三方工具,用的很少,暂不详细讨论): inline-source-map inline-cheap-source-map inline-cheap-module-source-map cheap-source-map cheap-module-source-map 其实就是inline,cheap,module,source-map的自由组合。 inline 是以dateURL的形式添加map,不额外生成map文件 cheap 是没有列信息 module 是包含了loader的sourcemap source-map 则是映射到源文件 不同的参数,生成的sourceMap不同,打包速度、体积、错误提示的效果也不同。而vue-cli3帮我们预选了一种模式: 开发环境,配置文件位置:node_modules\\@vue\cli-service\lib\config\dev.js,第5行 生产环境,配置文件位置: \node_modules\\@vue\cli-service\lib\config\prod.js,第11行 另外:css同样有sourceMap,不过vue-cli3是默认关闭的: module.exports = { css: { sourceMap: false, //默认false } } sourceMap的安全问题 事情都是具有两面性的,方便调试的同时,也将源码暴露在控制台,可能会导致代码泄露的安全问题。 虽然说前端代码是公开的,但是代码的压缩混淆也一定程度上提高了安全性。 代码泄露可能导致的问题: 代码被抄袭 场景:给A客户写了一个地图功能,A客户展示给B客户,B客户想要写地图,但是不会,查看源码后,抄走了地图功能 业务流失 场景:竞争对手拿到了源码,挑出其中的漏洞或者缺陷,大肆宣传。或者直接写一份后台,成本少,价格低导致业务被抢走 系统被攻击 场景:由于项目急,鉴权是通过前端动态生成路由控制的,后台没做判断,导致修改源码后,可以获得更多的权限(数据被删除) 例如常见的视频播放网站,如果不加密,直接一个标签,那么就很容易被人拿到视频资源,自己的会员业务也会损失。 sourceMap的处理 如果无特殊需求,生产环境是需要关闭这个选项的,vue-cli3直接配置productionSourceMap: false即可。或者不关闭但是在测试环境迁移到正式环境时删掉map文件。也可以通过服务器配置,特殊账号(调试专用)能访问到map文件,其他用户则不行。 如果需要控制台能正确提示报错的位置而不暴露源码,推荐用nosources-source-map模式,但是这个模式会暴露源码的目录结构与文件命名。一般测试环境用这个比较好,QA测试出来的问题能正确提示错误,即使运维忘了删除sourceMap文件,也不会暴露源码。 // vue.config.js module.exports = { configureWebpack: config => { if (process.env.NODE_ENV === 'production') {//只修改生产环境配置 return { devtool: 'nosources-source-map', }; } } } 补充:本地sourceMap调试 办法1 : 网络拦截 webpack打包仍然生成sourceMap,但是将map文件挑出放到本地服务器,将不含有map文件的部署到服务器,借助第三方软件(例如fiddler),将浏览器对map文件的请求拦截到本地服务器,就可以实现本地sourceMap调试。 办法2 : 修改webpack打包,测试环境不放map文件,修改sourceMap的引用到本地服务器 可行性:本地调试专用,由于测试环境和本机属于同一个局域网,所以测试环境可以请求到本地服务器上的sourceMap文件,部署之后,客户访问则不能访问到本机的局域网IP,也就无法拿到sourceMap文件。 缺点:其他人打包需要修改电脑IP和本地服务器文件地址,无法直接复用,优点是绝对安全。或者可以提供另一台专门放map文件的服务器,打包之后自动上传map文件到这个服务器。 借助SourceMapDevToolPlugin修改打包后的文件对map文件的引用地址,同时借助filemanager-webpack-plugin将map文件移到到本地服务器上。代码如下(vue.config.js): const webpack = require('webpack')const FileManagerPlugin = require('filemanager-webpack-plugin');module.exports = {configureWebpack: config => {if (process.env.NODE_ENV === 'production') {return {devtool: false, // 注意SourceMapDevToolPlugin和devtool不可共存plugins: [new webpack.SourceMapDevToolPlugin({append: '\n//# sourceMappingURL=http://192.168.23.230/sourceMap/', // 192.168.23.230是本机局域网IPfilename: '[file].map',}),new FileManagerPlugin({onEnd: {copy: [{source: './dist/js/*.map',destination: 'D:/xampp/htdocs/sourceMap', // D:/xampp/htdocs/是本地服务器的文件地址}],delete: ['./dist/js/*.map'],archive: [{ //顺便把网站文件压缩一下source: './dist',destination: './dist/dist.zip',}]}})]};}}}这个插件主要是实现了对sourceMap的更精细控制,比如打包之后的位置,以及修改js/css文件最后面对sourceMap的引用地址。 这里主要用到修改sourceMap引用地址的功能。 使用这个必须关闭devtool 这个插件的主要功能是打包之后文件的移动,删除,拷贝,压缩等功能,使用之前需要先安装: npm install filemanager-webpack-plugin --save-dev 注意: 这个插件有bug,移动操作不支持模糊匹配,只能改成复制+删除 ———————————————— 原文链接:https://blog.csdn.net/weixin_31047023/article/details/113687529
-
1. XSS是什么XSS就是跨站脚本攻击,全称是Cross-Site Scripting,简称不是CSS是因为和层叠样式表重名了所以改称为XSS,其中X是代表未知并且有扩展的含义。XSS攻击涉及到三个方面:攻击者、用户、web server。其中用户是通过浏览器来访问web server上的网页,XSS攻击就是攻击者通过各种办法,在用户访问的网页中插入自己的脚本,让其在用户访问网页时在其浏览器中进行执行。攻击者通过插入的脚本的执行,来获得用户的信息,比如cookie,发送到攻击者自己的网站(跨站了),所以这种攻击被称为跨站脚本攻击,即在用户的浏览器中执行攻击者自己定制的脚本。2.XSS的类型XSS可以分为反射型XSS、持久性XSS、DOM Based XSS。2.1 反射型XSS反射性XSS也称为非持久性XSS,它是用户点击攻击链接,服务器解析后响应,在返回的响应内容中出现攻击者的XSS代码,被浏览器去执行。XSS攻击脚本被web server反射回来给浏览器执行,所以称为反射型XSS。它没有保存在web server中,而是出现在URL地址中,一般是攻击者通过邮件,聊天软件等等方式发送攻击URL,然后用户点击来达到攻击的;2.2 持久型XSS和反射型不同的是这种XSS攻击是将恶意代码存储在web server中,这样每个访问特定网页的用户都会被攻击到,一般是通过网站的留言、评论、博客、日志等等所有能够向web server输入内容的地方,将攻击代码存储到web server上的;有时持久性XSS和反射型XSS是被同时使用的,比如先通过对一个攻击url进行编码来绕过xss filter,然后提交该web server并存储在web server中,最后在用户浏览特定页面如果点击该url,就会触发一个XSS攻击,同时当用户点击该url的时候,也可能会触发一个CSRF(Cross site request forgery)的攻击。2.3 DOM based XSS这种XSS攻击是基于DOM的XSS攻击,是只涉及到浏览器的XSS攻击。比如根据用户的输入来动态构造一个DOM节点,如果没有对用户的输入进行过滤,那么也就导致XSS攻击的产生,过滤可以考虑采用esapi4js进行过滤。3. XSS 攻击的防御XSS防御的总体思路是:对输入(和URL参数)进行过滤,对输出进行编码,也就是对提交的所有内容进行过滤,对url中的参数进行过滤,过滤掉会导致脚本执行的相关内容,对动态输出到页面的内容进行html编码,使脚本无法在浏览器中执行。采取这些方法虽然对输入过滤可以被绕过,但是也还是会拦截很大一部分的XSS攻击。对于输入的防御处理使用XSS filter之外,需要对于在客户端和服务器端进行各种入参验证,需要验证字符的合法性、长度、格式等等,即分为黑名单和白名单,黑名单表示验证中不能出现某些字符,白名单表示可以出现的字符;对于输出数据的防御,可以对存在潜在威胁的字符进行编码、转义。总的来说输入防御可以采用AntiSamy提供的XSS Filter来实现,对输出进行防御可以采用ESAPI提供的编码来实现。
-
1.如何生成随机数在Java编程过程中,我们常常会需要实现随机数,那么如果需要生成一个指定范围之内的随机数字要怎么来实现呢?答案是有两种实现的方法:一种是调用Math类的random() 方法,一种是使用Random随机类。2.Math 类Math 类是在java.lang包下的类,它是数学相关的工具类,里面提供了大量的静态方法,可以直接进行调用用来完成与数学运算相关的操作。如果要通过Math类来实现随机数的生成的,需要借助random()方法来的进行实现,random()方法没有参数,它会默认返回大于等于 0.0小于1.0 double类型随机数,即 0<=随机数<1.0。对 random() 方法返回的数字进行乘法就可以实现产生任意范围随机数的功能。比如要生产[0,100),即生成大于等于0,小于100的随机整数:int i = (int) (Math.random() * 100);2.Random类Random类是java.util包下的类,它提供了丰富的随机数生成方法(顾名思义嘛,名字就是随机数,当然专门进行随机数操作的),可以产生boolean、int、long、float、byte数组以及double类型的随机数,这是它与 random() 方法最大的不同之处,因为random()只能产生double类型的大于等于0小于1的随机数。Random类两个构造方法:一个是无参的Random():该构造方法使用一个和当前系统时间对应的数字作为种子数,然后使用这个种子数构造 Random 对象。一个是有参的Random(long seed),random里其实是一个数列,这个数列每一位的数字接近随机分布,seed是种子数,设置用于随机数生成器的初始化值。因为Random类提供的所有方法生成的随机数字都是均匀分布的,所以说区间内部的数字生成的概率都是均等的。下面是Random类生成int、double、long、boolean、float类型的随机数常用方法:Random r = new Random(); double d1 = r.nextDouble(); // 生成[0,1.0]区间的小数 double d2 = r.nextDouble() * 10; // 生成[0,10.0]区间的小数 int i1 = r.nextInt(10); // 生成[0,10]区间的整数 int i2 = r.nextInt(10) - 3; // 生成[-1,9)区间的整数 long l1 = r.nextLong(); // 生成一个随机长整型值 boolean b1 = r.nextBoolean(); // 生成一个随机布尔型值 float f1 = r.nextFloat(); // 生成一个随机浮点型值以上就是随机数的简单应用和实现方法的小知识的汇总。
-
1.#{}的用法在Java编程过程中,我们肯定是少不了和SQL打交道的,无论的是查询的SQL还是新增、修改、删除的SQL我们都会传对应数据的不断变化的参数值,我们不能每次传值就去修改SQL,需要一个占位符来接收这个可变的参数值,这样在Mapper.xml映射文件中,我们经常会使用#{属性名} 来作为SQL语句的占位符,来映射Sql需要的实际参数值。1.1 一个参数的占位符使用如果只有一个参数我们只需要在对应的字段后面跟上#{属性值}就可以接收传递过来的参数值,也就是说#{}起到的作用就是一个预编译的占位符的作用,运行代码的时候会编译成 ?,但这只适用于只有一个参数的情况,而且这种一个参数的情况#{id}中的id可以写成任何字符串,比如#{aaa} select * from test where id=#{id}1.2 多个参数的占位符使用1.2.1 按照参数的顺序,通过使用自然序号对应所传递的参数来对应标注出传递的参数位置,这种方法需要对代码和业务逻辑比较熟悉,因为是使用自然序号来进行标注参数的位置的,所以稍微错位就会造成整个语句的混乱,所以不太推荐这种形式。 select * from test where id=#{0} and name=#{1}1.2.2 跟第一种比较类似,只是改为按照顺序通过param1、param2来进行标注参数的位置,这种形式跟第一种比较相似,如果标注的稍微有错位的情况也会造成整个语句的混乱,所以也不是很推荐这种形式的编码。 select * from test where id=#{param1} and name=#{param2}1.2.3 利用接口Mapper中对应的参数,必须保证xml和接口Mper中的参数名称保持一致,这样对应起来代码的可读性就增加了很多,而且也不容易出现错位造成整个语句混乱这种错位,因为是接口Mapper中的参数每个都必须一一对应到sql中才能使用,所以准确率肯定会提高很多,因此推荐这种使用占位符#{}的方法来进行使用。public interface TestMapper { public Test getUserByNameAndAge(@Param("id") Integer id, @Param("name") String name);} select * from test where id=#{id} and name=#{name}2.${}的用法${}的用法和#{}的用法都是占位符的作用,但是它们的不同在于#{}会被编译成?,而${}则会被原样输出,并且用在参数上的时候,需要使用param注解。3.#{}和${}的区别#{}会对SQL进行预编译会将占位符的位置编译成?,${}是做sql拼接会原样输出,会有SQL注入的隐患。#{}不需要关注数据类型,MyBatis会自动实现数据类型的转换; ${} 必须自己判断数据类型(需要再接口Mapper中使用@param注解进行标注),两者相同之处都是占位符的作用,都是支持@param注解。因为${}有SQL注入隐患,所以我们一般在Sql中使用#{}来进行参数的占位。
-
1.为什么要使用Java链式编程为什么要使用Java链式编程呢?我们在Java语言编程的过程中,肯定少不了创建对象的,但是创建对象后,我们设置属性需要不停的使用对象.setXXX( )方法,然后再换行继续使用对象.setXXX( )方法,这样创建出来的对象需要被我们重复的写很多遍,效率低而且代码可读性也很差,怎么能让我们代码能不用重复书写对象而更优雅的编写出来呢,链式编程就能帮我们实现这个效果,我们可以在创建对象后直接在后面.setXXX().setXXX()...这样链式的设置完各个属性。2.Java链式编程的优缺点链式编程可以使得代码可读性变得更高,链式编程也比较好实现,它的原理就是返回一个this对象,即返回对象本身,这样就可以达到链式的效果。2.1 链式编程的优点有:编程性更强、可读性更强、代码更简洁。2.2 链式编程的缺点有:对程序员的业务能力要求更高、不太利于代码调试。3.Java链式编程的实现3.1 之前我们创建对象后设置属性是这样的,需要多次重复的写出对象的名字.set去设置:public class Student{ private Integer id; private String name; private String email; private String phone; /** setter/getter方法省略 */}...Student s = new Student();s.setId(1);s.setEmail("xxxx@123.com");s.setName("zhangsan");user.setPhone("1135654635");3.2 通过简单改造,我们将各个属性中的set方法的返回值设置为对象本身,并且返回类型设置为对象本身的类型这样就可以实现链式编程了,很明显代码简捷了很多,如果遇到很多属性的情况这会显得更加明细。 public Student setId(Integer id) { this.id = id; return this; } public Student setName(String name) { this.name = name; return this; } public Student setEmail(String email) { this.email = email; return this; } public Student setPhone(String phone) { this.phoneNum = phone; return this; }...Student s = new Student();s.setId(1).setName("zhangsan").setPhone("113334343")setEmail("xxxxx@123.com");3.3 lombok实现的方式项目中我们常常引入lombok来简化编程,只需要@Data就可以省略有参、无参构造方法以及属性中的set和get方法,那么get和set方法已经省略了,我们要如何设置set方法中的返回值和返回类型呢?其实lombok已经帮我们实现了,只需增加@Accessors(chain = true) 注解就可开启链式编程了。@Accessors(chain = true)@Data@AllArgsConstructor@NoArgsConstructorpublic class Student{ private Integer id; private String name; private String email; private String phone; }以上就是Java链式编程的简单实现和使用,让我们的代码变得更优雅简捷起来吧~
-
1、Content-Type 的简介Content-Type即互联网媒体类型,也叫MIME类型。在互联网HTTP传输数据对象中我们会为他们打上这个Content-Type的数据格式标签,用于区分数据类型。最初Content-Type只是用于电子邮件系统,随着慢慢的使用推广,我们在HTTP中也采用了这一套规范方案。现在在HTTP协议消息头中,我们使用Content-Type来表示请求和响应中的媒体类型信息,以此用来告诉服务端如何处理请求的数据,以及告诉客户端(一般是浏览器)如何解析响应的数据,比如显示图片,解析并展示html等等。2、Content-Type的常见格式类型Content-Type格式规范是这样的(Content-Type:type/subtype ;parameter) ,其中参数如下:type:主类型,任意的字符串,如text,如果是*号代表所有;subtype:子类型,任意的字符串,如html,如果是*号代表所有,用“/”与主类型隔开;parameter:可选参数,如charset,boundary等。常见的栗子有:HTML文档标记:text/html;普通ASCII文档标记:text/html;JPEG图片标记:image/jpeg;GIF图片标记:image/gif;js文档标记:application/javascript;xml文件标记:application/xml;json字符串并且使用utf-8编码:application/json;charset:utf-8;3.常用类型简介3.1 application/x-www-form-urlencodedHTTP会将请求参数用key1=val1&key2=val2的方式进行拼接然后放到请求实体里面,需要注意的是,如果是中文或特殊字符如"/"、","、“:" 等会自动进行URL转码,并且这种类型不支持文件,一般是用于表单提交的。3.2 multipart/form-data它是一个多部分多媒体类型,首先生成了一个 boundary 用于分割不同的字段,在请求实体里每个参数以boundary开始,然后是附加信息和参数名,然后是空行,最后是参数内容。多个参数将会有多个boundary块。如果参数是文件会有特别的文件域。最后以boundary为结束标识。multipart/form-data是支持文件上传的格式的,一般用于上传文件的表单。3.3 application/json顾名思义是以JSON这种轻量级的数据格式,以“键-值”对的方式拼接数据进行传输的。使用时需要参数本身就是json格式的数据,参数会被直接放到请求实体里,不进行任何处理。服务端/客户端会按json格式解析数据,一般我们在服务端创建实体类进行接收解析。3.4 application/xml 和 text/xml与application/json类似,是传输xml格式的数据,如果使用text/xml类型会忽略xml数据里的编码格式。4、Content-Type的使用 4.1 request中的Content-Type客户端发出的Request请求中的的Content-Type设置必须要准确,如果不准确的话可能会导致请求失败。在spring中,如果接口使用了@RequestBody,spring的自动解析功能会将请求实体的内容自动转换为对应的Bean,但前提是请求的Content-Type必须设置为application/json,否正就会返回415错误(415 错误是 Unsupported media type,即不支持的媒体类型)。在使用的时候可以这样来配置:如果是一个restful接口(json格式),一般将Content-Type设置为application/json; charset=UTF-8;如果是文件上传,一般Content-Type设置为multipart/form-data如果普通表单提交,一般Content-Type设置为application/x-www-form-urlencoded4.2 response的Content-Type服务端Response响应的Content-Type也必须准确,虽然前端解析响应的数据不会根据Content-Type,并且服务端一般能自动设置准确的Content-Type,但是如果乱设置某些情况下可能会有问题,比如导出文件,打开图片等。如果在spring项目里使用@ResponseBody,spring会将响应的Content-Type设置为application/json;charset=UTF-8;,可能会导致文件无法导出,需要注意下。在使用的时候可以这样来配置:如果是文件导出,Content-Type 设置为 multipart/form-data,并且添加一个Content-Disposition设置为attachment;fileName=文件.后缀。其中Content-Disposition是Content-Type的扩展,告诉浏览器弹窗下载框,而不是直接在浏览器里展示文件。因为一般浏览器对于它能够处理的文件类型,如txt,pdf 等,它都是直接打开展示,而不是弹窗下载框。以上就是Content-Type的使用和小知识点的总结。
-
rt,公司没有git托管,需要自己写pom,但没接触过FI平台,不知道是不是跟CDH一样有对应CDH版本的依赖包,所以求一个
匿名用户
发表于2022-08-26 16:58:00
2022-08-26 16:58:00
最后回复
匿名用户
2022-08-26 17:54:11
176 3 -
比较原始的一种调用做法,这里把get请求和post请求都统一放在一个方法里面。实现过程:GET:1、创建远程连接2、设置连接方式(get、post、put。。。)3、设置连接超时时间4、设置响应读取时间5、发起请求6、获取请求数据7、关闭连接POST:1、创建远程连接2、设置连接方式(get、post、put。。。)3、设置连接超时时间4、设置响应读取时间5、当向远程服务器传送数据/写数据时,需要设置为true(setDoOutput)6、当前向远程服务读取数据时,设置为true,该参数可有可无(setDoInput)7、设置传入参数的格式:(setRequestProperty)8、设置鉴权信息:Authorization:(setRequestProperty)9、设置参数10、发起请求11、获取请求数据12、关闭连接直接上代码:package com.riemann.springbootdemo.util.common.httpConnectionUtil; import org.springframework.lang.Nullable; import java.io.*;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.net.URLConnection; /** * @author riemann * @date 2019/05/24 23:42 */public class HttpURLConnectionUtil { /** * Http get请求 * @param httpUrl 连接 * @return 响应数据 */ public static String doGet(String httpUrl){ //链接 HttpURLConnection connection = null; InputStream is = null; BufferedReader br = null; StringBuffer result = new StringBuffer(); try { //创建连接 URL url = new URL(httpUrl); connection = (HttpURLConnection) url.openConnection(); //设置请求方式 connection.setRequestMethod("GET"); //设置连接超时时间 connection.setReadTimeout(15000); //开始连接 connection.connect(); //获取响应数据 if (connection.getResponseCode() == 200) { //获取返回的数据 is = connection.getInputStream(); if (null != is) { br = new BufferedReader(new InputStreamReader(is, "UTF-8")); String temp = null; while (null != (temp = br.readLine())) { result.append(temp); } } } } catch (IOException e) { e.printStackTrace(); } finally { if (null != br) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != is) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } //关闭远程连接 connection.disconnect(); } return result.toString(); } /** * Http post请求 * @param httpUrl 连接 * @param param 参数 * @return */ public static String doPost(String httpUrl, @Nullable String param) { StringBuffer result = new StringBuffer(); //连接 HttpURLConnection connection = null; OutputStream os = null; InputStream is = null; BufferedReader br = null; try { //创建连接对象 URL url = new URL(httpUrl); //创建连接 connection = (HttpURLConnection) url.openConnection(); //设置请求方法 connection.setRequestMethod("POST"); //设置连接超时时间 connection.setConnectTimeout(15000); //设置读取超时时间 connection.setReadTimeout(15000); //DoOutput设置是否向httpUrlConnection输出,DoInput设置是否从httpUrlConnection读入,此外发送post请求必须设置这两个 //设置是否可读取 connection.setDoOutput(true); connection.setDoInput(true); //设置通用的请求属性 connection.setRequestProperty("accept", "*/*"); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); connection.setRequestProperty("Content-Type", "application/json;charset=utf-8"); //拼装参数 if (null != param && param.equals("")) { //设置参数 os = connection.getOutputStream(); //拼装参数 os.write(param.getBytes("UTF-8")); } //设置权限 //设置请求头等 //开启连接 //connection.connect(); //读取响应 if (connection.getResponseCode() == 200) { is = connection.getInputStream(); if (null != is) { br = new BufferedReader(new InputStreamReader(is, "GBK")); String temp = null; while (null != (temp = br.readLine())) { result.append(temp); result.append("\r\n"); } } } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { //关闭连接 if(br!=null){ try { br.close(); } catch (IOException e) { e.printStackTrace(); } } if(os!=null){ try { os.close(); } catch (IOException e) { e.printStackTrace(); } } if(is!=null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } //关闭连接 connection.disconnect(); } return result.toString(); } public static void main(String[] args) { String message = doPost("https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13026194071", ""); System.out.println(message); }}运行结果:
上滑加载中
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第五期2026/09/04 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;蒋春阳-华为云AI开发者案例开发专家
本期直播内容: AI工具体验营 · 第5-8课连讲。Agent-Team 多智能体协作完成毕业设计实践
回顾中 -
华为云开发者AI素养ClassRoom·第六期2026/09/08 周二 19:00-20:00
樊渊-2026华为软件挑战赛冠军
高手来了:看软挑高手解析二维排样问题—从工业难题到算法突破
回顾中
热门标签