聚合支付宝、微信用户网页授权(OAuth)登录客户端。
自动装配,只需配置简单的中间件和授权参数即可。
下文提及的"宿主项目"一律指代:安装了 janus-server-sdk 的 spring-boot 工程。
参考资料:
内部处理逻辑:
- 系统异常:
- 请求失败:请求未能打到支付宝/微信网关,可能是:连接超时、DSN解析异常、网络故障、或者网关接口响应超时...
- 响应错误:支付宝/微信接口返回非2xx HTTP状态码
- 未知异常:支付宝/微信接口异常,未按文档约定返回正确的HTTP报文
- 内部错误:中间件不可用
- 业务异常:
- 微信接口返回错误码、错误信息
- 支付宝接口返回错误码、错误信息
在 application.yml 中自定义以下参数:
janus:
failure-url: /500
fallback-url: /401
denied-url: /403
logout-request-url: /logout
logout-success-url: /logout/success
以上均为缺省值。宿主项目可以在对应路由下返回一个对用户友好的HTML错误页。
repositories {
maven {
url "https://freeman.bintray.com/janus"
}
}
dependencies {
implementation 'com.github.xuyuanxiang:janus-server-sdk:1.0.0'
}
settings.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<settings xsi:schemaLocation='http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd'
xmlns='http://maven.apache.org/SETTINGS/1.0.0' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
<profiles>
<profile>
<repositories>
<repository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>bintray-freeman-janus</id>
<name>bintray</name>
<url>https://freeman.bintray.com/janus</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>bintray-freeman-janus</id>
<name>bintray-plugins</name>
<url>https://freeman.bintray.com/janus</url>
</pluginRepository>
</pluginRepositories>
<id>bintray</id>
</profile>
</profiles>
<activeProfiles>
<activeProfile>bintray</activeProfile>
</activeProfiles>
</settings>
pom.xml:
<dependency>
<groupId>com.github.xuyuanxiang</groupId>
<artifactId>janus-server-sdk</artifactId>
<version>1.0.0</version>
<type>pom</type>
</dependency>
Janus所有参数示例:
janus:
alipay:
enabled: false
app-id: # 当janus.alipay.enabled值为true时,必填
sign-type: # 当janus.alipay.enabled值为true时,必填
private-key: # 当janus.alipay.enabled值为true时,必填
time-zone: Asia/Shanghai
wechat:
enabled: false
appid: # 当janus.wechat.enabled值为true时,必填
secret: # 当janus.wechat.enabled值为true时,必填
fallback-url: /401
denied-url: /403
failure-url: /500
logout-success-url: /logout/success
logout-request-url: /logout
read-timeout: 30s
connection-timeout: 2s
没有特别注释的均为选填参数,上面填入的为缺省值,等价于不填。
安装依赖后在application.yml
文件中键入:janus
前缀通过IDE浮窗快速查看属性定义:
或者查看源码注释:JanusProperties.java。
第一步,添加spring依赖;
Gradle:
dependencies {
implementation "org.springframework.boot:spring-boot-starter-data-redis"
implementation "org.springframework.session:spring-session-data-redis"
}
Maven:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
</dependencies>
第二步,配置redis连接参数:
spring:
redis:
lettuce:
pool:
max-active: 32
max-idle: 8
max-wait: 2s
timeout: 600ms
host: localhost
port: 6379
password: mypassword
database: 11
session:
redis:
namespace: myApp:session
timeout: 2h
janus:
alipay:
app-id: APPID
sign-type: RSA2
private-key: PRIVATE—KEY
wechat:
appid: APPID
secret: SECRET
在spring-session文档中查看更多 session 存储方式。
默认情况下,用户使用支付宝或微信在未授权情况下访问任意路径,都会跳转到相应的授权页面,待用户同意授权后会返回未登录前所访问的路径。
注入一个 Bean 实现自定义:
import com.github.xuyuanxiang.janus.custom.CustomAuthorizationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
@Configuration
public class MyConfiguration {
@Bean
CustomAuthorizationConfiguration customAuthorizationConfiguration() {
// Expression-Based Access Control
return httpSecurity -> httpSecurity.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/protected", "/ping").permitAll()
.anyRequest().authenticated();
}
}
spring-security 文档中有详细的使用方式:
- Expression-Based Access Control
- 除了上面代码示例中的方式,还可以使用Method Security
import com.github.xuyuanxiang.janus.model.JanusAuthentication;
import com.github.xuyuanxiang.janus.model.User;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/profile")
public class MyController {
@GetMapping
public User getUser() {
JanusAuthentication authentication = (JanusAuthentication) SecurityContextHolder.getContext().getAuthentication();
return authentication.getUser();
}
}
属性详见User 类。
JanusAuthentication 类储存了 access_token 及其有效期,创建时间,授权方式等信息。
下文中使用${expression}
来标识具有特定含义的内容,比如:占位符,伪代码等等。
User-Agent
必须包含:MicroMessenger
或者AlipayClient
,即需要在支付宝或微信客户端中访问网页,否则一律响应:
HTTP/1.1 302 Redirection
Location: ${janus.fallback-url}?error=UNAUTHORIZED&error_description=${encodeURIComponent(请在支付宝或者微信中访问当前页面)}
可在application.yml
中配置janus.fallback-url
自定义路由,缺省值:/401
:
janus:
fallback-url: /401
宿主项目可在该请求路径下,响应一个对用户友好的 HTML 页之类的。
支付宝/微信客户端 Webview 请求:
GET /foo/bar HTTP/1.1
User-Agent: ${AlipayClient || MicroMessenger}
Host: ${我是宿主服务域名占位符}
janus-server-sdk 响应:
HTTP/1.1 302 Redirectiton
Location: ${支付宝 || 微信授权URL}
如果请求头Accept
中带有application/json
(比如宿主项目前端 Ajax 请求):
GET /foo/bar HTTP/1.1
Accept: application/json;charset=UTF-8
User-Agent: ${AlipayClient || MicroMessenger}
Host: ${我是宿主服务域名占位符}
janus-server-sdk 则会响应 JSON 格式数据,而不再跳转到授权 URL:
HTTP/1.1 401 Unauthorized
Content-Type: application/json;charset=UTF-8
{
"error": "UNAUTHORIZED",
"error_description": "请在支付宝或者微信中访问当前页面"
}
用户在支付宝或微信授权页面同意授权后,会携带 auth_code 返回/oauth/callback
路径,这一路径是写死的,不可配。
janus-server-sdk 一旦在/oauth/callback
请求路径中接收到 auth_code(支付宝)或者 code(微信)参数则会调用支付宝或微信接口获取 access_token,
拿到 access_token 后,再调支付宝或微信接口通过 access_token 换取用户信息。
**janus-server-sdk 会在系统异常首次发生后,递增间隔时间最多再重试 3 次,全都失败,会携带错误描述信息引导用户跳转到janus.failure-url
所配置的路由,缺省值:/500
。
可在application.yml
自定义其他值:
janus:
failure-url: /500
宿主项目可在该请求路径下,响应一个对用户友好的 HTML 错误页之类的。
系统异常细分下列 4 种情况:
- 请求失败——请求未能打到支付宝/微信网关,可能是:连接超时、DSN解析异常、网络故障、或者网关接口响应超时...:
HTTP/1.1 302 Redirectiton
Location: ${janus.failure-url}?error=WECHAT_REQUEST_FAILED&error_description=${encodeURIComponent(微信请求失败,请检查网络连接情况。异常:${0})}
error_description 参数占位符${0}
会替换为具体的异常,比如:微信请求失败,请检查网络连接情况。异常:java.net.SocketTimeoutException: connect timed out
。
可在application.yml
中配置请求和响应超时阈值:
janus:
connection-timeout: 5s # 请求超时阈值:5秒
read-timeout: 1m # 响应超时阈值:1分钟
- 响应错误——支付宝/微信接口返回非2xx HTTP状态码:
HTTP/1.1 302 Redirectiton
Location: ${janus.failure-url}?error=ALIPAY_RESPONSE_ERROR&error_description=${encodeURIComponent(支付宝网关不可用,请稍后重试或联系支付宝客服。响应报文:${0})}
error_description 参数占位符${0}
会替换为具体响应报文,比如:支付宝网关不可用,请稍后重试或联系支付宝客服。响应报文:<503 SERVICE_UNAVAILABLE Service Unavailable,[]>
。
- 未知异常——支付宝/微信接口异常,未按文档约定返回正确的HTTP报文:
HTTP/1.1 302 Redirectiton
Location: ${janus.failure-url}?error=WECHAT_UNKNOWN_ERROR&error_description=${encodeURIComponent(微信接口返回数据解析失败,可能是未按文档约定返回正确的HTTP报文格式或数据结构)}
正常情况下:
- 支付宝接口应该响应
Content-Type: text/html;Charset=UTF-8
,报文数据为 JSON 格式。 - 微信接口应该响
Content-Type: text/plain
,报文数据为 JSON 格式。
- 内部错误——中间件(比如:redis)不可用:
HTTP/1.1 302 Redirectiton
Location: ${janus.failure-url}?error=INTERNAL_SERVER_ERROR&error_description=${encodeURIComponent(系统错误:${0})}
error_description 参数占位符${0}
会替换为具体的异常,比如:系统错误:redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
。
支付宝/微信接口请求和响应成功,返回 JSON 结构体中携带错误信息和错误码。
HTTP/1.1 302 Redirectiton
Location: ${janus.denied-url}?error=WECHAT_BUSINESS_EXCEPTION&error_description=${encodeURIComponent(微信授权失败(errcode: ${0}, errmsg: ${1}))}
HTTP/1.1 302 Redirectiton
Location: ${janus.denied-url}?error=ALIPAY_BUSINESS_EXCEPTION&error_description=${encodeURIComponent(支付宝授权失败(code: {0}, msg: {1}, sub_code: {2}, sub_msg: {3}))}
error_description 中的占位符会被替换为支付宝/微信返回的错误码及错误信息字段值。
janus-server-sdk 不会对业务异常进行重试,失败 1 次即引导用户返回janus.denied-url
页面,缺省值:/403
。
可在application.yml
自定义其他值:
janus:
denied-url: /403
宿主项目可在该请求路径下,响应一个对用户友好的 HTML 错误页之类的。
项目自带简体中文的错误描述(即上文 error_description 字段):janus.properties。
其中的 key 即上文的 error 字段,${}
占位符会替换为实际运行时的内容。
INTERNAL_SERVER_ERROR=系统错误:${ExceptionUtils.getRootCause(throwable).toString()}
WECHAT_REQUEST_FAILED=微信请求失败,请检查网络连接情况。异常:${ExceptionUtils.getRootCause(throwable).toString()}
WECHAT_RESPONSE_ERROR=微信网关不可用,请稍后重试或联系微信客服。响应报文:${ResponseEntity.toString()}
WECHAT_BUSINESS_EXCEPTION=微信授权失败(errcode: ${微信接口返回字段}, errmsg: ${微信接口返回字段})
WECHAT_UNKNOWN_ERROR=微信接口返回数据解析失败,可能是未按文档约定返回正确的HTTP报文格式或数据结构
ALIPAY_REQUEST_FAILED=支付宝请求失败,请检查网络连接情况。异常:${ExceptionUtils.getRootCause(throwable).toString()}
ALIPAY_RESPONSE_ERROR=支付宝网关不可用,请稍后重试或联系支付宝客服。响应报文:${ResponseEntity.toString()}
ALIPAY_BUSINESS_EXCEPTION=支付宝授权失败(code: ${支付宝接口返回字段}, msg: ${支付宝接口返回字段}, sub_code: ${支付宝接口返回字段}, sub_msg: ${支付宝接口返回字段})
ALIPAY_UNKNOWN_ERROR=支付宝接口返回数据解析失败,可能是未按文档约定返回正确的HTTP报文格式或数据结构
FORBIDDEN=您没有权限访问当前页面
UNAUTHORIZED=请在支付宝或者微信中访问当前页面
如需其他语言的文案,可以在宿主项目resources
目录中存放比如:janus_en.properties
、janus_zh_TW.properties
等文件即可。
通过用户代理 HTTP 请求 Header 中的 Accept-Language 字段值匹配对应语言的描述信息。
比如,用户默认语言设为英文时,用户代理发送请求时通常会携带:
Accept-Language: en-US,en;
默认语言为中文时,用户代理发送请求时通常会携带:
Accept-Language: zh-CN;
使用 Nginx 或云服务(比如:阿里云 SLB)做代理转发时,只要有以下请求头之一:
Forwarded: proto=https;host=${宿主服务域名}
X-Forwarded-Proto: https
X-Forwarded-Ssl: on
spring-security 就"认为"原始请求是一个 HTTPS 请求,参考文献:https://tools.ietf.org/html/rfc7239。
请确保代理转发时添加了以上任意一个请求头。
阿里云 SLB 勾选红箭头所示选项:
Nginx:
server {
# 其他配置略
location / {
# 其他配置略
proxy_set_header X-Forwarded-Proto $scheme;
# 或者:
# proxy_set_header X-Forwarded-Ssl on;
}
}
专注于登录/退出。
Maven依赖,聚合登录SDK。
统一聚合登录服务(单点登录),依赖janus-server-sdk。
统一登录服务客户端SDK,对接janus-cas。
单服务部署集成janus-server-sdk即完成支付宝/微信用户授权登录的对接。
当存在多个微服务共用同一个支付宝应用/微信公众号ID的情况时,需要有唯一的中控服务:janus-cas来统一和支付宝与微信接口交互,维护和分发access_token、分享用户信息和会话等。
微服务集成janus-cas-client即完成与janus-cas的对接,实现同一个支付宝/微信用户在所有服务集群中统一登录,统一退出。