Skip to content
项目
群组
代码片段
帮助
正在加载...
帮助
提交反馈
为 GitLab 提交贡献
登录
切换导航
iBiz4j Spring R7
项目
项目
详情
动态
版本
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
计划
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
统计图
创建新议题
作业
提交
议题看板
打开侧边栏
iBiz-R7后台标准模板
iBiz4j Spring R7
提交
cb68ac3e
提交
cb68ac3e
编写于
6月 11, 2020
作者:
zhouweidong
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
token秘钥
上级
949f889e
变更
5
隐藏空白字符变更
内嵌
并排
正在显示
5 个修改的文件
包含
304 行增加
和
160 行删除
+304
-160
IBZUAAFallback.java.ftl
...in/java/%SYS_PKGPATH%/util/client/IBZUAAFallback.java.ftl
+5
-0
IBZUAAFeignClient.java.ftl
...java/%SYS_PKGPATH%/util/client/IBZUAAFeignClient.java.ftl
+5
-0
AuthTokenUtil.java.ftl
...n/java/%SYS_PKGPATH%/util/security/AuthTokenUtil.java.ftl
+4
-160
SimpleTokenUtil.java.ftl
...java/%SYS_PKGPATH%/util/security/SimpleTokenUtil.java.ftl
+174
-0
UAATokenUtil.java.ftl
...in/java/%SYS_PKGPATH%/util/security/UAATokenUtil.java.ftl
+116
-0
未找到文件。
SLN/%PUBPRJ%-util/src/main/java/%SYS_PKGPATH%/util/client/IBZUAAFallback.java.ftl
浏览文件 @
cb68ac3e
...
...
@@ -25,4 +25,9 @@ public class IBZUAAFallback implements IBZUAAFeignClient {
public
AuthenticationUser
loginByUsername
(
String
username
)
{
return
null
;
}
@
Override
public
String
getPublicKey
()
{
return
null
;
}
}
SLN/%PUBPRJ%-util/src/main/java/%SYS_PKGPATH%/util/client/IBZUAAFeignClient.java.ftl
浏览文件 @
cb68ac3e
...
...
@@ -5,6 +5,7 @@ package ${pub.getPKGCodeName()}.util.client;
import
${
pub
.
getPKGCodeName
()}.
util
.
security
.
AuthenticationUser
;
import
${
pub
.
getPKGCodeName
()}.
util
.
security
.
AuthorizationLogin
;
import
org
.
springframework
.
cache
.
annotation
.
Cacheable
;
import
org
.
springframework
.
cloud
.
openfeign
.
FeignClient
;
import
org
.
springframework
.
web
.
bind
.
annotation
.*;
import
com
.
alibaba
.
fastjson
.
JSONObject
;
...
...
@@ -31,4 +32,8 @@ public interface IBZUAAFeignClient
@
PostMapping
(
value
=
"/uaa/loginbyusername"
)
AuthenticationUser
loginByUsername
(@
RequestBody
String
username
);
@
Cacheable
(
value
=
"ibzuaa_publickey"
)
@
GetMapping
(
value
=
"/uaa/publickey"
)
String
getPublicKey
();
}
SLN/%PUBPRJ%-util/src/main/java/%SYS_PKGPATH%/util/security/AuthTokenUtil.java.ftl
浏览文件 @
cb68ac3e
...
...
@@ -3,171 +3,15 @@ TARGET=PSSYSTEM
</#
ibiztemplate
>
package
${
pub
.
getPKGCodeName
()}.
util
.
security
;
import
io
.
jsonwebtoken
.
Claims
;
import
io
.
jsonwebtoken
.
Clock
;
import
io
.
jsonwebtoken
.
Jwts
;
import
io
.
jsonwebtoken
.
SignatureAlgorithm
;
import
io
.
jsonwebtoken
.
impl
.
DefaultClock
;
import
org
.
springframework
.
beans
.
factory
.
annotation
.
Value
;
import
org
.
springframework
.
security
.
core
.
context
.
SecurityContext
;
import
org
.
springframework
.
security
.
core
.
context
.
SecurityContextHolder
;
import
org
.
springframework
.
security
.
core
.
userdetails
.
UserDetails
;
import
org
.
springframework
.
stereotype
.
Component
;
import
java
.
io
.
Serializable
;
import
java
.
util
.
Date
;
import
java
.
util
.
HashMap
;
import
java
.
util
.
Map
;
import
java
.
util
.
Optional
;
import
java
.
util
.
function
.
Function
;
public
interface
AuthTokenUtil
{
@
Component
public
class
AuthTokenUtil
implements
Serializable
{
String
generateToken
(
UserDetails
userDetails
);
private
static
final
long
serialVersionUID
=
-
3301605591108950415L
;
private
Clock
clock
=
DefaultClock
.
INSTANCE
;
Boolean
validateToken
(
String
token
,
UserDetails
userDetails
);
@
Value
(${
r
'"${ibiz.jwt.secret:ibzsecret}"'
})
private
String
secret
;
String
getUsernameFromToken
(
String
token
);
@
Value
(${
r
'"${ibiz.jwt.expiration:7200000}"'
})
private
Long
expiration
;
@
Value
(${
r
'"${ibiz.jwt.header:Authorization}"'
})
private
String
tokenHeader
;
public
String
getUsernameFromToken
(
String
token
)
{
return
getClaimFromToken
(
token
,
Claims
::
getSubject
);
}
public
Date
getIssuedAtDateFromToken
(
String
token
)
{
return
getClaimFromToken
(
token
,
Claims
::
getIssuedAt
);
}
public
Date
getExpirationDateFromToken
(
String
token
)
{
return
getClaimFromToken
(
token
,
Claims
::
getExpiration
);
}
public
<
T
>
T
getClaimFromToken
(
String
token
,
Function
<
Claims
,
T
>
claimsResolver
)
{
final
Claims
claims
=
getAllClaimsFromToken
(
token
);
return
claimsResolver
.
apply
(
claims
);
}
private
Claims
getAllClaimsFromToken
(
String
token
)
{
return
Jwts
.
parser
()
.
setSigningKey
(
secret
)
.
parseClaimsJws
(
token
)
.
getBody
();
}
private
Boolean
isTokenExpired
(
String
token
)
{
final
Date
expiration
=
getExpirationDateFromToken
(
token
);
return
expiration
.
before
(
clock
.
now
());
}
private
Boolean
isCreatedBeforeLastPasswordReset
(
Date
created
,
Date
lastPasswordReset
)
{
return
(
lastPasswordReset
!= null && created.before(lastPasswordReset));
}
private
Boolean
ignoreTokenExpiration
(
String
token
)
{
//
here
you
specify
tokens
,
for
that
the
expiration
is
ignored
return
false
;
}
public
String
generateToken
(
UserDetails
userDetails
)
{
Map
<
String
,
Object
>
claims
=
new
HashMap
<>();
return
doGenerateToken
(
claims
,
userDetails
.
getUsername
());
}
private
String
doGenerateToken
(
Map
<
String
,
Object
>
claims
,
String
subject
)
{
final
Date
createdDate
=
clock
.
now
();
final
Date
expirationDate
=
calculateExpirationDate
(
createdDate
);
return
Jwts
.
builder
()
.
setClaims
(
claims
)
.
setSubject
(
subject
)
.
setIssuedAt
(
createdDate
)
.
setExpiration
(
expirationDate
)
.
signWith
(
SignatureAlgorithm
.
HS512
,
secret
)
.
compact
();
}
public
Boolean
canTokenBeRefreshed
(
String
token
,
Date
lastPasswordReset
)
{
final
Date
created
=
getIssuedAtDateFromToken
(
token
);
return
!isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&&
(
!isTokenExpired(token) || ignoreTokenExpiration(token));
}
public
String
refreshToken
(
String
token
)
{
final
Date
createdDate
=
clock
.
now
();
final
Date
expirationDate
=
calculateExpirationDate
(
createdDate
);
final
Claims
claims
=
getAllClaimsFromToken
(
token
);
claims
.
setIssuedAt
(
createdDate
);
claims
.
setExpiration
(
expirationDate
);
return
Jwts
.
builder
()
.
setClaims
(
claims
)
.
signWith
(
SignatureAlgorithm
.
HS512
,
secret
)
.
compact
();
}
public
Boolean
validateToken
(
String
token
,
UserDetails
userDetails
)
{
AuthenticationUser
user
=
(
AuthenticationUser
)
userDetails
;
final
Date
created
=
getIssuedAtDateFromToken
(
token
);
return
(
!isTokenExpired(token) );
}
private
Date
calculateExpirationDate
(
Date
createdDate
)
{
return
new
Date
(
createdDate
.
getTime
()
+
expiration
);
}
/**
*
Get
the
login
of
the
current
user
.
*
*
@
return
the
login
of
the
current
user
*/
public
static
Optional
<
String
>
getCurrentUserLogin
()
{
SecurityContext
securityContext
=
SecurityContextHolder
.
getContext
();
return
Optional
.
ofNullable
(
securityContext
.
getAuthentication
())
.
map
(
authentication
->
{
if
(
authentication
.
getPrincipal
()
instanceof
UserDetails
)
{
UserDetails
springSecurityUser
=
(
UserDetails
)
authentication
.
getPrincipal
();
return
springSecurityUser
.
getUsername
();
}
else
if
(
authentication
.
getPrincipal
()
instanceof
String
)
{
return
(
String
)
authentication
.
getPrincipal
();
}
return
null
;
});
}
/**
*
Check
if
a
user
is
authenticated
.
*
*
@
return
true
if
the
user
is
authenticated
,
false
otherwise
*/
public
static
boolean
isAuthenticated
()
{
SecurityContext
securityContext
=
SecurityContextHolder
.
getContext
();
return
Optional
.
ofNullable
(
securityContext
.
getAuthentication
())
.
map
(
authentication
->
authentication
.
getAuthorities
().
stream
()
.
noneMatch
(
grantedAuthority
->
grantedAuthority
.
getAuthority
().
equals
(
"ANONYMOUS"
)))
.
orElse
(
false
);
}
/**
*
If
the
current
user
has
a
specific
authority
(
security
role
).
*
<
p
>
*
The
name
of
this
method
comes
from
the
isUserInRole
()
method
in
the
Servlet
API
*
*
@
param
authority
the
authority
to
check
*
@
return
true
if
the
current
user
has
the
authority
,
false
otherwise
*/
public
static
boolean
isCurrentUserInRole
(
String
authority
)
{
SecurityContext
securityContext
=
SecurityContextHolder
.
getContext
();
return
Optional
.
ofNullable
(
securityContext
.
getAuthentication
())
.
map
(
authentication
->
authentication
.
getAuthorities
().
stream
()
.
anyMatch
(
grantedAuthority
->
grantedAuthority
.
getAuthority
().
equals
(
authority
)))
.
orElse
(
false
);
}
}
SLN/%PUBPRJ%-util/src/main/java/%SYS_PKGPATH%/util/security/SimpleTokenUtil.java.ftl
0 → 100644
浏览文件 @
cb68ac3e
<#
ibiztemplate
>
TARGET
=
PSSYSTEM
</#
ibiztemplate
>
package
${
pub
.
getPKGCodeName
()}.
util
.
security
;
import
io
.
jsonwebtoken
.
Claims
;
import
io
.
jsonwebtoken
.
Clock
;
import
io
.
jsonwebtoken
.
Jwts
;
import
io
.
jsonwebtoken
.
SignatureAlgorithm
;
import
io
.
jsonwebtoken
.
impl
.
DefaultClock
;
import
org
.
springframework
.
beans
.
factory
.
annotation
.
Value
;
import
org
.
springframework
.
security
.
core
.
context
.
SecurityContext
;
import
org
.
springframework
.
security
.
core
.
context
.
SecurityContextHolder
;
import
org
.
springframework
.
security
.
core
.
userdetails
.
UserDetails
;
import
org
.
springframework
.
stereotype
.
Component
;
import
java
.
io
.
Serializable
;
import
java
.
util
.
Date
;
import
java
.
util
.
HashMap
;
import
java
.
util
.
Map
;
import
java
.
util
.
Optional
;
import
java
.
util
.
function
.
Function
;
@
Component
@
ConditionalOnExpression
(
"(!${r'${ibiz.enablePermissionValid:false}'})&&'${r'${ibiz.auth.service:'}<#if sys.getPSSystemSetting()?? && sys.getPSSystemSetting().getDataAccCtrlArch()?? && sys.getPSSystemSetting().getDataAccCtrlArch()==1>UAATokenUtil<#else>SimpleTokenUtil</#if>${r'}'}'.equals('SimpleTokenUtil')"
)
public
class
SimpleTokenUtil
implements
AuthTokenUtil
,
Serializable
{
private
static
final
long
serialVersionUID
=
-
3301605591108950415L
;
private
Clock
clock
=
DefaultClock
.
INSTANCE
;
@
Value
(${
r
'"${ibiz.jwt.secret:ibzsecret}"'
})
private
String
secret
;
@
Value
(${
r
'"${ibiz.jwt.expiration:7200000}"'
})
private
Long
expiration
;
@
Value
(${
r
'"${ibiz.jwt.header:Authorization}"'
})
private
String
tokenHeader
;
public
String
getUsernameFromToken
(
String
token
)
{
return
getClaimFromToken
(
token
,
Claims
::
getSubject
);
}
public
Date
getIssuedAtDateFromToken
(
String
token
)
{
return
getClaimFromToken
(
token
,
Claims
::
getIssuedAt
);
}
public
Date
getExpirationDateFromToken
(
String
token
)
{
return
getClaimFromToken
(
token
,
Claims
::
getExpiration
);
}
public
<
T
>
T
getClaimFromToken
(
String
token
,
Function
<
Claims
,
T
>
claimsResolver
)
{
final
Claims
claims
=
getAllClaimsFromToken
(
token
);
return
claimsResolver
.
apply
(
claims
);
}
private
Claims
getAllClaimsFromToken
(
String
token
)
{
return
Jwts
.
parser
()
.
setSigningKey
(
secret
)
.
parseClaimsJws
(
token
)
.
getBody
();
}
private
Boolean
isTokenExpired
(
String
token
)
{
final
Date
expiration
=
getExpirationDateFromToken
(
token
);
return
expiration
.
before
(
clock
.
now
());
}
private
Boolean
isCreatedBeforeLastPasswordReset
(
Date
created
,
Date
lastPasswordReset
)
{
return
(
lastPasswordReset
!= null && created.before(lastPasswordReset));
}
private
Boolean
ignoreTokenExpiration
(
String
token
)
{
//
here
you
specify
tokens
,
for
that
the
expiration
is
ignored
return
false
;
}
public
String
generateToken
(
UserDetails
userDetails
)
{
Map
<
String
,
Object
>
claims
=
new
HashMap
<>();
return
doGenerateToken
(
claims
,
userDetails
.
getUsername
());
}
private
String
doGenerateToken
(
Map
<
String
,
Object
>
claims
,
String
subject
)
{
final
Date
createdDate
=
clock
.
now
();
final
Date
expirationDate
=
calculateExpirationDate
(
createdDate
);
return
Jwts
.
builder
()
.
setClaims
(
claims
)
.
setSubject
(
subject
)
.
setIssuedAt
(
createdDate
)
.
setExpiration
(
expirationDate
)
.
signWith
(
SignatureAlgorithm
.
HS512
,
secret
)
.
compact
();
}
public
Boolean
canTokenBeRefreshed
(
String
token
,
Date
lastPasswordReset
)
{
final
Date
created
=
getIssuedAtDateFromToken
(
token
);
return
!isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&&
(
!isTokenExpired(token) || ignoreTokenExpiration(token));
}
public
String
refreshToken
(
String
token
)
{
final
Date
createdDate
=
clock
.
now
();
final
Date
expirationDate
=
calculateExpirationDate
(
createdDate
);
final
Claims
claims
=
getAllClaimsFromToken
(
token
);
claims
.
setIssuedAt
(
createdDate
);
claims
.
setExpiration
(
expirationDate
);
return
Jwts
.
builder
()
.
setClaims
(
claims
)
.
signWith
(
SignatureAlgorithm
.
HS512
,
secret
)
.
compact
();
}
public
Boolean
validateToken
(
String
token
,
UserDetails
userDetails
)
{
AuthenticationUser
user
=
(
AuthenticationUser
)
userDetails
;
final
Date
created
=
getIssuedAtDateFromToken
(
token
);
return
(
!isTokenExpired(token) );
}
private
Date
calculateExpirationDate
(
Date
createdDate
)
{
return
new
Date
(
createdDate
.
getTime
()
+
expiration
);
}
/**
*
Get
the
login
of
the
current
user
.
*
*
@
return
the
login
of
the
current
user
*/
public
static
Optional
<
String
>
getCurrentUserLogin
()
{
SecurityContext
securityContext
=
SecurityContextHolder
.
getContext
();
return
Optional
.
ofNullable
(
securityContext
.
getAuthentication
())
.
map
(
authentication
->
{
if
(
authentication
.
getPrincipal
()
instanceof
UserDetails
)
{
UserDetails
springSecurityUser
=
(
UserDetails
)
authentication
.
getPrincipal
();
return
springSecurityUser
.
getUsername
();
}
else
if
(
authentication
.
getPrincipal
()
instanceof
String
)
{
return
(
String
)
authentication
.
getPrincipal
();
}
return
null
;
});
}
/**
*
Check
if
a
user
is
authenticated
.
*
*
@
return
true
if
the
user
is
authenticated
,
false
otherwise
*/
public
static
boolean
isAuthenticated
()
{
SecurityContext
securityContext
=
SecurityContextHolder
.
getContext
();
return
Optional
.
ofNullable
(
securityContext
.
getAuthentication
())
.
map
(
authentication
->
authentication
.
getAuthorities
().
stream
()
.
noneMatch
(
grantedAuthority
->
grantedAuthority
.
getAuthority
().
equals
(
"ANONYMOUS"
)))
.
orElse
(
false
);
}
/**
*
If
the
current
user
has
a
specific
authority
(
security
role
).
*
<
p
>
*
The
name
of
this
method
comes
from
the
isUserInRole
()
method
in
the
Servlet
API
*
*
@
param
authority
the
authority
to
check
*
@
return
true
if
the
current
user
has
the
authority
,
false
otherwise
*/
public
static
boolean
isCurrentUserInRole
(
String
authority
)
{
SecurityContext
securityContext
=
SecurityContextHolder
.
getContext
();
return
Optional
.
ofNullable
(
securityContext
.
getAuthentication
())
.
map
(
authentication
->
authentication
.
getAuthorities
().
stream
()
.
anyMatch
(
grantedAuthority
->
grantedAuthority
.
getAuthority
().
equals
(
authority
)))
.
orElse
(
false
);
}
}
SLN/%PUBPRJ%-util/src/main/java/%SYS_PKGPATH%/util/security/UAATokenUtil.java.ftl
0 → 100644
浏览文件 @
cb68ac3e
<#
ibiztemplate
>
TARGET
=
PSSYSTEM
</#
ibiztemplate
>
package
${
pub
.
getPKGCodeName
()}.
util
.
security
;
import
io
.
jsonwebtoken
.
Claims
;
import
io
.
jsonwebtoken
.
Clock
;
import
io
.
jsonwebtoken
.
Jwts
;
import
io
.
jsonwebtoken
.
SignatureAlgorithm
;
import
io
.
jsonwebtoken
.
impl
.
DefaultClock
;
import
lombok
.
SneakyThrows
;
import
${
pub
.
getPKGCodeName
()}.
util
.
client
.
IBZUAAFeignClient
;
import
org
.
apache
.
commons
.
codec
.
binary
.
Base64
;
import
org
.
apache
.
commons
.
io
.
IOUtils
;
import
org
.
springframework
.
beans
.
factory
.
annotation
.
Autowired
;
import
org
.
springframework
.
beans
.
factory
.
annotation
.
Value
;
import
org
.
springframework
.
boot
.
autoconfigure
.
condition
.
ConditionalOnExpression
;
import
org
.
springframework
.
security
.
core
.
userdetails
.
UserDetails
;
import
org
.
springframework
.
stereotype
.
Component
;
import
java
.
io
.
File
;
import
java
.
io
.
FileInputStream
;
import
java
.
io
.
Serializable
;
import
java
.
security
.
KeyFactory
;
import
java
.
security
.
NoSuchAlgorithmException
;
import
java
.
security
.
PrivateKey
;
import
java
.
security
.
PublicKey
;
import
java
.
security
.
spec
.
InvalidKeySpecException
;
import
java
.
security
.
spec
.
PKCS8EncodedKeySpec
;
import
java
.
security
.
spec
.
X509EncodedKeySpec
;
import
java
.
util
.
Date
;
import
java
.
util
.
HashMap
;
import
java
.
util
.
Map
;
import
java
.
util
.
function
.
Function
;
@
Component
@
ConditionalOnExpression
(
"${r'${ibiz.enablePermissionValid:false}'}||'${r'${ibiz.auth.service:'}<#if sys.getPSSystemSetting()?? && sys.getPSSystemSetting().getDataAccCtrlArch()?? && sys.getPSSystemSetting().getDataAccCtrlArch()==1>UAATokenUtil<#else>SimpleTokenUtil</#if>${r'}'}'.equals('UAATokenUtil')"
)
public
class
UAATokenUtil
implements
AuthTokenUtil
,
Serializable
{
private
static
final
long
serialVersionUID
=
-
3301605591108950415L
;
private
Clock
clock
=
DefaultClock
.
INSTANCE
;
@
Value
(${
r
'"${ibiz.jwt.secret:ibzsecret}"'
})
private
String
secret
;
@
Value
(${
r
'"${ibiz.jwt.expiration:7200000}"'
})
private
Long
expiration
;
@
Value
(${
r
'"${ibiz.jwt.header:Authorization}"'
})
private
String
tokenHeader
;
@
Autowired
private
IBZUAAFeignClient
uaaFeignClient
;
public
String
getUsernameFromToken
(
String
token
)
{
return
getClaimFromToken
(
token
,
Claims
::
getSubject
);
}
public
Date
getIssuedAtDateFromToken
(
String
token
)
{
return
getClaimFromToken
(
token
,
Claims
::
getIssuedAt
);
}
public
Date
getExpirationDateFromToken
(
String
token
)
{
return
getClaimFromToken
(
token
,
Claims
::
getExpiration
);
}
public
<
T
>
T
getClaimFromToken
(
String
token
,
Function
<
Claims
,
T
>
claimsResolver
)
{
final
Claims
claims
=
getAllClaimsFromToken
(
token
);
return
claimsResolver
.
apply
(
claims
);
}
public
Claims
getAllClaimsFromToken
(
String
token
)
{
PublicKey
publicKey
=
getPublicKey
(
getPublicKeyString
());
return
Jwts
.
parser
()
.
setSigningKey
(
publicKey
)
.
parseClaimsJws
(
token
)
.
getBody
();
}
private
Boolean
isTokenExpired
(
String
token
)
{
final
Date
expiration
=
getExpirationDateFromToken
(
token
);
return
expiration
.
before
(
clock
.
now
());
}
public
String
generateToken
(
UserDetails
userDetails
)
{
return
null
;
}
public
Boolean
validateToken
(
String
token
,
UserDetails
userDetails
)
{
AuthenticationUser
user
=
(
AuthenticationUser
)
userDetails
;
final
Date
created
=
getIssuedAtDateFromToken
(
token
);
return
(
!isTokenExpired(token) );
}
private
String
getPublicKeyString
(){
return
uaaFeignClient
.
getPublicKey
();
}
/**
*
获取
PublicKey
对象
*
@
param
publicKeyBase64
*
@
return
*
@
throws
NoSuchAlgorithmException
*
@
throws
InvalidKeySpecException
*/
@
SneakyThrows
public
PublicKey
getPublicKey
(
String
publicKeyBase64
)
{
byte
[]
byteKey
=
Base64
.
decodeBase64
(
publicKeyBase64
);
X509EncodedKeySpec
x509EncodedKeySpec
=
new
X509EncodedKeySpec
(
byteKey
);
KeyFactory
keyFactory
=
KeyFactory
.
getInstance
(
"RSA"
);
return
keyFactory
.
generatePublic
(
x509EncodedKeySpec
);
}
}
编辑
预览
Markdown
格式
0%
请重试
or
添加新附件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
先完成此消息的编辑!
取消
想要评论请
注册
或
登录