优锘SSO单点登录系统对接文档
版本时间编辑备注
v12021-05-26李忠 
v22021-07-26吴昕增加JS SDK
v32021-08-25李忠增加 Server Mode OAuth2
v42021-08-31李忠增加 OAuth2
v52021-09-09吴昕@uino/sso-sdk 1.2.1版本增加环境切换API
v62021-09-15李忠支持Refresh Token,Oauth2登录独立管理
v72021-09-28李忠支持OAuth2客户端退出
v82021-10-09李忠支持通过ThingJS源登录的用户获取模模搭ID接口
v92021-10-14钟宏增加Java SDK
v102021-10-26李忠【OAuth2】增加字段login_source登录源
v112021-11-30李忠增加微信小程序登录方式
v122021-12-03李忠支持OIDC协议
v132021-12-09李忠增加公众号登录,修改小程序登录参数

SSO已做为全公司所有应用的单点登录服务,现在主要支持两种方式对接

  • 内部域名应用
  • OAuth2

以下是详细教程

环境要求

使用OAuth2

联系管理员申请应用信息

使用JS SDK

Web访问必须是内部域名

  • uino.com
  • uino.cn
  • 3dcome.com
  • udolphin.com

API

获取用户信息

GET https://sso.uino.com/api/user/userinfo

Headers

  • Content-Type: application/json

Response

  • Status 401 校验登录凭证失败
  • Status 400 请求不合法

登录

在应用的Web页面上跳转到 https://sso.uino.com/api/login

登录成功之后,会跳转回原地址

获取当前登录用户的企业微信用户信息

GET https://sso.uino.com/api/user/wecom/myinfo

Headers

  • Content-Type: application/json

Response

  • Status 200 信息
{
  "enable":1,
  "status":1,
  "email":"example@uino.com",
  "userid":"example@uinnova.com",
  "name":"张三",
  "mobile":"13888888888",
  "gender":"1",
  "alias":"小明",
  "avatar":"",
  "department":[316],
  "main_department":316,
  "position":"研发"
}

获取企业微信组织架构信息

GET https://sso.uino.com/api/user/wecom/department

Headers

  • Content-Type: application/json

Response

  • Status 200 信息
{
  "errcode": 0,
  "errmsg": "ok",
  "department": [
    {"id": 1, "name": "优锘科技", "parentid": 0, "order": 100000000},
    {"id":586,"name":"项目交付中心","parentid":544,"order":99997000},
    ...
  ]
}

获取当前用户MMD_ID

GET https://sso.uino.com/api/user/thingjs/get_mmd_id

Headers

  • Content-Type: application/json

Response

  • Status 200 信息
{
  user_id: 1,
  mmd_id: 798
}

OAuth2

目前流行的一种授权模式

术语

  • client_id 客户端ID
  • client_secret 客户端密钥(不可泄漏)
  • callback_url 验证通过后跳回地址
  • redirect_uri 重定向地址
  • scopes 授权范围
  • name 应用名称
  • state 状态码,不能重复
  • code 授权码

请求授权Code

  • 请求

GET https://sso.uino.com/oauth/authorize?response_type=code&client_id=[CLIENT _ID]&redirect_uri=[授权的callback_url]&state=[状态码]

Headers

字段备注
User-Agent取浏览器的值必须
  • 响应
< HTTP/1.1 302 Found
< content-length: 0
< location: [你的redirect_uri]?code=[授权code]&state=[请求时带过来的状态码]
< date: Wed, 25 Aug 2021 03:44:58 GMT

获取AccessToken

这个Access Token是做为访问SSO所有资源必须的凭证

  • 请求

POST https://sso.uino.com/oauth/token

Headers

字段备注
Content-Typeapplication/x-www-form-urlencoded必须
User-AgentXXX (Internal App)必须,xxx为应用名称

Form

字段备注
grant_typeauthorization_code固定这个值
code授权码从redirect_uri的查询串里取
redirect_uri重定向地址授权的callback_url
client_id客户端ID 
client_secret客户端密钥 
  • 响应

200 Ok

{
  "access_token": "访问token值",
  "token_type": "cookie",
  "expires_in": "有效期(秒),默认7200",
  "refresh_token": "如果access_token过期,需要用此值更新",
  "created_at": "Token创建时间戳(秒)",
  "login_source": "登录源,如: thingjs, thingclub, sms(短信验证码), wecom(企业微信)"
}

刷新Token

Access Token有实效性,默认是7200秒2小时,过期后需要使用refresh token获取新的Access Token,并同时保存下新签发的Refresh Token作为下一次到期签发新的Access Token用

  • 请求

POST https://sso.uino.com/oauth/refresh

Headers

字段备注
Content-Typeapplication/x-www-form-urlencoded必须
User-AgentXXX (Internal App)必须,xxx为应用名称

Form

字段备注
grant_typerefresh_token固定这个值
client_id客户端ID 
client_secret客户端密钥 
refresh_token刷新Token在上一部的请求结果里有Refresh Token
  • 响应

200 Ok

{
  "access_token": "访问token值",
  "token_type": "cookie",
  "expires_in": "有效期(秒),默认7200",
  "refresh_token": "如果access_token过期,需要用此值更新",
  "created_at": "Token创建时间戳(秒)",
  "login_source": "登录源,如: thingjs, thingclub, sms(短信验证码), wecom(企业微信)"
}

废弃Token

如果应用要实现退出功能,可以使用此接口

  • 请求

POST https://sso.uino.com/oauth/destroy

Headers

字段备注
Content-Typeapplication/x-www-form-urlencoded必须
User-AgentXXX (Internal App)必须,xxx为应用名称

Form

字段备注
grant_typedestroy_token固定这个值
client_id客户端ID 
client_secret客户端密钥 
refresh_token刷新Token在上一部的请求结果里有Refresh Token
access_token访问Token在上一部的请求结果里有Access Token
  • 影响

200 OK

获取SSO信息

拿着以上获取的access_token,请求任何接口必须按照以下格式

Headers

字段备注
Content-Type看接口要求 
User-AgentXXX (Internal App)必须
AuthorizationBearer [ACCESS_TOKEN]必须

OIDC

OIDC是基于OAuth2协议的,在authorize时多传输一个scope=openid参数即可,OIDC协议在获取token时,会额外多一个id_token字段,这个字段是JWT格式,加密格式如下

base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)

加密用的key就是申请应用时颁发的client_secret,所以解密也用它即可,解密之后就会得到用户的基本信息。

Endpoints

  • Base Endpoint https://sso.uino.com/oauth/
  • Authorize Endpoint https://sso.uino.com/oauth/authorize
  • Token Endpoint https://sso.uino.com/oauth/token
  • Userinfo Endpoint https://sso.uino.com/oauth/userinfo
  • JWKS Endpoint https://sso.uino.com/oauth/jwks
  • End Endpoint https://sso.uino.com/oauth/logout

Authorize 额外参数

  • nonce 随机值,通常为了防止数据伪造
  • scope 必须,值固定为openid

id_token包含字段

  • iss 颁发者,固定为https://sso.uino.com
  • sub 用户ID
  • aud client_id
  • exp 过期时间戳
  • iat 颁发时间戳
  • username 用户名
  • auth_time 授权时间
  • phone_number 电话号码
  • email 邮箱地址
  • email_verfied 是否验证
  • name 姓名
  • nickname 昵称
  • picture 头像地址
  • nonce 上一步Authorize带过来的值

JS SDK

安装

tips: 必须先使用npm login登录公司npm服务,详情请看https://wiki.uino.com/d/60e7f227fb60e7827229745d.html

  • npm
 npm i @uino/sso-sdk
  • yarn
 yarn add @uino/sso-sdk

使用

import sso from "@uino/sso-sdk"
方法描述返回
loginsso登录
login_from_thingjsthingjs登录
get_user_info获取用户信息Promise
get_wechat_userinfo获取当前登录用户的企业微信用户信息Promise
get_wechat_department获取企业微信组织架构Promise
get_wechat_staffs获取企业微信员工列表Promise
is_login判断是否登录状态是返回true,否返回false
logout退出当前登录
useProdEnv切换成http://sso.uino.com(cn)下的接口
useLocalEnv切换成http://sso.spacex.uino.local下的接口
getCurrentEnv返回当前所处的mode“production”/"local"

用例

  • useProdEnv (切换成正式的环境), 该方法需要在所有api之前调用
sso.useProdEnv()
  • useLocalEnv (切换成测试的环境),该方法需要在所有api之前调用
sso.useLocalEnv()
  • getCurrentEnv (当前所处的mode(环境))
sso.getCurrentEnv()
  • login (登录)
sso.login()
  • login_from_thingjs (thingjs登录)
sso.login_from_thingjs()
  • get_user_info (获取用户信息)
sso.get_user_info().then().catch()
//or
try{
  const userinfo = await sso.get_user_info()  
}catch(err){

}
  • get_wechat_userinfo (获取当前登录用户的企业微信用户信息 )
sso.get_wechat_userinfo().then().catch()
//or
try{
  const userinfo = await sso.get_wechat_userinfo()  
}catch(err){

}
  • get_wechat_department (获取企业微组织架构 )
sso.get_wechat_department().then().catch()
//or
try{
  const userinfo = await sso.get_wechat_department()  
}catch(err){

}
  • get_wechat_staffs (获取企业微信员工列表 )
sso.get_wechat_staffs().then().catch()
//or
try{
  const staffs = await sso.get_wechat_staffs()  
}catch(err){

}
  • is_login (判断是否登录状态)
sso.is_login().then().catch()
//or
try{
  const is_login = await sso.is_login()  
}catch(err){

}
  • logout(退出当前登录)
sso.logout()

Server Mode OAuth2

Server Mode OAuth2模式适用于服务器的自动校验模式,跳过用户登录的过程,目前仅支持Basic Authentization模式,使用前需要提前联系SSO管理员获取应用的授权码。

术语

  • client_id 客户端ID
  • client_secret 客户端密钥(不可泄漏)
  • callback_url 验证通过后跳回地址
  • redirect_uri 重定向地址
  • scopes 授权范围
  • name 应用名称
  • state 状态码,不能重复
  • code 授权码

请求授权Code

  • 请求

GET https://sso.uino.com/asvc/authorize?response_type=code&client_id=[CLIENT _ID]&redirect_uri=[授权的callback_url]&state=[状态码]

  • 响应
< HTTP/1.1 302 Found
< content-length: 0
< location: [你的redirect_uri]?code=[授权code]&state=[请求时带过来的状态码]
< date: Wed, 25 Aug 2021 03:44:58 GMT

获取AccessToken

  • 请求

POST https://sso.uino.com/asvc/token

Headers

字段备注
Content-Typeapplication/x-www-form-urlencoded必须
User-AgentXXX (Internal App)必须

Form

字段备注
grant_typepassword固定这个值
code授权码从redirect_uri的查询串里取
redirect_uri重定向地址授权的callback_url
client_id客户端ID 
client_secret客户端密钥 
issue授权源目前可选 thingjs
authorization授权加密串按照issue源生成
  • 响应

200 Ok

{
  "access_token": "访问token值",
  "token_type": "cookie",
  "expires_in": "有效期(秒)",
  "refresh_token": "刷新token值",
  "created_at": "Token创建时间戳(秒)"
}

刷新Token

Access Token有实效性,默认是7200秒2小时,过期后需要使用refresh token获取新的Access Token,并同时保存下新签发的Refresh Token作为下一次到期签发新的Access Token用

  • 请求

POST https://sso.uino.com/asvc/refresh

Headers

字段备注
Content-Typeapplication/x-www-form-urlencoded必须
User-AgentXXX (Internal App)必须,xxx为应用名称

Form

字段备注
grant_typerefresh_token固定这个值
client_id客户端ID 
client_secret客户端密钥 
refresh_token刷新Token在上一部的请求结果里有Refresh Token
issue签发源与Access Token的签发源保持一致
  • 响应

200 Ok

{
  "access_token": "访问token值",
  "token_type": "cookie",
  "expires_in": "有效期(秒),默认7200",
  "refresh_token": "如果access_token过期,需要用此值更新",
  "created_at": "Token创建时间戳(秒)"
}

微信小程序登录

本服务支持微信小程序对接登录,基本沿用OAuth2能力,所以要提前注册一个OAuth2应用信息,如果没有创建的直接联系忠哥创建,并通过开发者服务器对接SSO服务,具体业务逻辑看下图

微信小程序登录.png

  • 请提前通过wx.login()拿到code,
  • 通过 https://api.weixin.qq.com/sns/jscode2session?appid=[小程序ID]&js_code=[上一步获取的code]&grant_type=authorization_code 获取session_keyopenid/unionid
  • 通过open-type="getPhoneNumber"按钮授权获取手机号,这个接口会返回encrypt_dataiv

获取授权Code

加密数据由以下几个字段使用Json Web Token格式加密出来,算法固定为HS256,签名的secret为颁发的client_secret

  • unionid 微信开放接口获取
  • app_id 小程序ID
  • op_id 微信开放平台App ID
  • state 状态码
  • iv 小程序API获取
  • encrypt_data 小程序API获取
  • session_key 微信jscode2sessionAPI获取
  • exp 过期时间戳,通常取当前之间加60秒
  • 请求

GET https://sso.uino.com/oauth/authorize_wx_mini?response_type=code&client_id=[CLIENT _ID]&redirect_uri=[授权的callback_url]&state=[状态码]&unionid=[小程序API获取]&op_id=[开放平台ID]&app_id=[小程序ID]&encrypt=[加密数据]&session_key=[微信开放接口获取]

Headers

字段备注
User-Agentxxx (Internal App)XX为应用名称,必须
  • 响应
< HTTP/1.1 302 Found
< content-length: 0
< location: [你的redirect_uri]?code=[授权code]&state=[请求时带过来的状态码]
< date: Wed, 25 Aug 2021 03:44:58 GMT

获取AccessToken

跟以上OAuth2 > 获取AccessToken类同

微信公众号登录

本服务支持微信公众号对接登录,基本沿用OAuth2能力,所以要提前注册一个OAuth2应用信息,如果没有创建的直接联系管理员创建,并通过开发者服务器对接SSO服务,业务逻辑图与上面的小程序登录流程图相近。

  • 请提前准备好有效的二维码ticket

获取授权Code

加密数据由以下几个字段使用Json Web Token格式加密出来,算法固定为HS256,签名的secret为颁发的client_secret

  • unionid 微信开放接口获取
  • app_id 公众号ID
  • op_id 微信开放平台App ID
  • state 状态码
  • ticket 小程序二维码票据,可以通过微信接口获取
  • exp 过期时间戳,通常取当前之间加60秒
  • 请求

GET https://sso.uino.com/oauth/authorize_wx_oa?response_type=code&client_id=[CLIENT _ID]&redirect_uri=[授权的callback_url]&state=[状态码]&unionid=[公众号API获取]&op_id=[开放平台ID]&app_id=[公众号ID]&encrypt=[加密数据]&ticket=[二维码票据]

  • 响应
< HTTP/1.1 302 Found
< content-length: 0
< location: [你的redirect_uri]?code=[授权code]&state=[请求时带过来的状态码]
< date: Wed, 25 Aug 2021 03:44:58 GMT

获取AccessToken

跟以上OAuth2 > 获取AccessToken类同

Java SDK使用

pom中增加依赖

<dependency>
    <groupId>com.uino.sso</groupId>
    <artifactId>uino-sso-client</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

Spring Boot配置文件中增加配置项

# OAUTH2认证信息
uino_sso.internalAppUserAgent = XXX (Internal App)
uino_sso.client_id = flnHWNmwwznVJZ8KZdfKYlLbblUgsqOJxsXSPn3vYwd1Py6KRfZpGlu8btQD7WiC
uino_sso.client_secret = RZkFYeawf1BrMItpMI4ooF0xlHXKlORuDMNyW7BppiuCZgCNCV73AmFIfcgIDI1l
uino_sso.callback_url = http://localhost:8080/helloworld
# 静态资源类请求和涉及到destroyToken的请求,不走本Filter(';'分隔)
uino_sso.ignore_urls = /logout;.jpg;.png;.html;.js

编写IUinoSSOService实现类

继承com.uino.sso.client.UinoSSOService,需要实现5个方法。

protected void addState(String state):记录重定向到Uino SSO前的随机码票据,用于Uino SSO回调回来时的验证。

protected boolean containState(String state):Uino SSO回调回来时,验证票据是否合法。

public void putAccessToken(UinoSSOUser user, UinoSSOToken accessToken):向缓存中存储已登录用户的accessToken对象。

public void removeAccessToken(UinoSSOUser user):从缓存中删除已销毁accessToken的用户。

public UinoSSOToken getAccessToken(UinoSSOUser user):从缓存中按用户获取对应的accessToken。

如果是单机应用,可以使用如下实现类;如果是集群应用,本地缓存应换成外部统一的缓存。

package com.uino.sso.demo;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.stereotype.Service;

import com.uino.sso.client.UinoSSOService;
import com.uino.sso.client.UinoSSOToken;
import com.uino.sso.client.UinoSSOUser;

@Service
public class MyService extends UinoSSOService {

	private Set<String> stateSet = new HashSet<String>();
	private ConcurrentHashMap<Integer, UinoSSOToken> accessTokenMap = new ConcurrentHashMap<Integer, UinoSSOToken>();
	
	@Override
	public void putAccessToken(UinoSSOUser user, UinoSSOToken accessToken) {
		accessTokenMap.put(user.getUser_id(), accessToken);
	}
	
	@Override
	public void removeAccessToken(UinoSSOUser user) {
		accessTokenMap.remove(user.getUser_id());
	}

	@Override
	public UinoSSOToken getAccessToken(UinoSSOUser user) {
		return accessTokenMap.get(user.getUser_id());
	}

	@Override
	protected boolean containState(String state) {
		boolean contain = stateSet.contains(state);
		stateSet.remove(state);
		return contain;
	}

	@Override
	protected void addState(String state) {
		stateSet.add(state);
	}

}

编写Filter过滤器

继承com.uino.sso.client.UinoSSOFilter。加@WebFilter注解。当请求发生在本accessToken有效期的80%后到有效期前时,本过滤器会自动续期。

package com.uino.sso.demo;

import javax.servlet.annotation.WebFilter;

import com.uino.sso.client.UinoSSOFilter;

@WebFilter
public class MyFilter extends UinoSSOFilter {
	
}

SpringApplication启动类注解

扫包注入上述IUinoSSOService实现类和Filter过滤器。

package com.uino.sso.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan({"com.uino.sso.demo"})
@SpringBootApplication
@ServletComponentScan(basePackages = "com.uino.sso.demo")
public class Main {

	public static void main(String[] args) throws Exception {
		SpringApplication.run(Main.class, args);
	}

}

UinoSSOService提供的方法

public UinoSSOUser getUser(HttpServletRequest request):通过请求获取Uino SSO 用户。

public void destroyAccessToken(HttpServletRequest request):通过请求销毁accessToken。