#SSO接入规范
###1.服务地址 服务地址:http://ip:port/sso
###2.接口规范 ####2.1使用SSO默认登录视图单点登录 地址: /api/login
功能说明:调用SSO中心服务的登录功能,进行登录鉴权和获取访问SSO注册应用服务的ticket
请求方式:request请求重定向(即应用服务将用户请求重定向到SSO中心服务)
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
appId | String | 是 | SSO中心服务为应用颁发的应用编号 |
service | String | 是 | 用户原始请求的URL,通常为应用服务中受保护资源的地址 |
返回说明:
用户在SSO登录成功后,SSO中心服务会颁发SSO中心的授权ticket(Cookie),下发用户访问当前应用服务的加密凭证(SSO_CLIENT_EC)。SSO服务完成用户授权后,会转发用户请求到service指定的资源,并将加密后的用户凭证信息交由应用服务处理,已判断用户的合法性。如下是SSO服务最终生成的请求URL示例:
http://111.9.116.181:20161/app1/sso-app/home?SSO_CLIENT_EC=bGI0NlVzU3d1Q2VZSURSVHZKRFkrTXh5R1phYXJtYmpJT05VajA4UU5FQ1NhZmV2UWY3RDZrWVI0%0AVTZmajhDVGlpeFBNRGxhM1MwbApuNlhhMHJaUE0rZ0xqUjhVbHZGYz9hcHBJZD0xMDAxJmtleUlk%0APTI%3D
返回参数说:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
SSO_CLIENT_EC | String | 是 | SSO颁发的用户访问应用服务的加密凭证,应用服务需解密以判断凭证的合法性。 |
####2.2自定义视图单点登录(跨域登录)
地址:/api/rest/login
功能说明:用于自定义登录视图使用SSO中心服务的单点的登录功能.为避免用户重复输入用户名和密码,应用服务可在在向用户展示登录界面前,带上必要参数探询本接口,已确认用户是否已在SSO中心服务器登录,可在确认未登录时展示登录UI。
请求方式:Http POST
Content-type:x-www-form-urlencoded
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
appId | String | 是 | SSO中心服务为应用颁发的应用编号 |
service | String | 是 | 用户原始请求的URL,通常为应用服务中受保护资源的地址 |
username | String | 否 | 用户名 |
password | String | 否 | 密码 |
成功响应:200 OK
应答报文:
{
"code": "200", //状态码
"msg": "登录成功!", //执行信息
"data": { //返回数据项
"user": { //用户基本信息
"mail": "api-test@proedge.hk",
"name": "api-test",
"realName": "api-test",
"token": "k3CH2XQ4_c5wqFBp_DVyIHk02mtIwSM26QsXB6G88v4",//用户本次访问的token,对应用户中心的token
"uid": "6"
},
"SSO_CLIENT_EC":"dGJDYndPM2x4ZWVrY1hKMkdsY1NtdllhcjhVZUVMelBLWnZSRUZaMUhtZDZzZE9LU1hkV0cvWUZs%0AM2JKNUxGdGlyTXNWRFg3aUtoTQprR1NNOVluYzNLYkMxSTR1V05LZWRuWmVjMlNHK041U1VzS1NM%0ASm4wbjJEMFhLVFc1Z3VHYTFPU1o4YVNkem5pbVNIb1ZYQlVWUmlZCkpxNWs2dkQ1dC9zWjBkSkRm%0Ac2hheU80TUtGanl4K1dFTHg5Q1RZVmdqWGF4REhMZWwvTGFrUTB0VUErMEZham1hN2krbm53ckN0%0AZXgKaG1iK28vbjVPQldhM3NZbzhVcHNwNE9LQ1JtcXBOUTRqb2ltODc4bm1sYzJJT054c1Rqd01p%0AOTB2VHdTUWpNUnEvL3E4QmlXTXZDWApZbW01Rm1KTCtiN0VvTjZya3A0TExkNTFVaXA1TEQ1WW90%0AdHJrT2g3blJkUlpSZVFtSDZNYW5yRHB5U1g3dE82MkgwamdnPT0%2FYXBwSWQ9M2IwZDU1NmQzYWRk%0ANDIwNTk2YTlhZmRkNThmMWJjMTAma2V5SWQ9Mg%3D%3D" //加密凭证
}
}
返回参数说明:
SSO_CLIENT_EC是交由请求参数中service对应的应用服务平台验证的用户的加密凭证。若service对应的不是本应用程序,而是SSO体系中的其他兄弟应用服务,需要在转发请求到service时,带上SSO_CLIENT_EC,形如
service?SSO_CLIENT_EC=xxxxxxxx
若service对应本应用,可不做处理。
异常响应:401 Unauthorized
异常应答报文:
{
"code":错误状态码,
"msgKey":异常信息
}
异常这说明:
code | msgKey | 描述 |
---|---|---|
1001 | user credential is null | 用户凭证为空 |
1010 | username or password is invalid | 用户名或密码错误 |
####2.3获取应用解密秘钥
地址:/api/fetchKey
功能说明:应用服务获取解密私钥
请求方式:Http POST
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
appId | String | 是 | 应用服务的编号 |
返回形式:json
返回参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
keyId | int | 秘钥编号 | |
appId | String | 是 | 应用服务编号 |
value | String | 是 | 私钥值 |
####2.4统一注销(引用统一注销js文件)
地址:/api/logoutJs
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
appId | String | 是 | 应用服务的编号 |
logoutSuccessUrl | String | 是 | 注销后跳转的地址 |
接口使用说明:此接口为视图文件中引入js文件,以实现统一注销之用
HTML获取文件示例:
<script type="application/javascript" src="http://127.0.0.1:8080/sso/api/logoutJs?appId=3b0d556d3add420596a9afdd58f1bc10&logoutSuccessUrl=http://www.baidu.com"></script>
HTML中实现统一注销功能示例:
<a href="javascript:logout()">统一注销</a>
####2.5解密客户端加密凭证
POST /api/rest/decode
功能说明:应用服务平台解密SSO_CLIENT_EC获取用户凭证的明文
Content-type:application/json
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
clientEC | String | 是 | SSO_CLIENT_EC |
####2.6发送短信验证码
地址:/api/smsGateway/sendSmsCode
功能说明:用于发送短信验证码
请求方式:Http POST
Content-type:application/json
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
appId | String | 是 | SSO中心服务为应用颁发的应用编号 |
phoneNum | String | 是 | 手机号 |
templateCode | int | 是 | 短信模板id(值1:注册模板;值2:修改密码模板) |
请求报文:
{
"appId":"62cbc1********2fgg45vhh6",
"phoneNum":"15892645809",
"templateCode":1
}
成功响应:200 OK
应答报文:
{
"code": "000000", //状态码
"msg": "短信发送成功" //执行信息
}
####2.7短信验证码校验
地址:/api/smsGateway/validateSmsCode
功能说明:校验短信验证码
请求方式:Http POST
Content-type:application/json
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
appId | String | 是 | SSO中心服务为应用颁发的应用编号 |
phoneNum | String | 是 | 手机号 |
templateCode | int | 是 | 短信模板id(值1:注册模板;值2:修改密码模板) |
smsCode | String | 是 | 短信验证码 |
请求报文:
{
"appId":"62cbc1********2fgg45vhh6",
"phoneNum":"15892645809",
"templateCode":1,
"smsCode":"1234"
}
成功响应:200 OK
应答报文:
{
"code": "000000", //状态码
"msg": "验证码验证通过" //执行信息
}
####2.8短信发送重要事件通知
地址:/api/smsGateway/sendSmsCode
功能说明:发送重要事件通知
请求方式:Http POST
Content-type:application/json
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
appId | String | 是 | SSO中心服务为应用颁发的应用编号 |
phoneNum | String | 是 | 手机号 |
templateCode | int | 是 | 短信模板id(固定值:3) |
event | String | 是 | 短信通知内容 |
请求报文:
{
"appId":"62cbc1********2fgg45vhh6",
"phoneNum":"15892645809",
"templateCode":3,
"event":"重要事件通知"
}
成功响应:200 OK
应答报文:
{
"code": "000000", //状态码
"msg": "短信发送成功" //执行信息
}
####2.9注册
地址:/api/member/add
功能说明:注册
请求方式:Http POST
Content-Type: application/x-www-form-urlencoded
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
username | String | 是 | 用户名仅限手机号 |
password | String | 是 | 密码(大于6位,小于32位) |
memberType | int | 是 | 用户类型(0:个人用户,1:企业用户,2:保留不用,3:行政机构,4:第三方服务商,5:投资人) |
smsCode | String | 是 | 短信验证码 |
service | String | 否 | 注册成功跳转地址 |
请求报文:
{
"username":"15892645809",
"password":"********",
"memberType":1,
"smsCode":"1234"
}
成功响应:200 OK
应答报文:
{
"code": "000000", //状态码
"msg": "添加用户成功",
"content":{
"service":"http://www.wfyizhan.com/comet/kcytp/index.php?s=/home/base/ses.html"
}
}
####2.10修改密码
地址:/api/member/modifyPassword
功能说明:修改密码
请求方式:Http POST
Content-Type: application/x-www-form-urlencoded
请求参数说明:
参数名称 | 参数类型 | 是否必须 | 描述 |
---|---|---|---|
username | String | 是 | 用户名仅限手机号 |
password | String | 是 | 密码(大于6位,小于32位) |
smsCode | String | 是 | 短信验证码 |
service | String | 是 | 修改密码成功跳转地址 |
请求报文:
{
"username":"15892645809",
"password":"********",
"smsCode":"1234",
"service":"http://******/comet/kcytp/index.php?s=/home/base/ses.html"
}
成功响应:200 OK
应答报文:
{
"code": "000000", //状态码
"msg": "修改密码成功",
"content":{
"service":"http://******/comet/kcytp/index.php?s=/home/base/ses.html"
}
}
####3.1对应用服务的要求
- 应用服务需实现登录拦截
- 应用服务与用户间的会话状态用Session实现
- 应用服务平台需提供客户端(浏览器)注销退出,操作当前Session失效的API
####3.2单点登录集成(需要使用SSO登录界面) 下面将通过J2EE应用服务的集成单点登录的实例阐述应用服务平台集成单点登录时需要实现的操作。
一下是实现登录拦截过滤器的核心代码:SSOClientFilter.java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpSession session = httpServletRequest.getSession();
//监测是否在本地应用登录
//若未在本应用登录,则开始SSO登录流程
if (session.getAttribute(USER_STAT_IN_SESSION_KEY) == null) {
//查找参数中是否存在SSO_CLIENT_EC,若不存在,则重定向到登录页
//SO_CLIENT_EC是SSO服务器回调时传给应用服务的参数,http:url?SO_CLIENT_EC=xxxxx
String ssoClientEC = request.getParameter("SO_CLIENT_EC");
if (StringUtils.isEmpty(ssoClientEC)) {
//若为获取到加密的身份信息则重定向请求到SSO服务器
httpServletResponse.sendRedirect(buildRedirectToSSOServer(httpServletRequest));
return;
}
//从SSO服务获取当前应用服务的秘钥
if (ssoKey == null) {
ssoKey = keyService.findByAppId(应用服务的AppId);
}
//用获取到的私钥解密凭证
EncryCredentialInfo encryCredentialInfo = this.encryCredentialManager.decrypt(new EncryCredential(ssoClientEC));
if (encryCredentialInfo != null) {
//检验凭证的合法性
boolean valid = this.encryCredentialManager.checkEncryCredentialInfo(encryCredentialInfo);
if (valid) {
//如果合法,建立与本应用服务的session会话
session.setAttribute(USER_STAT_IN_SESSION_KEY, encryCredentialInfo);
if (appClientLoginHandler != null) {
appClientLoginHandler.loginClient(encryCredentialInfo, httpServletRequest, httpServletResponse);
}
//重新请求原始请求
String url = httpServletRequest.getRequestURL().toString();
if (!StringUtils.isEmpty(url)) {
//如果请求中存在EC参数,则去除这个参数,重定位。
if (url.contains(WebConstants.SSO_CLIENT_COOKIE_KEY)) {
//TODO 去除请求URL中的EC参数
}
}
//重新定位请求,避免尾部出现长参数。
httpServletResponse.sendRedirect(url);
return;
}
}
httpServletResponse.sendRedirect(buildRedirectToSSOServer(httpServletRequest));
} else {
//若已登录,则接续其他过滤器链
chain.doFilter(request, response);
}
}
public boolean checkEncryCredentialInfo(EncryCredentialInfo encryCredentialInfo) {
if (encryCredentialInfo != null) {
//无凭据对应的用户标识,则无效。
if (StringUtils.isEmpty(encryCredentialInfo.getUserId())) {
return false;
}
Date now = new Date();
if (encryCredentialInfo.getExpiredTime() != null) {
//将未来过期时间减去当前时间。
long deta = encryCredentialInfo.getExpiredTime().getTime() - now.getTime();
//若差值大于0,表示过期时间还没有到,凭据继续可以有效使用。
if (deta > 0) {
return true;
}
}
}
return false;
}
public EncryCredentialInfo decrypt(EncryCredential encryCredential) throws InvalidEncryededentialException {
//判空
if (encryCredential != null && !StringUtils.isEmpty(encryCredential.getCredential())) {
String credential = encryCredential.getCredential();
return parseEncryCredential(credential);
}
return null;
}
private EncryCredentialInfo parseEncryCredential(String credential) throws InvalidEncryededentialException {
EncryCredentialInfo encryCredentialInfo = new EncryCredentialInfo();
try {
//进行Base64URLSafe解码
credential=new String(StringUtil.decodeBase64URLSafeString(credential),ENCODE);
//问号分割字符串
String[] items = credential.split("\\?");
//如果长度为2,即是敏感信息和不敏感信息拼接的结果
if (items.length == 2) {
//第二个字符串不为空,先解析第二个字符串
if (!StringUtils.isEmpty(items[1])) {
//使用&分割
String[] params = items[1].split("&");
for (String param : params) {
//使用=分割
String[] values = param.split("=");
if (values != null && values.length == 2) {
if ("appId".equals(values[0])) {
encryCredentialInfo.setAppId(values[1]);
} else if ("keyId".equals(values[0])) {
encryCredentialInfo.setKeyId(values[1]);
}
}
}
} else {
throw InvalidEncryededentialException.INSTANCE;
}
//第一个字符串不为空
if (!StringUtils.isEmpty(items[0])) {
//解码敏感信息
//先进行Base64URLSafe解码
byte[] data = StringUtil.decodeBase64URLSafeString(items[0]);
//查询秘钥
SSOKey ssoKey = keyService.findByKeyId(encryCredentialInfo.getKeyId());
if (ssoKey != null) {
//使用秘钥进行解密
byte[] origin = DESCoder.decrypt(data, ssoKey.toSecurityKey());
//将byte数组转化成字符串
String jsonStr = new String(origin);
Map<String, Object> map = (Map<String, Object>) JSON.parse(jsonStr);
if (map != null) {
Object userId = map.get("username");
Object createTime = map.get("createTime");
Object expireTime = map.get("expireTime");
Object token = map.get("token");
Object user = map.get("user");
encryCredentialInfo.setUserId(userId == null ? null : userId.toString());
encryCredentialInfo.setCreateTime(createTime == null ? null : new Date(Long.parseLong(createTime.toString())));
encryCredentialInfo.setExpiredTime(expireTime == null ? null : new Date(Long.parseLong(expireTime.toString())));
encryCredentialInfo.setUser(user == null ? null : JSON.parseObject(user.toString(),User.class));
encryCredentialInfo.setToken(token == null ? null : token.toString());
}
}
} else {
throw InvalidEncryededentialException.INSTANCE;
}
} else {
throw InvalidEncryededentialException.INSTANCE;
}
} catch (UnsupportedEncodingException e) {
logger.error("parse encry credential exception:{}", e);
e.printStackTrace();
throw InvalidEncryededentialException.INSTANCE;
} catch (Exception e) {
logger.error("parse encry credential exception:{}", e);
e.printStackTrace();
throw InvalidEncryededentialException.INSTANCE;
}
return encryCredentialInfo;
}
public static byte[] decodeBase64URLSafeString(String data) throws UnsupportedEncodingException {
if (data.contains("-")) {
data = data.replaceAll("-", "+");
}
if (data.contains("_")) {
data = data.replaceAll("_", "/");
}
return Base64.decodeBase64(data);
}
package com.changhong.sso.common;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import java.security.Key;
import java.security.SecureRandom;
public class DESCoder {
/**
* 密钥算法
*/
private static final String KEY_ALGORITHM = "DESede";
private static final String DEFAULT_CIPHER_ALGORITHM = "DESede/ECB/ISO10126Padding";
private static final int KEY_SIZE = 168;
/**
* 初始化密钥
*
* @return byte[] 密钥
* @throws Exception
*/
public static Key initSecretKey(String seed) throws Exception{
//返回生成指定算法的秘密密钥的 KeyGenerator 对象
KeyGenerator keyPairGen = KeyGenerator.getInstance(KEY_ALGORITHM);
//初始化此密钥生成器,使其具有确定的密钥大小
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(seed.getBytes());
keyPairGen.init(KEY_SIZE, secureRandom);
//生成一个密钥
SecretKey secretKey = keyPairGen.generateKey();
//实例化DES密钥规则
DESedeKeySpec dks = new DESedeKeySpec(secretKey.getEncoded());
//实例化密钥工厂
SecretKeyFactory skf = SecretKeyFactory.getInstance(KEY_ALGORITHM);
//生成密钥
return skf.generateSecret(dks);
}
/**
* 加密
*
* @param data 待加密数据
* @param key 二进制密钥
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data,Key key) throws Exception{
return encrypt(data, key,DEFAULT_CIPHER_ALGORITHM);
}
/**
* 加密
*
* @param data 待加密数据
* @param key 密钥
* @param cipherAlgorithm 加密算法/工作模式/填充方式
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data,Key key,String cipherAlgorithm) throws Exception{
//实例化
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
//使用密钥初始化,设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE, key);
//执行操作
return cipher.doFinal(data);
}
/**
* 解密
*
* @param data 待解密数据
* @param key 二进制密钥
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data,Key key) throws Exception{
return decrypt(data, key,DEFAULT_CIPHER_ALGORITHM);
}
/**
* 解密
*
* @param data 待解密数据
* @param key 密钥
* @param cipherAlgorithm 加密算法/工作模式/填充方式
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data,Key key,String cipherAlgorithm) throws Exception{
//实例化
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
//使用密钥初始化,设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, key);
//执行操作
return cipher.doFinal(data);
}
}
####3.3统一注销集成
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpSession session = httpServletRequest.getSession();
//本地应用未登出,则进行登出处理
try {
if (session != null && session.getAttribute(SSOClientFilter.USER_STAT_IN_SESSION_KEY) != null) {
//清除session
if (session.getAttribute(SSOClientFilter.USER_STAT_IN_SESSION_KEY) != null) {
session.setAttribute(SSOClientFilter.USER_STAT_IN_SESSION_KEY, null);
}
//TODO 其他处理
//设置session过期
session.setMaxInactiveInterval(0);
}
} catch (Exception e) {
//响应登录结果。
sendError(httpServletResponse);
}
//相应登录结果
sendResponse(httpServletResponse);
}
//执行成功后的返回格式必须是{result:true},以实现前端jsonp统一退出用户已登录的全部应用服务
private void sendResponse(HttpServletResponse response) {
response.setContentType("text/javascript;charset=utf-8");
response.setHeader("Cache-Control", "no-cache");
PrintWriter outhtml;
try {
outhtml = response.getWriter();
outhtml.print("{result:true}");
outhtml.close();
} catch (IOException e) {
logger.error("send sendResponse error", e);
}
}
private void sendError(HttpServletResponse response) {
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (IOException e) {
logger.error("send response error :{}", e);
}
}
//引入SSO提供的统一注销js API
<script type="application/javascript" src="http://111.9.116.181:20160/sso/api/logoutJs?appId=xxxx&logoutSuccessUrl=xxxx"></script>
//DOM中调用统一注销方法
<a href="javascript:logout()">统一注销</a>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。