Liferay使用第三方权限系统控制Portlet权限问题记录

本文主要是介绍Liferay使用第三方权限系统控制Portlet权限问题记录,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

更多最新文章欢迎大家访问我的个人博客😄:豆腐别馆

Liferay使用第三方权限系统控制Portlet权限问题记录

前言:在无法彻底摸透一个框架或技术之前,相关环境、框架等请务必选择与其相对应的版本!!!版本对不上号可能会出现各种本不该出现的异常,徒劳伤神。
在Liferay6.2 CE版中,如使用集成了Liferay ide7版的Eclipse,即使JDK、JRE都为7,也都将无法正常生成自定义外部接口的WebService客户端jar!

本文概要

  1. 使用第三方权限系统控制Portlet权限
  2. 调用对外接口时报Authenticated access required身份验证异常
  3. 服务端与客户端未在同一机器上调用对外接口出现的WebService 403问题

一、第三方权限系统控制Portlet权限

主要涉及到的技术点有:

  • 如何发布使用Liferay自带对外接口
  • 当自带对外接口不够用时如何发布自定义的对外接口
  • Liferay如何使用原生SQL语句查询
  • 调用权限相关接口时注意actionids与bitwisevalue之间的位与关系运算

1、发布调用Liferay自带对外接口,这个网上有相关资料,不做过多描述,基本流程为:下载Liferay客户端jar,下其对应版本的客户端jar即可下载链接,有如下jar包:
这里写图片描述

2、当自带对外接口不够用时,可编写自定义对外接口,可参考该篇博文:
Liferay自定义对外接口,但里面内容不知是版本问题还是其它,6.25版来说,里面的内容有误,需:
(1)将文章里导包的说明替换成上面链接下载下来的jar
(2)当执行到build-client时,若正常执行完毕,在docroot/WEB-INF下应生成client文件夹,包含xxx-portlet-client.jar和namespace-mapping.properties文件。而非portal-client.jar,复制xxx-portlet-client.jar到第三方系统,与调用Liferay自带外部接口一样调用即可。

3、这里主要是涉及到Liferay资源权限表(Resourcepermission)里的actionids与资源动作表(resourceaction)里的bitwisevalue的位与关系的判断与控制。

参考这篇博客里对于Liferay权限体系的介绍Liferay权限体系简介,

以获取对某一Portlet拥有查看权限的角色ID为例,自定义SQL查询
(1)在xxx.service.persistence里面新建xxxFinderImpl方法,继承自BasePersistenceImpl类。此处的命名必须是xxxFinderImpl(xxx为实体)
(2)执行service builder,此时会在service包的xxx.service.persistence下面生成xxxFinder的接口类和对应的xxxFinderUtil类。
(3)让xxxFinderImpl继承xxxFinder类,在此类中编写代码,如下:

public class ExternalPrivilegeFinderImpl extends BasePersistenceImpl<ResourcePermission> implements
ExternalPrivilegeFinder {@SuppressWarnings("unchecked")public List<String> getRoleIdHaveViewPermission(String name, int scope,String primkey) {List<String> list = new ArrayList<String>();StringBuffer sql = new StringBuffer("SELECT r.roleId FROM resourcepermission r WHERE r.name = '"+ name + "' ");if (scope != 0) {sql.append("AND r.scope = " + scope + " ");}if (primkey != null) {sql.append("AND r.primkey = '" + primkey + "' ");}sql.append("AND r.actionIds&1 = 1 GROUP BY r.roleId");Session session = null;try {List<BigInteger> blist = openSession().createSQLQuery(sql.toString()).list();for (BigInteger b : blist) {list.add(b.toString());}} catch (Exception e) {e.printStackTrace();} finally {closeSession(session);}return list;}
}

(4)执行ServiceBuilder,现在会在xxxFinderUtil里面生成相应的接口,但是我们不能直接调用xxxFinderUtil方法,需要将我们的这个方法添加到xxxLocalServiceImpl里面。我们在xxxLocalServiceImpl里面添加相应的方法,在xxxLocalServiceImpl里面使用xxxFinder.xxx()进行调用。
(5)再次执行build-service,现在就可以通过xxxLocalServiceUtil类调用自定义的查询类了,至此自定义查询完毕

参考博文:ServiceBuilder自定义SQL查询

(6)接下来要在第三方系统使用该接口,就跟上面提到的使用Liferay自带对外接口方法一致:build-wsdd -> deploy -> 启动Tomcat -> build-client
(7)上面的几个步骤都没问题之后,拿到生成的客户端xxx-portlet-client.jar导入第三方系统即可。

依旧以获取对某一Portlet拥有查看权限为例,由于调用相关权限接口时其scope、primKey等参数将需要动态赋值,特别当某个Portlet被多次拖拽到相同或不同页面上显示之后,其primkey将会出现变化,即在ResourcePermission表中将会出现多条不同primKey的数据,因此需要获取ResourcePermission下拥有查看权限的集合后进行遍历调用。集合获取代码如下:

/*** 获取资源查看权限列表* * @param name* @return*/
@SuppressWarnings({ "unchecked", "static-access" })
public List<ResourcePermission> getResourcePermission(String name, Long roleId) {List<ResourcePermission> list = new ArrayList<ResourcePermission>();StringBuffer sql = new StringBuffer("SELECT r.* FROM ResourcePermission r WHERE r.name = '" + name + "' ");if(roleId != null) {sql.append("AND r.roleId = " + roleId + " ");}sql.append("AND r.actionIds&1 = 1");Session session = null;try {List<Object> oList = openSession().createSQLQuery(sql.toString()).list();for(int x = 0; x < oList.size(); x ++) {Object[] obj = (Object[]) oList.get(x);BigInteger bResourcePermissionId = (BigInteger)obj[0];BigInteger bCompanyId = (BigInteger) obj[1];BigInteger bRoleId = (BigInteger) obj[5];BigInteger bOwnerId = (BigInteger) obj[6];BigInteger bActionIds = (BigInteger) obj[7];ResourcePermission r = new ResourcePermissionUtil().create(bResourcePermissionId.longValue());r.setCompanyId(bCompanyId.longValue());r.setName(obj[2].toString());r.setScope((Integer) obj[3]);r.setPrimKey(obj[4].toString());r.setRoleId(bRoleId.longValue());r.setOwnerId(bOwnerId.longValue());r.setActionIds(bActionIds.longValue());list.add(r);}} catch (Exception e) {e.printStackTrace();} finally {closeSession(session);}return list;
}

此处只需要注意下面两个点:
(1)Hibernat执行含有查询条件的原生SQL返回列表时,返回的是一个List<Object>
(2)注意代码中部分参数为long类型但数据库为BigInteger类型之间的转换。

也有疑问:在此无法使用Hibernate的addEntity()方法自动进行对象的封装,提示找不到ResourcePermission实体,因Liferay中对Hibernate进行了封装,暂未去找其映射文件/注解,因此直接采用暴力添加进List<ResourcePermission> ,如有更好解决方法者,请告诉我,O(∩_∩)O谢谢。

第三方权限系统调用测试代码(此处原返回的List<xxx>将会默认被封装成返回一个xxx实体类型的数组):

/*** 获取资源权限角色* * @param name* @return*/
public void getResourcePermission(String name, Long roleId) {try {ExternalPrivilegeServiceSoapServiceLocator privilegeLocaltor = new ExternalPrivilegeServiceSoapServiceLocator();ExternalPrivilegeServiceSoap service = privilegeLocaltor.getPlugin_t_ExternalPrivilegeService(APIUtil.getPortalURLForAddress(privilegeLocaltor.getPlugin_t_ExternalPrivilegeServiceAddress()));((Stub) service)._setProperty(Call.USERNAME_PROPERTY, USERNAME);((Stub) service)._setProperty(Call.PASSWORD_PROPERTY, PASSWORD);ResourcePermissionSoap[] resourcePermissionSoaps = service.getResourcePermission(name, roleId);for (ResourcePermissionSoap r : resourcePermissionSoaps) {System.out.println(r.getName());}} catch (ServiceException | RemoteException e) {e.printStackTrace();}
}

第三方系统系统调用:

/*** * @Title: bindbing   * @Description: TODO(绑定门户角色权限)   * @param: @param model* @param: @param portletId* @param: @param roleId* @param: @return      * @return: String      * @throws*/
@RequestMapping(value="/binding",method=RequestMethod.POST )
@ResponseBodypublic JsonVo  bindbing(String portletId,String ident,long companyId,String roleAlias){JsonVo vo = new JsonVo();try {LiferayUitl liferayUitl = new LiferayUitl();Setting setting = SystemUtils.getSetting();RoleSoap soap = liferayUitl.getRoleId(roleAlias, companyId);ResourcePermissionSoap[] privilege = liferayUitl.getPrivilege(portletId, soap.getRoleId());if (ident.equals("add")) {liferayUitl.addResourcePermission(liferayUitl.GROUPID, companyId,portletId, setting.getScope(), setting.getPrimKey(),soap.getRoleId(), liferayUitl.ACTIONID);} else {for (ResourcePermissionSoap resourcePermissionSoap : privilege) {liferayUitl.removeResourcePermission(liferayUitl.GROUPID, resourcePermissionSoap.getCompanyId(),portletId, resourcePermissionSoap.getScope(),soap.getRoleId(), liferayUitl.ACTIONID);}}vo.setSuccess(true);} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();vo.setSuccess(false);}return vo;}

二、调用对外接口时报Authenticated access required身份验证异常

异常如下:

AxisFaultfaultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server.userExceptionfaultSubcode: faultString: java.rmi.RemoteException: Authenticated access requiredfaultActor: faultNode: faultDetail: {http://xml.apache.org/axis/}hostname:PC-201606131336java.rmi.RemoteException: Authenticated access requiredat org.apache.axis.message.SOAPFaultBuilder.createFault(SOAPFaultBuilder.java:222)at org.apache.axis.message.SOAPFaultBuilder.endElement(SOAPFaultBuilder.java:129)at org.apache.axis.encoding.DeserializationContext.endElement(DeserializationContext.java:1087)at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.endElement(AbstractSAXParser.java:609)at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanEndElement(XMLDocumentFragmentScannerImpl.java:1782)at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:2973)at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:606)at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.next(XMLNSDocumentScannerImpl.java:117)at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:510)at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:848)at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:777)at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:141)at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(AbstractSAXParser.java:1213)at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(SAXParserImpl.java:648)at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl.parse(SAXParserImpl.java:332)at org.apache.axis.encoding.DeserializationContext.parse(DeserializationContext.java:227)at org.apache.axis.SOAPPart.getAsSOAPEnvelope(SOAPPart.java:696)at org.apache.axis.Message.getSOAPEnvelope(Message.java:435)at org.apache.axis.handlers.soap.MustUnderstandChecker.invoke(MustUnderstandChecker.java:62)at org.apache.axis.client.AxisClient.invoke(AxisClient.java:206)at org.apache.axis.client.Call.invokeEngine(Call.java:2784)at org.apache.axis.client.Call.invoke(Call.java:2767)at org.apache.axis.client.Call.invoke(Call.java:2443)at org.apache.axis.client.Call.invoke(Call.java:2366)at org.apache.axis.client.Call.invoke(Call.java:1812)at com.liferay.client.soap.portal.service.http.Portal_ResourcePermissionServiceSoapBindingStub.removeResourcePermission(Portal_ResourcePermissionServiceSoapBindingStub.java:250)at com.test.Test.removeResourcePermission(Test.java:112)at com.test.Test.main(Test.java:376)

出现该异常的主要原因是Liferay本身对外接口对以邮箱地址及密码的验证方式的支持不友好(不知7版该问题是否得到解决),解决该问题的方法有二:

1、修改Liferay默认认证方式,将用户认证方式更改为除邮件地址外的方式,同时调用代码处传入相对应的参数即可解决。具体流程:管理 -> 控制面板 -> Portal设置 -> 认证:

这里写图片描述

如图所示设置为通过屏幕名称认证,但若使用此种方式将会导致无法跟原先一样以邮箱登录,即将会更改为与所选认证方式一致的参数作为登录认证账号。这显然不是我想要的。因此未采用此种方式,而是使用方式2。

2、在方法调用之前提前进行身份验证:

((Stub) service)._setProperty(Call.USERNAME_PROPERTY, USERNAME);
((Stub) service)._setProperty(Call.PASSWORD_PROPERTY, PASSWORD);

Liferay的对外接口其底层其实就是WebService,Java调用WebService时,服务端如需要client客户端进行授权验证,那么这时只需要在client端提供用户名和密码即可,其实这个异常就是WebService的401异常(unauthorized未授权错误。)
调用代码示例:

/*** 添加权限* * @param groupId* @param companyId* @param name* @param scope* @param primKey* @param roleId* @param actionId* @return*/
public boolean addResourcePermission(long groupId, long companyId,String name, int scope, String primKey, long roleId, String actionId) {try {ResourcePermissionServiceSoapService localtor = new ResourcePermissionServiceSoapServiceLocator();ResourcePermissionServiceSoap service = localtor.getPortal_ResourcePermissionService(APIUtil.getPortalURLForAddress(localtor.getPortal_ResourcePermissionServiceAddress()));((Stub) service)._setProperty(Call.USERNAME_PROPERTY, USERNAME);((Stub) service)._setProperty(Call.PASSWORD_PROPERTY, PASSWORD);service.addResourcePermission(groupId, companyId, name, scope, primKey, roleId, actionId);System.out.println("添加成功");return true;} catch (ServiceException | RemoteException e) {e.printStackTrace();}return false;
}

三、服务端与客户端未在同一机器上调用对外接口时出现的WebService 403问题

一开始的服务端与客户端调用都是在我本机子上完成,并未有该异常出现,但是当客户端不在我本机上时,这异常就出来了,如下:

AxisFaultfaultCode: {http://xml.apache.org/axis/}HTTPfaultSubcode: faultString: (403)ForbiddenfaultActor: faultNode: faultDetail: {}:return code:  403
&lt;html&gt;&lt;head&gt;&lt;title&gt;Apache Tomcat/7.0.62 - Error report&lt;/title&gt;&lt;style&gt;&lt;!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--&gt;&lt;/style&gt; &lt;/head&gt;&lt;body&gt;&lt;h1&gt;HTTP Status 403 - Access denied for 192.168.2.117&lt;/h1&gt;&lt;HR size=&quot;1&quot; noshade=&quot;noshade&quot;&gt;&lt;p&gt;&lt;b&gt;type&lt;/b&gt; Status report&lt;/p&gt;&lt;p&gt;&lt;b&gt;message&lt;/b&gt; &lt;u&gt;Access denied for 192.168.2.117&lt;/u&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;description&lt;/b&gt; &lt;u&gt;Access to the specified resource has been forbidden.&lt;/u&gt;&lt;/p&gt;&lt;HR size=&quot;1&quot; noshade=&quot;noshade&quot;&gt;&lt;h3&gt;Apache Tomcat/7.0.62&lt;/h3&gt;&lt;/body&gt;&lt;/html&gt;{http://xml.apache.org/axis/}HttpErrorCode:403(403)Forbiddenat org.apache.axis.transport.http.HTTPSender.readFromSocket(HTTPSender.java:744)at org.apache.axis.transport.http.HTTPSender.invoke(HTTPSender.java:144)at org.apache.axis.strategies.InvocationStrategy.visit(InvocationStrategy.java:32)at org.apache.axis.SimpleChain.doVisiting(SimpleChain.java:118)at org.apache.axis.SimpleChain.invoke(SimpleChain.java:83)at org.apache.axis.client.AxisClient.invoke(AxisClient.java:165)at org.apache.axis.client.Call.invokeEngine(Call.java:2784)at org.apache.axis.client.Call.invoke(Call.java:2767)at org.apache.axis.client.Call.invoke(Call.java:2443)at org.apache.axis.client.Call.invoke(Call.java:2366)at org.apache.axis.client.Call.invoke(Call.java:1812)at com.liferay.client.soap.portal.service.http.Portal_ResourcePermissionServiceSoapBindingStub.addResourcePermission(Portal_ResourcePermissionServiceSoapBindingStub.java:226)at com.test.Test.addResourcePermission(Test.java:92)at com.test.Test.main(Test.java:382)

解决过程稍有点绕,先上解决方法:修改Liferay portal-tomcat下portal-setup-wizard.properties文件,在里面加上相应配置即可:

axis.servlet.hosts.allowed = 允许访问的客户端IP(以逗号分隔,为空则全部允许)
axis.servlet.https.required = false

解决思路:
上网一查,发现还真有人遇到类似的,但网上所提供的方法都是在tomcat\webapps\ROOT\WEB-INF\classes\portal-ext.properties文件中设置axis.servlet.hosts.allowed的值,一开始还有点小确幸这么快找到解决方法,但翻开我的tomcat一看,醉了,压根没有portal-ext.properties这个配置文件。总不能我新建一个上去吧(咳咳,我还真新建加上去了,但是没用- -),不知道是因为这网上复制来复制去的解决方法没经过验证还是我姿势不对,总之都不是一个好消息,那我只能回到异常本身慢慢看。
此处报的异常虽是在调用Liferay对外接口时所报,但实则依旧是WebService的范畴。而403异常,我们都知道是服务端拒绝了客户端访问,即客户端缺少相应访问权限,由此得出必定是服务端做了相应权限控制,而之前的一番查找也不是一无所获,至少我知道了在Liferay对外接口中有这么一个参数在控制着客户端权限,翻开Liferay源码一看,果然,在每一个对外的接口的xxxServiceSoap中都有这么一个注释:

You can see a list of services at http://localhost:8080/api/axis.
Set the property <b>axis.servlet.hosts.allowed</b> in portal.properties to configure security.

源码注释已经明白地说明我可以在portal.properties文件中配置axis.servlet.hosts.allowed参数来控制安全权限,还是找源码,发现在源码src下的portal.properties文件有这么一个配置:

##
## Axis Servlet
#### See the properties "main.servlet.hosts.allowed" and# "main.servlet.https.required" on how to protect this servlet.#axis.servlet.hosts.allowed=127.0.0.1,SERVER_IPaxis.servlet.https.required=false

二话不说就上Tomcat里面去找这个portal.properties配置文件去了,嘿,一找同名的文件倒不少,慢慢排除掉一些Tomcat里面portlet下的、编译后生成的、缓存下的等等文件夹下的properties,最后排除只剩下三个都在Tomcat下,文件名一致、内容一致的properties,由于不确定那只能一个个试了,但是,试了个遍,包括全部添上去,还是不行,在清除完Tomcat缓存依旧不行后无奈下就再到网上去翻找了,少有的几个帖子里(基本都一样内容的帖子- -)依旧都提及到了Tomcat下portal-ext.properties这个配置文件,啊,几个帖子总不能都空穴来风,想想我的Tomcat没有,源码上可能会有,一找源码,发现在源码src\tools\db-upgrade文件夹下还真有这么个properties,
内容如下:

jdbc.default.jndi.name=jdbc.default.driverClassName=com.mysql.jdbc.Driver
jdbc.default.url=jdbc:mysql://localhost/lportal?useUnicode=true&characterEncoding=UTF-8&useFastDateParsing=false
jdbc.default.username=
jdbc.default.password=

嘿,JDBC配置!这不是我portal-tomcat下的portal-setup-wizard.properties文件里该有的内容嘛,OK,往里加上客户端配置,一试果然异常没有了。

时间与资历有限,难免有局限之处,请谅解指导。在此感谢IT人生录里相关资料对我的帮助及博主的答疑。

这篇关于Liferay使用第三方权限系统控制Portlet权限问题记录的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/679361

相关文章

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

pdfmake生成pdf的使用

实际项目中有时会有根据填写的表单数据或者其他格式的数据,将数据自动填充到pdf文件中根据固定模板生成pdf文件的需求 文章目录 利用pdfmake生成pdf文件1.下载安装pdfmake第三方包2.封装生成pdf文件的共用配置3.生成pdf文件的文件模板内容4.调用方法生成pdf 利用pdfmake生成pdf文件 1.下载安装pdfmake第三方包 npm i pdfma

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]