Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
cas-relay
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
jiatao
cas-relay
Commits
13e594d6
Commit
13e594d6
authored
Jun 18, 2026
by
jiatao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
完善sso.json文件
parent
bf7142c4
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
1355 additions
and
2 deletions
+1355
-2
.env
.env
+47
-0
.env.example
.env.example
+34
-0
Dockerfile
Dockerfile
+1
-1
docker-compose.yml
docker-compose.yml
+23
-0
CasRelayApplication.java
...in/java/com/jd/easywalk/casrelay/CasRelayApplication.java
+2
-1
DotenvPropertySourceInitializer.java
...jd/easywalk/casrelay/DotenvPropertySourceInitializer.java
+33
-0
TeacherDirectoryService.java
...ava/com/jd/easywalk/casrelay/TeacherDirectoryService.java
+14
-0
WeComRelayController.java
...n/java/com/jd/easywalk/casrelay/WeComRelayController.java
+67
-0
WeComRelayProperties.java
...n/java/com/jd/easywalk/casrelay/WeComRelayProperties.java
+368
-0
WeComRelayService.java
...main/java/com/jd/easywalk/casrelay/WeComRelayService.java
+637
-0
WeComRelayStartupLogger.java
...ava/com/jd/easywalk/casrelay/WeComRelayStartupLogger.java
+29
-0
spring.factories
src/main/resources/META-INF/spring.factories
+2
-0
application.yml
src/main/resources/application.yml
+42
-0
sso.wecom.example.json
sso.wecom.example.json
+28
-0
sso.wecom.mock.example.json
sso.wecom.mock.example.json
+28
-0
No files found.
.env
0 → 100644
View file @
13e594d6
CAS_RELAY_PORT=18080
CAS_SERVICE_URL=https://nercwbs-lms.xidian.edu.cn/orgsso
CAS_SERVER_URL_PREFIX=https://ids.xidian.edu.cn/authserver
CAS_SERVICE_VALIDATE_PATH=/serviceValidate
CAS_LOGIN_PATH=/login
CAS_LOGOUT_PATH=/logout
MINGDAO_ENABLED=true
MINGDAO_REFRESH_ENABLED=true
MINGDAO_REFRESH_INTERVAL_MS=600000
MINGDAO_API_URL=https://nercwbs-lms.xidian.edu.cn/api/v2/open/worksheet/getFilterRows
MINGDAO_APP_KEY=3ed6154939e6aa92
MINGDAO_SIGN= MDM0ZWU1YWM4NzM0ODljZTZiNDJlYmUwYzRmODU4OTNlN2ZkYzIwMjI1ZjBhNzIwZjA4OWU3YzQ1NTdiNGM0Mg==
OAUTH_CLIENT_ID=1506958348348624896
OAUTH_CLIENT_SECRET=19E483DC4D272E4JD5KD
OAUTH_REDIRECT_URI=https://nercwbs-lms.xidian.edu.cn/orgsso/oauth2
OAUTH_AUTHORIZATION_URI=https://ids.xidian.edu.cn/authserver/oauth2.0/authorize
OAUTH_TOKEN_URI=https://ids.xidian.edu.cn/authserver/oauth2.0/accessToken
OAUTH_USER_INFO_URI=https://ids.xidian.edu.cn/authserver/oauthApi/user/profile
OAUTH_USER_MOBILE_URI=https://ids.xidian.edu.cn/authserver/oauthApi/user/getMobile
WECOM_RELAY_ENABLED=true
WECOM_APP_ID=200260525100013951
WECOM_APP_SECRET=8ekpsdgmb9eh3m9w1gf3s1nm5ft2b3ne
WECOM_PLATFORM_DOMAIN=xxcapp.xidian.edu.cn
WECOM_CORP_ID=
WECOM_AGENT_ID=
WECOM_CORP_SECRET=
WECOM_REDIRECT_URI=https://nercwbs-lms.xidian.edu.cn/orgsso/oauth2
WECOM_AUTHORIZATION_URI=https://xxcapp.xidian.edu.cn/uc/api/oauth/index
WECOM_AUTHORIZATION_APP_ID_PARAM=appid
WECOM_AUTHORIZATION_REDIRECT_URI_PARAM=redirect
WECOM_AUTHORIZATION_RESPONSE_TYPE_PARAM=
WECOM_AUTHORIZATION_SCOPE_PARAM=
WECOM_AUTHORIZATION_STATE_PARAM=state
WECOM_AUTHORIZATION_QRCODE_PARAM=qrcode
WECOM_AUTHORIZATION_QRCODE_VALUE=1
WECOM_ACCESS_TOKEN_URI=https://qyapi.weixin.qq.com/cgi-bin/gettoken
WECOM_CODE_USER_INFO_URI=https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo
WECOM_USER_DETAIL_URI=https://qyapi.weixin.qq.com/cgi-bin/user/get
WECOM_SCOPE=snsapi_base
WECOM_MOCK_ENABLED=false
WECOM_MOCK_WORK_NO=20240001
WECOM_MOCK_NAME=Mock User
WECOM_MOCK_EMAIL=20240001@xidian.edu.cn
WECOM_MOCK_MOBILE=13800000000
.env.example
View file @
13e594d6
...
...
@@ -19,3 +19,37 @@ OAUTH_AUTHORIZATION_URI=https://ids.xidian.edu.cn/authserver/oauth2.0/authorize
OAUTH_TOKEN_URI=https://ids.xidian.edu.cn/authserver/oauth2.0/accessToken
OAUTH_USER_INFO_URI=https://ids.xidian.edu.cn/authserver/oauthApi/user/profile
OAUTH_USER_MOBILE_URI=https://ids.xidian.edu.cn/authserver/oauthApi/user/getMobile
WECOM_RELAY_ENABLED=false
WECOM_APP_ID=200260525100013951
WECOM_APP_SECRET=
WECOM_PLATFORM_DOMAIN=xxcapp.xidian.edu.cn
WECOM_CORP_ID=
WECOM_AGENT_ID=
WECOM_CORP_SECRET=
WECOM_REDIRECT_URI=https://nercwbs-lms.xidian.edu.cn/orgsso/oauth2
WECOM_ALLOWED_REDIRECT_HOST=nercwbs-lms.xidian.edu.cn
WECOM_AUTHORIZATION_URI=https://xxcapp.xidian.edu.cn/uc/api/oauth/index
WECOM_AUTHORIZATION_APP_ID_PARAM=appid
WECOM_AUTHORIZATION_REDIRECT_URI_PARAM=redirect
WECOM_AUTHORIZATION_RESPONSE_TYPE_PARAM=
WECOM_AUTHORIZATION_SCOPE_PARAM=
WECOM_AUTHORIZATION_STATE_PARAM=state
WECOM_AUTHORIZATION_QRCODE_PARAM=qrcode
WECOM_AUTHORIZATION_QRCODE_VALUE=1
WECOM_CODE_EXCHANGE_URI=https://xxcapp.xidian.edu.cn/api/third/get-token
WECOM_TOKEN_USER_INFO_URI=https://xxcapp.xidian.edu.cn/uc/api/oauth/user-by-code
WECOM_CODE_PARAM=code
WECOM_CODE_EXCHANGE_APP_ID_PARAM=appid
WECOM_CODE_EXCHANGE_APP_SECRET_PARAM=appsecret
WECOM_TOKEN_USER_INFO_TOKEN_PARAM=access_token
WECOM_PLATFORM_ACCESS_TOKEN_SKEW_SECONDS=300
WECOM_ACCESS_TOKEN_URI=
WECOM_CODE_USER_INFO_URI=
WECOM_USER_DETAIL_URI=
WECOM_SCOPE=snsapi_base
WECOM_MOCK_ENABLED=false
WECOM_MOCK_WORK_NO=20240001
WECOM_MOCK_NAME=MockUser
WECOM_MOCK_EMAIL=20240001@xidian.edu.cn
WECOM_MOCK_MOBILE=13800000000
Dockerfile
View file @
13e594d6
FROM
openjdk:8
FROM
eclipse-temurin:8-jre
WORKDIR
/app
...
...
docker-compose.yml
View file @
13e594d6
...
...
@@ -14,6 +14,17 @@ services:
MINGDAO_REFRESH_INTERVAL_MS
:
"
${MINGDAO_REFRESH_INTERVAL_MS:-600000}"
MINGDAO_APP_KEY
:
"
${MINGDAO_APP_KEY}"
MINGDAO_SIGN
:
"
${MINGDAO_SIGN}"
WECOM_RELAY_ENABLED
:
"
${WECOM_RELAY_ENABLED:-false}"
WECOM_APP_ID
:
"
${WECOM_APP_ID:-200260525100013951}"
WECOM_APP_SECRET
:
"
${WECOM_APP_SECRET:-}"
WECOM_PLATFORM_DOMAIN
:
"
${WECOM_PLATFORM_DOMAIN:-xxcapp.xidian.edu.cn}"
WECOM_REDIRECT_URI
:
"
${WECOM_REDIRECT_URI:-https://nercwbs-lms.xidian.edu.cn/orgsso/oauth2}"
WECOM_ALLOWED_REDIRECT_HOST
:
"
${WECOM_ALLOWED_REDIRECT_HOST:-nercwbs-lms.xidian.edu.cn}"
WECOM_AUTHORIZATION_URI
:
"
${WECOM_AUTHORIZATION_URI:-https://xxcapp.xidian.edu.cn/uc/api/oauth/index}"
WECOM_AUTHORIZATION_APP_ID_PARAM
:
"
${WECOM_AUTHORIZATION_APP_ID_PARAM:-appid}"
WECOM_AUTHORIZATION_REDIRECT_URI_PARAM
:
"
${WECOM_AUTHORIZATION_REDIRECT_URI_PARAM:-redirect}"
WECOM_AUTHORIZATION_QRCODE_PARAM
:
"
${WECOM_AUTHORIZATION_QRCODE_PARAM:-qrcode}"
WECOM_AUTHORIZATION_QRCODE_VALUE
:
"
${WECOM_AUTHORIZATION_QRCODE_VALUE:-1}"
JAVA_OPTS
:
>-
-Dcas.service=${CAS_SERVICE_URL}
-Dcas.server-url-prefix=${CAS_SERVER_URL_PREFIX:-https://ids.xidian.edu.cn/authserver}
...
...
@@ -28,3 +39,15 @@ services:
-Doauth-relay.token-uri=${OAUTH_TOKEN_URI:-https://ids.xidian.edu.cn/authserver/oauth2.0/accessToken}
-Doauth-relay.user-info-uri=${OAUTH_USER_INFO_URI:-https://ids.xidian.edu.cn/authserver/oauthApi/user/profile}
-Doauth-relay.user-mobile-uri=${OAUTH_USER_MOBILE_URI:-https://ids.xidian.edu.cn/authserver/oauthApi/user/getMobile}
-Dwecom-relay.code-exchange-uri=${WECOM_CODE_EXCHANGE_URI:-https://xxcapp.xidian.edu.cn/api/third/get-token}
-Dwecom-relay.token-user-info-uri=${WECOM_TOKEN_USER_INFO_URI:-https://xxcapp.xidian.edu.cn/uc/api/oauth/user-by-code}
-Dwecom-relay.code-param=${WECOM_CODE_PARAM:-code}
-Dwecom-relay.code-exchange-app-id-param=${WECOM_CODE_EXCHANGE_APP_ID_PARAM:-appid}
-Dwecom-relay.code-exchange-app-secret-param=${WECOM_CODE_EXCHANGE_APP_SECRET_PARAM:-appsecret}
-Dwecom-relay.token-user-info-token-param=${WECOM_TOKEN_USER_INFO_TOKEN_PARAM:-access_token}
-Dwecom-relay.platform-access-token-skew-seconds=${WECOM_PLATFORM_ACCESS_TOKEN_SKEW_SECONDS:-300}
-Dwecom-relay.allowed-redirect-host=${WECOM_ALLOWED_REDIRECT_HOST:-nercwbs-lms.xidian.edu.cn}
-Dwecom-relay.access-token-uri=${WECOM_ACCESS_TOKEN_URI:-}
-Dwecom-relay.code-user-info-uri=${WECOM_CODE_USER_INFO_URI:-}
-Dwecom-relay.user-detail-uri=${WECOM_USER_DETAIL_URI:-}
-Dwecom-relay.scope=${WECOM_SCOPE:-snsapi_base}
src/main/java/com/jd/easywalk/casrelay/CasRelayApplication.java
View file @
13e594d6
...
...
@@ -7,7 +7,8 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties
({
CasRelayProperties
.
class
,
MingdaoProperties
.
class
,
OAuthRelayProperties
.
class
})
@EnableConfigurationProperties
({
CasRelayProperties
.
class
,
MingdaoProperties
.
class
,
OAuthRelayProperties
.
class
,
WeComRelayProperties
.
class
})
public
class
CasRelayApplication
{
public
static
void
main
(
String
[]
args
)
{
...
...
src/main/java/com/jd/easywalk/casrelay/DotenvPropertySourceInitializer.java
0 → 100644
View file @
13e594d6
package
com
.
jd
.
easywalk
.
casrelay
;
import
java.io.FileInputStream
;
import
java.io.InputStream
;
import
java.util.Properties
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.springframework.context.ApplicationContextInitializer
;
import
org.springframework.context.ConfigurableApplicationContext
;
import
org.springframework.core.env.ConfigurableEnvironment
;
import
org.springframework.core.env.PropertiesPropertySource
;
public
class
DotenvPropertySourceInitializer
implements
ApplicationContextInitializer
<
ConfigurableApplicationContext
>
{
private
static
final
Logger
log
=
LoggerFactory
.
getLogger
(
DotenvPropertySourceInitializer
.
class
);
@Override
public
void
initialize
(
ConfigurableApplicationContext
applicationContext
)
{
Properties
properties
=
new
Properties
();
try
(
InputStream
inputStream
=
new
FileInputStream
(
".env"
))
{
properties
.
load
(
inputStream
);
}
catch
(
Exception
ex
)
{
log
.
info
(
"Local .env file not loaded: {}"
,
ex
.
getMessage
());
return
;
}
ConfigurableEnvironment
environment
=
applicationContext
.
getEnvironment
();
environment
.
getPropertySources
().
addLast
(
new
PropertiesPropertySource
(
"localDotenv"
,
properties
));
log
.
info
(
"Local .env file loaded"
);
}
}
src/main/java/com/jd/easywalk/casrelay/TeacherDirectoryService.java
View file @
13e594d6
...
...
@@ -26,6 +26,7 @@ public class TeacherDirectoryService {
private
final
MingdaoProperties
properties
;
private
final
RestTemplate
restTemplate
;
private
final
Map
<
String
,
TeacherInfo
>
teachersByWorkNo
=
new
ConcurrentHashMap
<
String
,
TeacherInfo
>();
private
final
Map
<
String
,
TeacherInfo
>
teachersByMobile
=
new
ConcurrentHashMap
<
String
,
TeacherInfo
>();
public
TeacherDirectoryService
(
MingdaoProperties
properties
,
RestTemplateBuilder
restTemplateBuilder
)
{
this
.
properties
=
properties
;
...
...
@@ -45,6 +46,7 @@ public class TeacherDirectoryService {
validateConfig
();
Map
<
String
,
TeacherInfo
>
loaded
=
new
HashMap
<
String
,
TeacherInfo
>();
Map
<
String
,
TeacherInfo
>
loadedByMobile
=
new
HashMap
<
String
,
TeacherInfo
>();
int
pageIndex
=
1
;
int
pageSize
=
Math
.
max
(
properties
.
getPageSize
(),
1
);
while
(
true
)
{
...
...
@@ -58,6 +60,9 @@ public class TeacherDirectoryService {
TeacherInfo
teacher
=
toTeacherInfo
(
row
);
if
(
teacher
!=
null
)
{
loaded
.
put
(
teacher
.
getWorkNo
(),
teacher
);
if
(
StringUtils
.
hasText
(
teacher
.
getMobile
()))
{
loadedByMobile
.
put
(
teacher
.
getMobile
().
trim
(),
teacher
);
}
}
}
...
...
@@ -69,6 +74,8 @@ public class TeacherDirectoryService {
teachersByWorkNo
.
clear
();
teachersByWorkNo
.
putAll
(
loaded
);
teachersByMobile
.
clear
();
teachersByMobile
.
putAll
(
loadedByMobile
);
log
.
info
(
"Loaded {} teachers from Mingdao worksheet"
,
teachersByWorkNo
.
size
());
logTeacherDetails
();
}
...
...
@@ -80,6 +87,13 @@ public class TeacherDirectoryService {
return
teachersByWorkNo
.
get
(
workNo
.
trim
());
}
public
TeacherInfo
findByMobile
(
String
mobile
)
{
if
(!
StringUtils
.
hasText
(
mobile
))
{
return
null
;
}
return
teachersByMobile
.
get
(
mobile
.
trim
());
}
public
int
size
()
{
return
teachersByWorkNo
.
size
();
}
...
...
src/main/java/com/jd/easywalk/casrelay/WeComRelayController.java
0 → 100644
View file @
13e594d6
package
com
.
jd
.
easywalk
.
casrelay
;
import
java.util.Map
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.util.StringUtils
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.PostMapping
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestParam
;
import
org.springframework.web.bind.annotation.RestController
;
import
org.springframework.web.servlet.view.RedirectView
;
@RestController
@RequestMapping
(
"/wecom/oauth2"
)
public
class
WeComRelayController
{
private
static
final
Logger
log
=
LoggerFactory
.
getLogger
(
WeComRelayController
.
class
);
private
final
WeComRelayService
weComRelayService
;
public
WeComRelayController
(
WeComRelayService
weComRelayService
)
{
this
.
weComRelayService
=
weComRelayService
;
}
@GetMapping
({
"/authorize"
,
"/login"
})
public
RedirectView
authorize
(
@RequestParam
Map
<
String
,
String
>
params
)
{
String
target
=
weComRelayService
.
buildAuthorizationRedirect
(
params
);
log
.
info
(
"WeCom OAuth relay authorize request: redirectUri={}, state={}, target={}"
,
params
.
get
(
"redirect_uri"
),
params
.
get
(
"state"
),
target
);
RedirectView
redirectView
=
new
RedirectView
(
target
);
redirectView
.
setExpandUriTemplateVariables
(
false
);
return
redirectView
;
}
@GetMapping
(
"/mock/authorize"
)
public
RedirectView
mockAuthorize
(
@RequestParam
Map
<
String
,
String
>
params
)
{
String
target
=
weComRelayService
.
buildMockAuthorizationRedirect
(
params
);
log
.
info
(
"WeCom OAuth relay mock authorize request: redirectUri={}, state={}, target={}"
,
params
.
get
(
"redirect_uri"
),
params
.
get
(
"state"
),
target
);
RedirectView
redirectView
=
new
RedirectView
(
target
);
redirectView
.
setExpandUriTemplateVariables
(
false
);
return
redirectView
;
}
@PostMapping
({
"/access_token"
,
"/token"
})
public
ResponseEntity
<
Map
<
String
,
Object
>>
accessToken
(
@RequestParam
Map
<
String
,
String
>
params
)
{
log
.
info
(
"WeCom OAuth relay token request received: grantType={}, hasCode={}, redirectUri={}"
,
params
.
get
(
"grant_type"
),
StringUtils
.
hasText
(
params
.
get
(
"code"
)),
params
.
get
(
"redirect_uri"
));
Map
<
String
,
Object
>
body
=
weComRelayService
.
exchangeToken
(
params
);
log
.
info
(
"WeCom OAuth relay token response generated: keys={}, hasAccessToken={}, error={}"
,
body
.
keySet
(),
body
.
containsKey
(
"access_token"
),
body
.
get
(
"error_description"
));
return
ResponseEntity
.
ok
(
body
);
}
@GetMapping
({
"/userinfo"
,
"/user_info"
})
public
ResponseEntity
<
Map
<
String
,
Object
>>
userInfo
(
@RequestParam
(
required
=
false
)
String
token
,
@RequestParam
(
required
=
false
)
String
access_token
)
{
String
realToken
=
StringUtils
.
hasText
(
token
)
?
token
:
access_token
;
log
.
info
(
"WeCom OAuth relay Mingdao userInfo request received: hasToken={}, hasAccessToken={}, token={}"
,
StringUtils
.
hasText
(
token
),
StringUtils
.
hasText
(
access_token
),
weComRelayService
.
maskForLog
(
realToken
));
return
ResponseEntity
.
ok
(
weComRelayService
.
userInfo
(
realToken
));
}
}
src/main/java/com/jd/easywalk/casrelay/WeComRelayProperties.java
0 → 100644
View file @
13e594d6
package
com
.
jd
.
easywalk
.
casrelay
;
import
org.springframework.boot.context.properties.ConfigurationProperties
;
@ConfigurationProperties
(
prefix
=
"wecom-relay"
)
public
class
WeComRelayProperties
{
private
boolean
enabled
=
false
;
private
String
appId
=
""
;
private
String
appSecret
=
""
;
private
String
platformDomain
=
""
;
private
String
corpId
=
""
;
private
String
agentId
=
""
;
private
String
corpSecret
=
""
;
private
String
authorizationUri
=
"https://xxcapp.xidian.edu.cn/uc/api/oauth/index"
;
private
String
codeExchangeUri
=
"https://xxcapp.xidian.edu.cn/api/third/get-token"
;
private
String
tokenUserInfoUri
=
"https://xxcapp.xidian.edu.cn/uc/api/oauth/user-by-code"
;
private
String
accessTokenUri
=
""
;
private
String
codeUserInfoUri
=
""
;
private
String
userDetailUri
=
""
;
private
String
codeParam
=
"code"
;
private
String
codeExchangeAppIdParam
=
"appid"
;
private
String
codeExchangeAppSecretParam
=
"appsecret"
;
private
String
tokenUserInfoTokenParam
=
"access_token"
;
private
int
platformAccessTokenSkewSeconds
=
300
;
private
String
authorizationAppIdParam
=
"appid"
;
private
String
authorizationRedirectUriParam
=
"redirect"
;
private
String
authorizationResponseTypeParam
=
""
;
private
String
authorizationScopeParam
=
""
;
private
String
authorizationStateParam
=
"state"
;
private
String
authorizationQrcodeParam
=
"qrcode"
;
private
String
authorizationQrcodeValue
=
"1"
;
private
String
redirectUri
=
""
;
private
String
allowedRedirectHost
=
"nercwbs-lms.xidian.edu.cn"
;
private
String
scope
=
"snsapi_base"
;
private
int
tokenExpiresIn
=
7200
;
private
int
corpAccessTokenSkewSeconds
=
300
;
private
String
emailDomain
=
"xidian.edu.cn"
;
private
boolean
synthesizeEmail
=
true
;
private
boolean
appendWechatRedirect
=
true
;
private
int
connectTimeoutMs
=
5000
;
private
int
readTimeoutMs
=
10000
;
private
boolean
mockEnabled
=
false
;
private
String
mockWorkNo
=
"20240001"
;
private
String
mockName
=
"MockUser"
;
private
String
mockEmail
=
"20240001@xidian.edu.cn"
;
private
String
mockMobile
=
"13800000000"
;
public
boolean
isEnabled
()
{
return
enabled
;
}
public
void
setEnabled
(
boolean
enabled
)
{
this
.
enabled
=
enabled
;
}
public
String
getAppId
()
{
return
appId
;
}
public
void
setAppId
(
String
appId
)
{
this
.
appId
=
appId
;
}
public
String
getAppSecret
()
{
return
appSecret
;
}
public
void
setAppSecret
(
String
appSecret
)
{
this
.
appSecret
=
appSecret
;
}
public
String
getPlatformDomain
()
{
return
platformDomain
;
}
public
void
setPlatformDomain
(
String
platformDomain
)
{
this
.
platformDomain
=
platformDomain
;
}
public
String
getCorpId
()
{
return
corpId
;
}
public
void
setCorpId
(
String
corpId
)
{
this
.
corpId
=
corpId
;
}
public
String
getAgentId
()
{
return
agentId
;
}
public
void
setAgentId
(
String
agentId
)
{
this
.
agentId
=
agentId
;
}
public
String
getCorpSecret
()
{
return
corpSecret
;
}
public
void
setCorpSecret
(
String
corpSecret
)
{
this
.
corpSecret
=
corpSecret
;
}
public
String
getAuthorizationUri
()
{
return
authorizationUri
;
}
public
void
setAuthorizationUri
(
String
authorizationUri
)
{
this
.
authorizationUri
=
authorizationUri
;
}
public
String
getAuthorizationAppIdParam
()
{
return
authorizationAppIdParam
;
}
public
void
setAuthorizationAppIdParam
(
String
authorizationAppIdParam
)
{
this
.
authorizationAppIdParam
=
authorizationAppIdParam
;
}
public
String
getAuthorizationRedirectUriParam
()
{
return
authorizationRedirectUriParam
;
}
public
void
setAuthorizationRedirectUriParam
(
String
authorizationRedirectUriParam
)
{
this
.
authorizationRedirectUriParam
=
authorizationRedirectUriParam
;
}
public
String
getAuthorizationResponseTypeParam
()
{
return
authorizationResponseTypeParam
;
}
public
void
setAuthorizationResponseTypeParam
(
String
authorizationResponseTypeParam
)
{
this
.
authorizationResponseTypeParam
=
authorizationResponseTypeParam
;
}
public
String
getAuthorizationScopeParam
()
{
return
authorizationScopeParam
;
}
public
void
setAuthorizationScopeParam
(
String
authorizationScopeParam
)
{
this
.
authorizationScopeParam
=
authorizationScopeParam
;
}
public
String
getAuthorizationStateParam
()
{
return
authorizationStateParam
;
}
public
void
setAuthorizationStateParam
(
String
authorizationStateParam
)
{
this
.
authorizationStateParam
=
authorizationStateParam
;
}
public
String
getAuthorizationQrcodeParam
()
{
return
authorizationQrcodeParam
;
}
public
void
setAuthorizationQrcodeParam
(
String
authorizationQrcodeParam
)
{
this
.
authorizationQrcodeParam
=
authorizationQrcodeParam
;
}
public
String
getAuthorizationQrcodeValue
()
{
return
authorizationQrcodeValue
;
}
public
void
setAuthorizationQrcodeValue
(
String
authorizationQrcodeValue
)
{
this
.
authorizationQrcodeValue
=
authorizationQrcodeValue
;
}
public
String
getAccessTokenUri
()
{
return
accessTokenUri
;
}
public
void
setAccessTokenUri
(
String
accessTokenUri
)
{
this
.
accessTokenUri
=
accessTokenUri
;
}
public
String
getCodeExchangeUri
()
{
return
codeExchangeUri
;
}
public
void
setCodeExchangeUri
(
String
codeExchangeUri
)
{
this
.
codeExchangeUri
=
codeExchangeUri
;
}
public
String
getTokenUserInfoUri
()
{
return
tokenUserInfoUri
;
}
public
void
setTokenUserInfoUri
(
String
tokenUserInfoUri
)
{
this
.
tokenUserInfoUri
=
tokenUserInfoUri
;
}
public
String
getCodeParam
()
{
return
codeParam
;
}
public
void
setCodeParam
(
String
codeParam
)
{
this
.
codeParam
=
codeParam
;
}
public
String
getCodeExchangeAppIdParam
()
{
return
codeExchangeAppIdParam
;
}
public
void
setCodeExchangeAppIdParam
(
String
codeExchangeAppIdParam
)
{
this
.
codeExchangeAppIdParam
=
codeExchangeAppIdParam
;
}
public
String
getCodeExchangeAppSecretParam
()
{
return
codeExchangeAppSecretParam
;
}
public
void
setCodeExchangeAppSecretParam
(
String
codeExchangeAppSecretParam
)
{
this
.
codeExchangeAppSecretParam
=
codeExchangeAppSecretParam
;
}
public
String
getTokenUserInfoTokenParam
()
{
return
tokenUserInfoTokenParam
;
}
public
void
setTokenUserInfoTokenParam
(
String
tokenUserInfoTokenParam
)
{
this
.
tokenUserInfoTokenParam
=
tokenUserInfoTokenParam
;
}
public
int
getPlatformAccessTokenSkewSeconds
()
{
return
platformAccessTokenSkewSeconds
;
}
public
void
setPlatformAccessTokenSkewSeconds
(
int
platformAccessTokenSkewSeconds
)
{
this
.
platformAccessTokenSkewSeconds
=
platformAccessTokenSkewSeconds
;
}
public
String
getCodeUserInfoUri
()
{
return
codeUserInfoUri
;
}
public
void
setCodeUserInfoUri
(
String
codeUserInfoUri
)
{
this
.
codeUserInfoUri
=
codeUserInfoUri
;
}
public
String
getUserDetailUri
()
{
return
userDetailUri
;
}
public
void
setUserDetailUri
(
String
userDetailUri
)
{
this
.
userDetailUri
=
userDetailUri
;
}
public
String
getRedirectUri
()
{
return
redirectUri
;
}
public
void
setRedirectUri
(
String
redirectUri
)
{
this
.
redirectUri
=
redirectUri
;
}
public
String
getAllowedRedirectHost
()
{
return
allowedRedirectHost
;
}
public
void
setAllowedRedirectHost
(
String
allowedRedirectHost
)
{
this
.
allowedRedirectHost
=
allowedRedirectHost
;
}
public
String
getScope
()
{
return
scope
;
}
public
void
setScope
(
String
scope
)
{
this
.
scope
=
scope
;
}
public
int
getTokenExpiresIn
()
{
return
tokenExpiresIn
;
}
public
void
setTokenExpiresIn
(
int
tokenExpiresIn
)
{
this
.
tokenExpiresIn
=
tokenExpiresIn
;
}
public
int
getCorpAccessTokenSkewSeconds
()
{
return
corpAccessTokenSkewSeconds
;
}
public
void
setCorpAccessTokenSkewSeconds
(
int
corpAccessTokenSkewSeconds
)
{
this
.
corpAccessTokenSkewSeconds
=
corpAccessTokenSkewSeconds
;
}
public
String
getEmailDomain
()
{
return
emailDomain
;
}
public
void
setEmailDomain
(
String
emailDomain
)
{
this
.
emailDomain
=
emailDomain
;
}
public
boolean
isSynthesizeEmail
()
{
return
synthesizeEmail
;
}
public
void
setSynthesizeEmail
(
boolean
synthesizeEmail
)
{
this
.
synthesizeEmail
=
synthesizeEmail
;
}
public
boolean
isAppendWechatRedirect
()
{
return
appendWechatRedirect
;
}
public
void
setAppendWechatRedirect
(
boolean
appendWechatRedirect
)
{
this
.
appendWechatRedirect
=
appendWechatRedirect
;
}
public
int
getConnectTimeoutMs
()
{
return
connectTimeoutMs
;
}
public
void
setConnectTimeoutMs
(
int
connectTimeoutMs
)
{
this
.
connectTimeoutMs
=
connectTimeoutMs
;
}
public
int
getReadTimeoutMs
()
{
return
readTimeoutMs
;
}
public
void
setReadTimeoutMs
(
int
readTimeoutMs
)
{
this
.
readTimeoutMs
=
readTimeoutMs
;
}
public
boolean
isMockEnabled
()
{
return
mockEnabled
;
}
public
void
setMockEnabled
(
boolean
mockEnabled
)
{
this
.
mockEnabled
=
mockEnabled
;
}
public
String
getMockWorkNo
()
{
return
mockWorkNo
;
}
public
void
setMockWorkNo
(
String
mockWorkNo
)
{
this
.
mockWorkNo
=
mockWorkNo
;
}
public
String
getMockName
()
{
return
mockName
;
}
public
void
setMockName
(
String
mockName
)
{
this
.
mockName
=
mockName
;
}
public
String
getMockEmail
()
{
return
mockEmail
;
}
public
void
setMockEmail
(
String
mockEmail
)
{
this
.
mockEmail
=
mockEmail
;
}
public
String
getMockMobile
()
{
return
mockMobile
;
}
public
void
setMockMobile
(
String
mockMobile
)
{
this
.
mockMobile
=
mockMobile
;
}
}
src/main/java/com/jd/easywalk/casrelay/WeComRelayService.java
0 → 100644
View file @
13e594d6
package
com
.
jd
.
easywalk
.
casrelay
;
import
java.net.URI
;
import
java.net.URISyntaxException
;
import
java.time.Instant
;
import
java.util.LinkedHashMap
;
import
java.util.Map
;
import
java.util.UUID
;
import
java.util.concurrent.ConcurrentHashMap
;
import
com.fasterxml.jackson.core.type.TypeReference
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.springframework.boot.web.client.RestTemplateBuilder
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.stereotype.Service
;
import
org.springframework.util.StringUtils
;
import
org.springframework.web.client.RestClientException
;
import
org.springframework.web.client.RestTemplate
;
import
org.springframework.web.util.UriComponentsBuilder
;
@Service
public
class
WeComRelayService
{
private
static
final
Logger
log
=
LoggerFactory
.
getLogger
(
WeComRelayService
.
class
);
private
final
WeComRelayProperties
properties
;
private
final
TeacherDirectoryService
teacherDirectoryService
;
private
final
RestTemplate
restTemplate
;
private
final
ObjectMapper
objectMapper
;
private
final
Map
<
String
,
LocalToken
>
localTokens
=
new
ConcurrentHashMap
<
String
,
LocalToken
>();
private
volatile
CorpAccessToken
corpAccessToken
;
private
volatile
PlatformAccessToken
platformAccessToken
;
public
WeComRelayService
(
WeComRelayProperties
properties
,
TeacherDirectoryService
teacherDirectoryService
,
RestTemplateBuilder
restTemplateBuilder
,
ObjectMapper
objectMapper
)
{
this
.
properties
=
properties
;
this
.
teacherDirectoryService
=
teacherDirectoryService
;
this
.
restTemplate
=
restTemplateBuilder
.
setConnectTimeout
(
java
.
time
.
Duration
.
ofMillis
(
properties
.
getConnectTimeoutMs
()))
.
setReadTimeout
(
java
.
time
.
Duration
.
ofMillis
(
properties
.
getReadTimeoutMs
()))
.
build
();
this
.
objectMapper
=
objectMapper
;
}
public
String
buildAuthorizationRedirect
(
Map
<
String
,
String
>
params
)
{
String
redirectUri
=
normalizeRedirectUri
(
firstText
(
params
.
get
(
"redirect_uri"
),
properties
.
getRedirectUri
()));
UriComponentsBuilder
builder
=
UriComponentsBuilder
.
fromHttpUrl
(
properties
.
getAuthorizationUri
());
addQueryParam
(
builder
,
properties
.
getAuthorizationAppIdParam
(),
clientId
());
addQueryParam
(
builder
,
properties
.
getAuthorizationRedirectUriParam
(),
redirectUri
);
addQueryParam
(
builder
,
properties
.
getAuthorizationResponseTypeParam
(),
firstText
(
params
.
get
(
"response_type"
),
"code"
));
addQueryParam
(
builder
,
properties
.
getAuthorizationScopeParam
(),
firstText
(
params
.
get
(
"scope"
),
properties
.
getScope
()));
addQueryParam
(
builder
,
properties
.
getAuthorizationQrcodeParam
(),
properties
.
getAuthorizationQrcodeValue
());
if
(
StringUtils
.
hasText
(
properties
.
getAgentId
()))
{
builder
.
queryParam
(
"agentid"
,
properties
.
getAgentId
());
}
if
(
StringUtils
.
hasText
(
params
.
get
(
"state"
)))
{
addQueryParam
(
builder
,
properties
.
getAuthorizationStateParam
(),
params
.
get
(
"state"
));
}
String
target
=
builder
.
build
().
toUriString
();
if
(
properties
.
isAppendWechatRedirect
()
&&
target
.
indexOf
(
"#"
)
<
0
)
{
target
=
target
+
"#wechat_redirect"
;
}
log
.
info
(
"WeCom OAuth relay authorize redirect: redirectUri={}, target={}"
,
redirectUri
,
target
);
return
target
;
}
public
String
buildMockAuthorizationRedirect
(
Map
<
String
,
String
>
params
)
{
if
(!
properties
.
isMockEnabled
())
{
return
buildAuthorizationRedirect
(
params
);
}
String
redirectUri
=
normalizeRedirectUri
(
firstText
(
params
.
get
(
"redirect_uri"
),
properties
.
getRedirectUri
()));
UriComponentsBuilder
builder
=
UriComponentsBuilder
.
fromHttpUrl
(
redirectUri
)
.
queryParam
(
"code"
,
"WCMOCK-"
+
UUID
.
randomUUID
().
toString
().
replace
(
"-"
,
""
));
if
(
StringUtils
.
hasText
(
params
.
get
(
"state"
)))
{
builder
.
queryParam
(
"state"
,
params
.
get
(
"state"
));
}
String
target
=
builder
.
build
().
toUriString
();
log
.
info
(
"WeCom OAuth relay mock authorize redirect: redirectUri={}, target={}"
,
redirectUri
,
target
);
return
target
;
}
private
void
addQueryParam
(
UriComponentsBuilder
builder
,
String
name
,
String
value
)
{
if
(
StringUtils
.
hasText
(
name
)
&&
StringUtils
.
hasText
(
value
))
{
builder
.
queryParam
(
name
,
value
);
}
}
private
String
normalizeRedirectUri
(
String
redirectUri
)
{
if
(
StringUtils
.
hasText
(
redirectUri
)
&&
redirectUri
.
contains
(
"{HAP}"
)
&&
StringUtils
.
hasText
(
properties
.
getRedirectUri
()))
{
return
properties
.
getRedirectUri
();
}
validateRedirectUri
(
redirectUri
);
return
redirectUri
;
}
private
void
validateRedirectUri
(
String
redirectUri
)
{
if
(!
StringUtils
.
hasText
(
redirectUri
)
||
!
StringUtils
.
hasText
(
properties
.
getAllowedRedirectHost
()))
{
return
;
}
try
{
URI
uri
=
new
URI
(
redirectUri
);
String
host
=
uri
.
getHost
();
if
(!
properties
.
getAllowedRedirectHost
().
equalsIgnoreCase
(
host
))
{
throw
new
IllegalArgumentException
(
"redirect_uri host is not allowed: "
+
host
);
}
}
catch
(
URISyntaxException
ex
)
{
throw
new
IllegalArgumentException
(
"redirect_uri is invalid: "
+
redirectUri
,
ex
);
}
}
public
Map
<
String
,
Object
>
exchangeToken
(
Map
<
String
,
String
>
params
)
{
String
code
=
params
.
get
(
"code"
);
if
(!
properties
.
isEnabled
())
{
return
oauthError
(
"wecom oauth relay disabled"
);
}
if
(!
StringUtils
.
hasText
(
code
))
{
return
oauthError
(
"code is required"
);
}
if
(!
StringUtils
.
hasText
(
clientId
())
||
!
StringUtils
.
hasText
(
clientSecret
()))
{
return
oauthError
(
"wecom app-id and app-secret are required"
);
}
try
{
if
(
properties
.
isMockEnabled
()
&&
code
.
startsWith
(
"WCMOCK-"
))
{
return
issueMockToken
(
code
);
}
log
.
info
(
"Campus OAuth relay real token exchange start: code={}, platformTokenUri={}, userByCodeUri={}"
,
mask
(
code
),
properties
.
getCodeExchangeUri
(),
properties
.
getTokenUserInfoUri
());
Map
<
String
,
Object
>
codeUser
=
getCampusCodeUser
(
code
);
return
issueTokenForCodeUser
(
codeUser
);
}
catch
(
RestClientException
ex
)
{
log
.
error
(
"Campus OAuth relay token exchange request failed: code={}"
,
mask
(
code
),
ex
);
return
oauthError
(
"token exchange request failed: "
+
ex
.
getMessage
());
}
catch
(
Exception
ex
)
{
log
.
error
(
"Campus OAuth relay token exchange failed: code={}"
,
mask
(
code
),
ex
);
return
oauthError
(
"token exchange failed: "
+
ex
.
getMessage
());
}
}
private
Map
<
String
,
Object
>
getCampusCodeUser
(
String
code
)
throws
Exception
{
if
(
StringUtils
.
hasText
(
properties
.
getCodeExchangeUri
()))
{
String
accessToken
=
getPlatformAccessToken
();
return
getCampusUserByCode
(
code
,
accessToken
);
}
if
(
StringUtils
.
hasText
(
properties
.
getAccessTokenUri
())
&&
StringUtils
.
hasText
(
properties
.
getCodeUserInfoUri
()))
{
log
.
warn
(
"Campus code exchange uri is empty, falling back to legacy WeCom qyapi flow"
);
String
accessToken
=
getCorpAccessToken
();
return
getCodeUser
(
accessToken
,
code
);
}
throw
new
IllegalStateException
(
"wecom-relay.code-exchange-uri is required for campus OAuth code exchange"
);
}
private
URI
buildPlatformTokenUri
()
{
UriComponentsBuilder
builder
=
UriComponentsBuilder
.
fromHttpUrl
(
properties
.
getCodeExchangeUri
());
addQueryParam
(
builder
,
properties
.
getCodeExchangeAppIdParam
(),
clientId
());
addQueryParam
(
builder
,
properties
.
getCodeExchangeAppSecretParam
(),
clientSecret
());
return
builder
.
build
(
true
).
toUri
();
}
private
String
getPlatformAccessToken
()
throws
Exception
{
PlatformAccessToken
current
=
platformAccessToken
;
if
(
current
!=
null
&&
!
current
.
isExpired
())
{
return
current
.
getToken
();
}
synchronized
(
this
)
{
current
=
platformAccessToken
;
if
(
current
!=
null
&&
!
current
.
isExpired
())
{
return
current
.
getToken
();
}
URI
uri
=
buildPlatformTokenUri
();
log
.
info
(
"Campus platform token request: uri={}"
,
maskSecret
(
uri
.
toString
()));
ResponseEntity
<
String
>
response
=
restTemplate
.
getForEntity
(
uri
,
String
.
class
);
Map
<
String
,
Object
>
body
=
parseJson
(
response
.
getBody
());
log
.
info
(
"Campus platform token response: httpStatus={}, body={}"
,
response
.
getStatusCodeValue
(),
maskAccessTokenBody
(
body
));
assertCampusSuccess
(
body
,
"platform token"
);
Map
<
String
,
Object
>
data
=
unwrapData
(
body
);
String
token
=
firstText
(
stringValue
(
data
.
get
(
"access_token"
)),
stringValue
(
body
.
get
(
"access_token"
)));
if
(!
StringUtils
.
hasText
(
token
))
{
throw
new
IllegalStateException
(
"Campus platform token response missing access_token: "
+
body
);
}
int
expiresIn
=
intValue
(
firstObject
(
data
.
get
(
"expires_in"
),
body
.
get
(
"expires_in"
),
null
,
null
),
7200
);
int
skew
=
Math
.
max
(
properties
.
getPlatformAccessTokenSkewSeconds
(),
0
);
Instant
expiresAt
=
Instant
.
now
().
plusSeconds
(
Math
.
max
(
expiresIn
-
skew
,
60
));
platformAccessToken
=
new
PlatformAccessToken
(
token
,
expiresAt
);
log
.
info
(
"Campus platform token cached: expiresAt={}"
,
expiresAt
);
return
token
;
}
}
private
Map
<
String
,
Object
>
getCampusUserByCode
(
String
code
,
String
accessToken
)
throws
Exception
{
if
(!
StringUtils
.
hasText
(
properties
.
getTokenUserInfoUri
()))
{
throw
new
IllegalStateException
(
"wecom-relay.token-user-info-uri is required"
);
}
URI
uri
=
UriComponentsBuilder
.
fromHttpUrl
(
properties
.
getTokenUserInfoUri
())
.
queryParam
(
properties
.
getTokenUserInfoTokenParam
(),
accessToken
)
.
queryParam
(
properties
.
getCodeParam
(),
code
)
.
build
(
true
)
.
toUri
();
log
.
info
(
"Campus user-by-code request: code={}, token={}, uri={}"
,
mask
(
code
),
mask
(
accessToken
),
maskAccessTokenInUri
(
uri
.
toString
(),
accessToken
));
ResponseEntity
<
String
>
response
=
restTemplate
.
getForEntity
(
uri
,
String
.
class
);
Map
<
String
,
Object
>
body
=
parseJson
(
response
.
getBody
());
log
.
info
(
"Campus user-by-code response: httpStatus={}, body={}"
,
response
.
getStatusCodeValue
(),
body
);
assertCampusSuccess
(
body
,
"user by code"
);
return
unwrapData
(
body
);
}
private
Map
<
String
,
Object
>
issueTokenForCodeUser
(
Map
<
String
,
Object
>
codeUser
)
{
String
workNo
=
firstText
(
roleNumber
(
codeUser
),
stringValue
(
codeUser
.
get
(
"number"
)),
stringValue
(
codeUser
.
get
(
"workNo"
)),
stringValue
(
codeUser
.
get
(
"work_no"
)),
firstText
(
stringValue
(
codeUser
.
get
(
"workId"
)),
stringValue
(
codeUser
.
get
(
"work_id"
)),
stringValue
(
codeUser
.
get
(
"UserId"
)),
stringValue
(
codeUser
.
get
(
"userid"
)),
firstText
(
stringValue
(
codeUser
.
get
(
"user_id"
)),
stringValue
(
codeUser
.
get
(
"uid"
)),
stringValue
(
codeUser
.
get
(
"id"
)))));
String
mobile
=
firstText
(
stringValue
(
codeUser
.
get
(
"mobile"
)),
stringValue
(
codeUser
.
get
(
"mobilePhone"
)),
stringValue
(
codeUser
.
get
(
"phone"
)));
TeacherInfo
teacher
=
teacherDirectoryService
.
findByWorkNo
(
workNo
);
if
(
teacher
==
null
&&
StringUtils
.
hasText
(
mobile
))
{
teacher
=
teacherDirectoryService
.
findByMobile
(
mobile
);
}
if
(
teacher
==
null
)
{
String
missingKey
=
StringUtils
.
hasText
(
workNo
)
?
workNo
:
firstText
(
mobile
,
"unknown"
);
log
.
warn
(
"Campus OAuth relay teacher cache miss: workNo={}, platformUid={}, mobile={}, identity={}, teacherCount={}"
,
workNo
,
codeUser
.
get
(
"uid"
),
mobile
,
roleIdentity
(
codeUser
),
teacherDirectoryService
.
size
());
return
oauthError
(
"teacher not found: "
+
missingKey
);
}
String
localToken
=
"WCT-"
+
UUID
.
randomUUID
().
toString
().
replace
(
"-"
,
""
);
Instant
expiresAt
=
Instant
.
now
().
plusSeconds
(
Math
.
max
(
properties
.
getTokenExpiresIn
(),
60
));
localTokens
.
put
(
localToken
,
new
LocalToken
(
localToken
,
expiresAt
,
teacher
,
codeUser
,
new
LinkedHashMap
<
String
,
Object
>()));
cleanupExpiredTokens
();
Map
<
String
,
Object
>
body
=
new
LinkedHashMap
<
String
,
Object
>();
body
.
put
(
"access_token"
,
localToken
);
body
.
put
(
"token_type"
,
"Bearer"
);
body
.
put
(
"expires_in"
,
properties
.
getTokenExpiresIn
());
log
.
info
(
"Campus OAuth relay local token issued: token={}, workNo={}, expiresAt={}"
,
mask
(
localToken
),
teacher
.
getWorkNo
(),
expiresAt
);
return
body
;
}
private
Map
<
String
,
Object
>
issueMockToken
(
String
code
)
{
TeacherInfo
teacher
=
new
TeacherInfo
(
properties
.
getMockWorkNo
(),
properties
.
getMockName
(),
properties
.
getMockEmail
(),
properties
.
getMockMobile
());
Map
<
String
,
Object
>
codeUser
=
new
LinkedHashMap
<
String
,
Object
>();
codeUser
.
put
(
"mock"
,
true
);
codeUser
.
put
(
"code"
,
code
);
codeUser
.
put
(
"UserId"
,
teacher
.
getWorkNo
());
codeUser
.
put
(
"mobile"
,
teacher
.
getMobile
());
String
localToken
=
"WCT-"
+
UUID
.
randomUUID
().
toString
().
replace
(
"-"
,
""
);
Instant
expiresAt
=
Instant
.
now
().
plusSeconds
(
Math
.
max
(
properties
.
getTokenExpiresIn
(),
60
));
localTokens
.
put
(
localToken
,
new
LocalToken
(
localToken
,
expiresAt
,
teacher
,
codeUser
,
new
LinkedHashMap
<
String
,
Object
>()));
Map
<
String
,
Object
>
body
=
new
LinkedHashMap
<
String
,
Object
>();
body
.
put
(
"access_token"
,
localToken
);
body
.
put
(
"token_type"
,
"Bearer"
);
body
.
put
(
"expires_in"
,
properties
.
getTokenExpiresIn
());
log
.
info
(
"WeCom OAuth relay mock local token issued: token={}, workNo={}, expiresAt={}"
,
mask
(
localToken
),
teacher
.
getWorkNo
(),
expiresAt
);
return
body
;
}
public
Map
<
String
,
Object
>
userInfo
(
String
token
)
{
if
(!
properties
.
isEnabled
())
{
return
resultError
(
"wecom oauth relay disabled"
);
}
if
(!
StringUtils
.
hasText
(
token
))
{
return
resultError
(
"token or access_token is required"
);
}
LocalToken
localToken
=
localTokens
.
get
(
token
);
if
(
localToken
==
null
||
localToken
.
isExpired
())
{
if
(
localToken
!=
null
)
{
localTokens
.
remove
(
token
);
}
return
resultError
(
"token expired or invalid"
);
}
TeacherInfo
teacher
=
localToken
.
getTeacher
();
String
email
=
teacher
.
getEmail
();
if
(!
StringUtils
.
hasText
(
email
)
&&
properties
.
isSynthesizeEmail
()
&&
StringUtils
.
hasText
(
teacher
.
getWorkNo
())
&&
StringUtils
.
hasText
(
properties
.
getEmailDomain
()))
{
email
=
teacher
.
getWorkNo
()
+
"@"
+
properties
.
getEmailDomain
();
}
Map
<
String
,
Object
>
data
=
new
LinkedHashMap
<
String
,
Object
>();
data
.
put
(
"uid"
,
teacher
.
getWorkNo
());
data
.
put
(
"name"
,
firstText
(
teacher
.
getName
(),
stringValue
(
localToken
.
getCodeUser
().
get
(
"realname"
)),
teacher
.
getWorkNo
()));
data
.
put
(
"email"
,
firstText
(
email
,
stringValue
(
localToken
.
getCodeUser
().
get
(
"email"
))));
data
.
put
(
"mobilePhone"
,
firstText
(
teacher
.
getMobile
(),
stringValue
(
localToken
.
getCodeUser
().
get
(
"mobile"
))));
data
.
put
(
"mobile"
,
firstText
(
teacher
.
getMobile
(),
stringValue
(
localToken
.
getCodeUser
().
get
(
"mobile"
))));
data
.
put
(
"workId"
,
teacher
.
getWorkNo
());
data
.
put
(
"raw"
,
localToken
.
getCodeUser
());
Map
<
String
,
Object
>
result
=
new
LinkedHashMap
<
String
,
Object
>();
result
.
put
(
"data"
,
data
);
log
.
info
(
"WeCom OAuth relay Mingdao userInfo result: workNo={}, token={}"
,
teacher
.
getWorkNo
(),
mask
(
token
));
return
result
;
}
public
String
maskForLog
(
String
value
)
{
return
mask
(
value
);
}
private
String
getCorpAccessToken
()
throws
Exception
{
CorpAccessToken
current
=
corpAccessToken
;
if
(
current
!=
null
&&
!
current
.
isExpired
())
{
return
current
.
getToken
();
}
synchronized
(
this
)
{
current
=
corpAccessToken
;
if
(
current
!=
null
&&
!
current
.
isExpired
())
{
return
current
.
getToken
();
}
URI
uri
=
UriComponentsBuilder
.
fromHttpUrl
(
properties
.
getAccessTokenUri
())
.
queryParam
(
"corpid"
,
clientId
())
.
queryParam
(
"corpsecret"
,
clientSecret
())
.
build
(
true
)
.
toUri
();
log
.
info
(
"WeCom corp access token request: uri={}"
,
maskSecret
(
uri
.
toString
()));
ResponseEntity
<
String
>
response
=
restTemplate
.
getForEntity
(
uri
,
String
.
class
);
Map
<
String
,
Object
>
body
=
parseJson
(
response
.
getBody
());
assertWeComSuccess
(
body
,
"corp access token"
);
String
token
=
stringValue
(
body
.
get
(
"access_token"
));
if
(!
StringUtils
.
hasText
(
token
))
{
throw
new
IllegalStateException
(
"WeCom access token response missing access_token"
);
}
int
expiresIn
=
intValue
(
body
.
get
(
"expires_in"
),
7200
);
int
skew
=
Math
.
max
(
properties
.
getCorpAccessTokenSkewSeconds
(),
0
);
Instant
expiresAt
=
Instant
.
now
().
plusSeconds
(
Math
.
max
(
expiresIn
-
skew
,
60
));
corpAccessToken
=
new
CorpAccessToken
(
token
,
expiresAt
);
log
.
info
(
"WeCom corp access token refreshed: expiresAt={}"
,
expiresAt
);
return
token
;
}
}
private
Map
<
String
,
Object
>
getCodeUser
(
String
accessToken
,
String
code
)
throws
Exception
{
URI
uri
=
UriComponentsBuilder
.
fromHttpUrl
(
properties
.
getCodeUserInfoUri
())
.
queryParam
(
"access_token"
,
accessToken
)
.
queryParam
(
"code"
,
code
)
.
build
(
true
)
.
toUri
();
log
.
info
(
"WeCom code userInfo request: code={}, uri={}"
,
mask
(
code
),
maskAccessTokenInUri
(
uri
.
toString
(),
accessToken
));
ResponseEntity
<
String
>
response
=
restTemplate
.
getForEntity
(
uri
,
String
.
class
);
Map
<
String
,
Object
>
body
=
parseJson
(
response
.
getBody
());
assertWeComSuccess
(
body
,
"code user info"
);
log
.
info
(
"WeCom code userInfo response: code={}, body={}"
,
mask
(
code
),
body
);
return
body
;
}
private
Map
<
String
,
Object
>
getUserDetailQuietly
(
String
accessToken
,
String
userId
)
{
if
(!
StringUtils
.
hasText
(
properties
.
getUserDetailUri
()))
{
return
new
LinkedHashMap
<
String
,
Object
>();
}
try
{
URI
uri
=
UriComponentsBuilder
.
fromHttpUrl
(
properties
.
getUserDetailUri
())
.
queryParam
(
"access_token"
,
accessToken
)
.
queryParam
(
"userid"
,
userId
)
.
build
(
true
)
.
toUri
();
log
.
info
(
"WeCom user detail request: userId={}, uri={}"
,
userId
,
maskAccessTokenInUri
(
uri
.
toString
(),
accessToken
));
ResponseEntity
<
String
>
response
=
restTemplate
.
getForEntity
(
uri
,
String
.
class
);
Map
<
String
,
Object
>
body
=
parseJson
(
response
.
getBody
());
assertWeComSuccess
(
body
,
"user detail"
);
return
body
;
}
catch
(
Exception
ex
)
{
log
.
warn
(
"WeCom user detail request ignored: userId={}, message={}"
,
userId
,
ex
.
getMessage
());
return
new
LinkedHashMap
<
String
,
Object
>();
}
}
private
void
assertWeComSuccess
(
Map
<
String
,
Object
>
body
,
String
action
)
{
Object
errCode
=
body
.
get
(
"errcode"
);
if
(
errCode
==
null
||
"0"
.
equals
(
String
.
valueOf
(
errCode
)))
{
return
;
}
throw
new
IllegalStateException
(
"WeCom "
+
action
+
" failed: "
+
body
);
}
private
void
assertCampusSuccess
(
Map
<
String
,
Object
>
body
,
String
action
)
{
Object
success
=
body
.
get
(
"success"
);
Object
code
=
firstObject
(
body
.
get
(
"e"
),
body
.
get
(
"code"
),
body
.
get
(
"errcode"
),
body
.
get
(
"errorCode"
));
if
(
code
==
null
)
{
code
=
body
.
get
(
"status"
);
}
if
(
Boolean
.
FALSE
.
equals
(
success
))
{
throw
new
IllegalStateException
(
"Campus "
+
action
+
" failed: "
+
body
);
}
if
(
code
==
null
||
"0"
.
equals
(
String
.
valueOf
(
code
))
||
"200"
.
equals
(
String
.
valueOf
(
code
)))
{
return
;
}
throw
new
IllegalStateException
(
"Campus "
+
action
+
" failed: "
+
body
);
}
private
Map
<
String
,
Object
>
unwrapData
(
Map
<
String
,
Object
>
body
)
{
Object
data
=
firstObject
(
body
.
get
(
"d"
),
body
.
get
(
"data"
),
body
.
get
(
"result"
),
body
.
get
(
"user"
));
if
(
data
==
null
)
{
data
=
body
.
get
(
"userInfo"
);
}
if
(
data
instanceof
Map
)
{
return
(
Map
<
String
,
Object
>)
data
;
}
return
body
;
}
private
Map
<
String
,
Object
>
maskAccessTokenBody
(
Map
<
String
,
Object
>
body
)
{
Map
<
String
,
Object
>
masked
=
new
LinkedHashMap
<
String
,
Object
>(
body
);
maskAccessTokenField
(
masked
);
Object
data
=
masked
.
get
(
"d"
);
if
(
data
instanceof
Map
)
{
Map
<
String
,
Object
>
maskedData
=
new
LinkedHashMap
<
String
,
Object
>((
Map
<
String
,
Object
>)
data
);
maskAccessTokenField
(
maskedData
);
masked
.
put
(
"d"
,
maskedData
);
}
return
masked
;
}
private
void
maskAccessTokenField
(
Map
<
String
,
Object
>
body
)
{
Object
accessToken
=
body
.
get
(
"access_token"
);
if
(
accessToken
!=
null
)
{
body
.
put
(
"access_token"
,
mask
(
String
.
valueOf
(
accessToken
)));
}
}
private
void
cleanupExpiredTokens
()
{
for
(
Map
.
Entry
<
String
,
LocalToken
>
entry
:
localTokens
.
entrySet
())
{
if
(
entry
.
getValue
().
isExpired
())
{
localTokens
.
remove
(
entry
.
getKey
());
}
}
}
private
Map
<
String
,
Object
>
oauthError
(
String
message
)
{
Map
<
String
,
Object
>
body
=
new
LinkedHashMap
<
String
,
Object
>();
body
.
put
(
"error"
,
"invalid_request"
);
body
.
put
(
"error_description"
,
message
);
return
body
;
}
private
Map
<
String
,
Object
>
resultError
(
String
message
)
{
Map
<
String
,
Object
>
body
=
new
LinkedHashMap
<
String
,
Object
>();
body
.
put
(
"success"
,
false
);
body
.
put
(
"error_code"
,
10101
);
body
.
put
(
"error_msg"
,
message
);
return
body
;
}
private
Map
<
String
,
Object
>
parseJson
(
String
json
)
throws
Exception
{
if
(!
StringUtils
.
hasText
(
json
))
{
return
new
LinkedHashMap
<
String
,
Object
>();
}
return
objectMapper
.
readValue
(
json
,
new
TypeReference
<
Map
<
String
,
Object
>>()
{
});
}
private
String
firstText
(
String
first
,
String
second
)
{
return
StringUtils
.
hasText
(
first
)
?
first
:
second
;
}
private
Object
firstObject
(
Object
first
,
Object
second
,
Object
third
,
Object
fourth
)
{
if
(
first
!=
null
)
{
return
first
;
}
if
(
second
!=
null
)
{
return
second
;
}
return
third
!=
null
?
third
:
fourth
;
}
private
String
roleNumber
(
Map
<
String
,
Object
>
codeUser
)
{
Object
role
=
codeUser
.
get
(
"role"
);
if
(
role
instanceof
Map
)
{
return
stringValue
(((
Map
<?,
?>)
role
).
get
(
"number"
));
}
return
null
;
}
private
String
roleIdentity
(
Map
<
String
,
Object
>
codeUser
)
{
Object
role
=
codeUser
.
get
(
"role"
);
if
(
role
instanceof
Map
)
{
return
stringValue
(((
Map
<?,
?>)
role
).
get
(
"identity"
));
}
return
null
;
}
private
String
firstText
(
String
first
,
String
second
,
String
third
)
{
return
StringUtils
.
hasText
(
first
)
?
first
:
(
StringUtils
.
hasText
(
second
)
?
second
:
third
);
}
private
String
firstText
(
String
first
,
String
second
,
String
third
,
String
fourth
)
{
return
StringUtils
.
hasText
(
first
)
?
first
:
firstText
(
second
,
third
,
fourth
);
}
private
String
firstText
(
String
first
,
String
second
,
String
third
,
String
fourth
,
String
fifth
)
{
return
StringUtils
.
hasText
(
first
)
?
first
:
firstText
(
second
,
third
,
fourth
,
fifth
);
}
private
String
stringValue
(
Object
value
)
{
return
value
==
null
?
null
:
String
.
valueOf
(
value
);
}
private
int
intValue
(
Object
value
,
int
defaultValue
)
{
if
(
value
instanceof
Number
)
{
return
((
Number
)
value
).
intValue
();
}
if
(
value
!=
null
)
{
try
{
return
Integer
.
parseInt
(
String
.
valueOf
(
value
));
}
catch
(
Exception
ignored
)
{
}
}
return
defaultValue
;
}
private
String
mask
(
String
value
)
{
if
(!
StringUtils
.
hasText
(
value
))
{
return
""
;
}
if
(
value
.
length
()
<=
12
)
{
return
value
.
substring
(
0
,
Math
.
min
(
value
.
length
(),
4
))
+
"***"
;
}
return
value
.
substring
(
0
,
6
)
+
"***"
+
value
.
substring
(
value
.
length
()
-
6
);
}
private
String
maskSecret
(
String
value
)
{
if
(!
StringUtils
.
hasText
(
value
))
{
return
value
;
}
return
value
.
replace
(
clientSecret
(),
"***"
);
}
private
String
clientId
()
{
return
firstText
(
properties
.
getAppId
(),
properties
.
getCorpId
());
}
private
String
clientSecret
()
{
return
firstText
(
properties
.
getAppSecret
(),
properties
.
getCorpSecret
());
}
private
String
maskAccessTokenInUri
(
String
uri
,
String
token
)
{
if
(!
StringUtils
.
hasText
(
uri
)
||
!
StringUtils
.
hasText
(
token
))
{
return
uri
;
}
return
uri
.
replace
(
token
,
mask
(
token
));
}
private
static
class
CorpAccessToken
{
private
final
String
token
;
private
final
Instant
expiresAt
;
private
CorpAccessToken
(
String
token
,
Instant
expiresAt
)
{
this
.
token
=
token
;
this
.
expiresAt
=
expiresAt
;
}
private
String
getToken
()
{
return
token
;
}
private
boolean
isExpired
()
{
return
Instant
.
now
().
isAfter
(
expiresAt
);
}
}
private
static
class
PlatformAccessToken
{
private
final
String
token
;
private
final
Instant
expiresAt
;
private
PlatformAccessToken
(
String
token
,
Instant
expiresAt
)
{
this
.
token
=
token
;
this
.
expiresAt
=
expiresAt
;
}
private
String
getToken
()
{
return
token
;
}
private
boolean
isExpired
()
{
return
Instant
.
now
().
isAfter
(
expiresAt
);
}
}
private
static
class
LocalToken
{
private
final
String
token
;
private
final
Instant
expiresAt
;
private
final
TeacherInfo
teacher
;
private
final
Map
<
String
,
Object
>
codeUser
;
private
final
Map
<
String
,
Object
>
userDetail
;
private
LocalToken
(
String
token
,
Instant
expiresAt
,
TeacherInfo
teacher
,
Map
<
String
,
Object
>
codeUser
,
Map
<
String
,
Object
>
userDetail
)
{
this
.
token
=
token
;
this
.
expiresAt
=
expiresAt
;
this
.
teacher
=
teacher
;
this
.
codeUser
=
codeUser
;
this
.
userDetail
=
userDetail
;
}
private
TeacherInfo
getTeacher
()
{
return
teacher
;
}
private
Map
<
String
,
Object
>
getCodeUser
()
{
return
codeUser
;
}
private
boolean
isExpired
()
{
return
Instant
.
now
().
isAfter
(
expiresAt
);
}
}
}
src/main/java/com/jd/easywalk/casrelay/WeComRelayStartupLogger.java
0 → 100644
View file @
13e594d6
package
com
.
jd
.
easywalk
.
casrelay
;
import
javax.annotation.PostConstruct
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.springframework.stereotype.Component
;
@Component
public
class
WeComRelayStartupLogger
{
private
static
final
Logger
log
=
LoggerFactory
.
getLogger
(
WeComRelayStartupLogger
.
class
);
private
final
WeComRelayProperties
properties
;
public
WeComRelayStartupLogger
(
WeComRelayProperties
properties
)
{
this
.
properties
=
properties
;
}
@PostConstruct
public
void
logConfig
()
{
log
.
info
(
"WeCom relay config: enabled={}, appId={}, platformDomain={}, authorizationUri={}, "
+
"redirectUri={}, appIdParam={}, redirectParam={}, qrcodeParam={}, codeExchangeUri={}, mockEnabled={}"
,
properties
.
isEnabled
(),
properties
.
getAppId
(),
properties
.
getPlatformDomain
(),
properties
.
getAuthorizationUri
(),
properties
.
getRedirectUri
(),
properties
.
getAuthorizationAppIdParam
(),
properties
.
getAuthorizationRedirectUriParam
(),
properties
.
getAuthorizationQrcodeParam
(),
properties
.
getCodeExchangeUri
(),
properties
.
isMockEnabled
());
}
}
src/main/resources/META-INF/spring.factories
0 → 100644
View file @
13e594d6
org.springframework.context.ApplicationContextInitializer=\
com.jd.easywalk.casrelay.DotenvPropertySourceInitializer
src/main/resources/application.yml
View file @
13e594d6
...
...
@@ -42,3 +42,45 @@ oauth-relay:
expires-in
:
${OAUTH_EXPIRES_IN:7200}
email-domain
:
${OAUTH_EMAIL_DOMAIN:xidian.edu.cn}
synthesize-email
:
${OAUTH_SYNTHESIZE_EMAIL:
true
}
wecom-relay
:
enabled
:
${WECOM_RELAY_ENABLED:
false
}
app-id
:
${WECOM_APP_ID:200260525100013951}
app-secret
:
${WECOM_APP_SECRET:}
platform-domain
:
${WECOM_PLATFORM_DOMAIN:xxcapp.xidian.edu.cn}
corp-id
:
${WECOM_CORP_ID:}
agent-id
:
${WECOM_AGENT_ID:}
corp-secret
:
${WECOM_CORP_SECRET:}
authorization-uri
:
${WECOM_AUTHORIZATION_URI:https://xxcapp.xidian.edu.cn/uc/api/oauth/index}
authorization-app-id-param
:
${WECOM_AUTHORIZATION_APP_ID_PARAM:appid}
authorization-redirect-uri-param
:
${WECOM_AUTHORIZATION_REDIRECT_URI_PARAM:redirect}
authorization-response-type-param
:
${WECOM_AUTHORIZATION_RESPONSE_TYPE_PARAM:}
authorization-scope-param
:
${WECOM_AUTHORIZATION_SCOPE_PARAM:}
authorization-state-param
:
${WECOM_AUTHORIZATION_STATE_PARAM:state}
authorization-qrcode-param
:
${WECOM_AUTHORIZATION_QRCODE_PARAM:qrcode}
authorization-qrcode-value
:
${WECOM_AUTHORIZATION_QRCODE_VALUE:1}
code-exchange-uri
:
${WECOM_CODE_EXCHANGE_URI:https://xxcapp.xidian.edu.cn/api/third/get-token}
token-user-info-uri
:
${WECOM_TOKEN_USER_INFO_URI:https://xxcapp.xidian.edu.cn/uc/api/oauth/user-by-code}
code-param
:
${WECOM_CODE_PARAM:code}
code-exchange-app-id-param
:
${WECOM_CODE_EXCHANGE_APP_ID_PARAM:appid}
code-exchange-app-secret-param
:
${WECOM_CODE_EXCHANGE_APP_SECRET_PARAM:appsecret}
token-user-info-token-param
:
${WECOM_TOKEN_USER_INFO_TOKEN_PARAM:access_token}
platform-access-token-skew-seconds
:
${WECOM_PLATFORM_ACCESS_TOKEN_SKEW_SECONDS:300}
access-token-uri
:
${WECOM_ACCESS_TOKEN_URI:}
code-user-info-uri
:
${WECOM_CODE_USER_INFO_URI:}
user-detail-uri
:
${WECOM_USER_DETAIL_URI:}
redirect-uri
:
${WECOM_REDIRECT_URI:}
allowed-redirect-host
:
${WECOM_ALLOWED_REDIRECT_HOST:nercwbs-lms.xidian.edu.cn}
scope
:
${WECOM_SCOPE:snsapi_base}
token-expires-in
:
${WECOM_TOKEN_EXPIRES_IN:7200}
corp-access-token-skew-seconds
:
${WECOM_CORP_ACCESS_TOKEN_SKEW_SECONDS:300}
email-domain
:
${WECOM_EMAIL_DOMAIN:xidian.edu.cn}
synthesize-email
:
${WECOM_SYNTHESIZE_EMAIL:
true
}
append-wechat-redirect
:
${WECOM_APPEND_WECHAT_REDIRECT:
true
}
connect-timeout-ms
:
${WECOM_CONNECT_TIMEOUT_MS:5000}
read-timeout-ms
:
${WECOM_READ_TIMEOUT_MS:10000}
mock-enabled
:
${WECOM_MOCK_ENABLED:
false
}
mock-work-no
:
${WECOM_MOCK_WORK_NO:20240001}
mock-name
:
${WECOM_MOCK_NAME:MockUser}
mock-email
:
${WECOM_MOCK_EMAIL:20240001@xidian.edu.cn}
mock-mobile
:
${WECOM_MOCK_MOBILE:13800000000}
sso.wecom.example.json
0 → 100644
View file @
13e594d6
{
"mode"
:
"common-oauth2"
,
"name"
:
"xidian-wecom-oauth-relay"
,
"oauth2"
:
{
"oauth2Url"
:
"https://casrelay.xidian.edu.cn"
,
"clientId"
:
"mingdao-wecom-relay"
,
"clientSecret"
:
"mingdao-wecom-relay-secret"
,
"idParamName"
:
"client_id"
,
"secretParamName"
:
"client_secret"
,
"accessTokenParamName"
:
"access_token"
,
"redirectUri"
:
"https://nercwbs-lms.xidian.edu.cn/orgsso/oauth2"
,
"authorizePath"
:
"/wecom/oauth2/authorize"
,
"tokenPath"
:
"/wecom/oauth2/access_token"
,
"userInfoUrl"
:
"https://casrelay.xidian.edu.cn/wecom/oauth2/userinfo"
,
"tokenKeyParamName"
:
"token"
,
"authorizationMethod"
:
"body"
,
"bodyFormat"
:
"form"
,
"scope"
:
"snsapi_base"
,
"params"
:
{
"UserId"
:
"uid"
,
"Name"
:
"name"
,
"Email"
:
"email"
,
"Mobile"
:
"mobilePhone"
},
"autoRegister"
:
true
,
"projectId"
:
"22070640-479c-493b-9045-9c39e303cf2a"
}
}
sso.wecom.mock.example.json
0 → 100644
View file @
13e594d6
{
"mode"
:
"common-oauth2"
,
"name"
:
"xidian-wecom-oauth-relay-mock"
,
"oauth2"
:
{
"oauth2Url"
:
"https://casrelay.xidian.edu.cn"
,
"clientId"
:
"mingdao-wecom-relay"
,
"clientSecret"
:
"mingdao-wecom-relay-secret"
,
"idParamName"
:
"client_id"
,
"secretParamName"
:
"client_secret"
,
"accessTokenParamName"
:
"access_token"
,
"redirectUri"
:
"https://nercwbs-lms.xidian.edu.cn/orgsso/oauth2"
,
"authorizePath"
:
"/wecom/oauth2/mock/authorize"
,
"tokenPath"
:
"/wecom/oauth2/access_token"
,
"userInfoUrl"
:
"https://casrelay.xidian.edu.cn/wecom/oauth2/userinfo"
,
"tokenKeyParamName"
:
"token"
,
"authorizationMethod"
:
"body"
,
"bodyFormat"
:
"form"
,
"scope"
:
""
,
"params"
:
{
"UserId"
:
"uid"
,
"Name"
:
"name"
,
"Email"
:
"email"
,
"Mobile"
:
"mobilePhone"
},
"autoRegister"
:
true
,
"projectId"
:
"22070640-479c-493b-9045-9c39e303cf2a"
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment