实习中接到需求单点登录,记录一下单点登录在企业中的实际应用。其实单点登录在不同的业务中处理细节还是不相同的,我这里仅举例我们这个需求中如何处理的一种情形。
一.概念
简单来说就是一次登录,各处访问。
二.流程
场景一: 用户采取三方登录。
场景一的流程:
- 用户采用三方登录方式,点击访问三方应用的认证方式
- 访问认证服务器,用户登录三方的登录界面,登录完成后,点击授权,此时将会根据我们配置的回调地址,携带授权后生成的code.回调访问
- 随后服务器接收到回调的请求,拿到对应的code,携带这个code再去认证服务器获取token.
- 获取到token后,再向资源服务器获取用户信息。
- 解析返回回来的用户信息,完成登录。
背景:三方的数据已同步到本地,这样才有本地的映射。没有也没有关系,根据具体的业务更改逻辑即可。
具体流程
- 首先通过redirect_uri注册到资源认证服务器。获取到注册在资源服务器的client_id与client_secret.
- 随后访问自己产品的登录界面,配置资源认证服务器的url,对应前端来讲就是一个三方按钮。例如gitee中登录界面中下面的其他软件的登录方式。
- 之后我们将携带一系列的参数去访问这个url, 主要参数为
- client_id: 注册在认证服务器的id
- client_secret: 注册在认证服务器的secret
- redirect_uri: 获取code后的回调地址。
- 用户在三方完成登录后将会根据回调地址redirect_uri,拼接code参数。打回到本地服务器。
- 随后本地服务器将会发送两个http请求。
-
- 一个携带我们获取到的code,去三方服务器获取token
- 拿到token后,发送第二个http请求。获取用户信息。
- 获取到用户信息后,匹配本地的数据库信息,例如userInfo会返回回来一个字段,能够唯一标识一个用户。根据这个标识我们在本地做一个映射,映射到我们本地服务器数据库存储用户信息的一个字段。查询到用户信息。
- 返回用户信息到客户端,完成登录。
我在实现的时候,采取了配置的形式,会在回调地址中拼接一个配置参数。例如sso_code=xxx,利用这个配置我们可以去数据库查询到具体的配置结构。
对应的类
public class Auth2SSOLoginConfig implements Serializable {
/**
* sso服务端名称
*/
private String name;
/**
* sso配置唯一id
*/
private String ssoCode;
/**
* 拼接token参数名
*/
private String tokenKey;
/**
* code参数名
*/
private String codeKey;
/**
* token配置
*/
private HttpContentConfig token;
/**
* userinfo配置
*/
private HttpContentConfig userinfo;
}
HttpContentConfig
@Data
public class HttpContentConfig {
private HttpRequestConfig requestConfig;
private SSOResultConfig ssoResultConfig;
@Data
public static class HttpRequestConfig{
private String url;
private String method;
private Map<String,String> header;
private Map<String,Object> params;
private Map<String,Object> paramsMapping;
}
@Data
public static class SSOResultConfig {
/**
* code 解析的path
*/
private String codePath;
/**
* code 成功值,状态码
*/
private String codeSuccessValue;
/**
* 参数映射
*/
private Map<String,String> valueMapping;
}
}
数据库表结构为
字段名 | 类型 | 描述 |
id | bigint | 自增主键 |
name | varchar(63) | SSO服务端名 |
sso_code | varchar(63) | 配置唯一id |
config | text | 配置内容 |
gmt_create | timestamp | 创建时间 |
gmt_update | timestamp | 更新时间 |
gmt_delete | timestamp | 删除时间 |
create_operator | varchar(255) | 创建人 |
update_operator | varchar(255) | 更新人 |
delete_operator | varchar(255) | 删除人 |
is_delete | boolean | 逻辑删除字段 |
config的结构为一个大Json
{
"token": {
"requestConfig": {
"url": "http://localhost:8080/demo/code",
"method": "POST",
"header": [
{
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "text/html,application/xhtml+xml,application/xml"
}
],
"params": {
"grant_type": "authorization_code",
"client_id": "",
"client_secret": "",
"redirect_uri": "",
"endpoint_uri": ""
}
},
"ssoResultConfig": {
"codeSuccessValue": "",
"codePath": "",
"valueMapping": {
"access_token": "access_token",
"refresh_token": "refresh_token",
"scope": "scope",
"token_type": "token_type",
"expires_in": "expires_in"
},
"paramsMapping": {
"code": "code"
}
}
},
"userInfo": {
"requestConfig": {
"url": "http://localhost:8080/demo/token",
"method": "POST",
"header": [
{
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "text/html,application/xhtml+xml,application/xml"
}
],
"params": {
"client_id": "",
"client_secret": "",
"endpoint_uri": ""
},
"paramsMapping": {
"token": "token"
}
},
"ssoResultConfig": {
"codeSuccessValue": "",
"codePath": "",
"valueMapping": {
"scope": "scope",
"active": "active",
"token_type": "token_type",
"exp": "exp",
"iat": "iat",
"client_id": "client_id",
"account": "username"
}
}
},
"codeKey": "code",
"tokenKey": "token"
}
这里解释一下codeKey,tokenKey与paramsMapping中的code与token的差别与作用。
codeKey是我们去动态解析响应的结果的参数名。这样配置以后获取是不叫code,只需要改配置解析即可。
而tokenKey是场景二的使用,待会再解释。
而paramsMapping中的code与token是我们拼接参数值的参数名。作用也是可以动态配置解析,以后参数名变更我们只需要改配置即可。
场景二的流程:
场景二:
用户已在三方登录完成,访问本地产品页面
- 检验本地session中用户信息是否存在
- 存在直接登录。不存在查询配置
- 获取所有的配置,拿到对应的Map,key为tokenKey,value为token对应的配置
- 查询cookie当中是否存在tokenKey
- 存在直接携带此token与配置获取用户信息
- 不存在直接重定向回登录页面。
这是我们需要去拦截器去完成的一个过程。
这里需要了解一个知识,我们可以通过设置cookie的domain,来保证双方的cookie互通,
比如
A网址 org.example.com
B网址 www.example.com
我们设置domain为example.com这样会把这样设置的cookie共同传递到两个网址。我们再根据我们配置的tokenKey就能够正常拿到对应的tokenValue。然后剩下的流程其实就是我们场景一有了token之后的过程。
然后我们在拦截器大致就是这样一个处理逻辑,如下。
private boolean checkSSOLogin(HttpServletRequest request, HttpServletResponse response) {
Map<String, Auth2SSOLoginConfig> allConfig = ssoAuth2LoginManager.getAllConfig();
if(MapUtils.isEmpty(allConfig)){
return false;
}
Set<String> keySet = allConfig.keySet();
String tokenValue = null;
String tokenKey = null;
for (Cookie cookie : request.getCookies()) {
if (keySet.contains(cookie.getName())) {
tokenKey = cookie.getName();
tokenValue = cookie.getValue();
break;
}
}
if (StringUtils.isNotBlank(tokenValue)) {
UserDTO userDTO = userManager.getUserDTOForTokenAndUserInfo(allConfig.get(tokenKey), tokenValue);
HttpSession session = request.getSession();
//在session域中记录信息,并在SessionId中记录信息
userHelper.writeUserIntoToSession(userDTO, session);
return true;
}
return false;
}