SSO实践

OK,正式说明了

SSO的说明网上有很多我就不在这儿丢人了。找了张小图SSO的作用一目了然。

以下主要记录一下我在产品中SSO的实践案例。

案例1

案例1 是比较标准的基于OpenID方式的SSO,用Node.js写的。

案例1没什么说的,网上样例很多,如果有兴趣可以看下我之前写的,不过比较老了,也是第一次写nodejs。

案例2

则是非标的SSO,用Java+javascript写的。

案例2虽然不是非标的,不过整体流程是具备的,比较适用特定编码场景(Spring Security+OpenID),可能有需要的同学,反正我是没在网上找到这类案例。

客户现场的系统A需要登入到我们提供的系统B,没有单独用户中心即也不存在用户同步,客户要求的是能无缝登入,所以解决办法有用户则直接登入无用户则创建后再登入,登录效果与从登录页面发起的登录一样,所以token解析后用Security的方式执行登录。

前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* sso出现在路径末尾 react router方式 目前采用这种方式 http://.../frame/#/module/xxx?sso=xxx 避免sso一直保留
* @param key 需要获取url参数key
* @returns {string|null}
*/
export function getSsoString(key) {
const str = location.hash;
if (str == null || str.length < 2) {
return null;
}
const arr = str.split('?');
if (arr != null && arr.length === 2) {
const query = arr[1];
if (query != null && query.length > 0) {
const words = query.split('&');
// 将每一个数组元素以=分隔并赋给obj对象
for (let i = 0; i < words.length; i++) {
const tmp_arr = words[i].split('=');
const k = decodeURIComponent(tmp_arr[0]);
const v = decodeURIComponent(tmp_arr[1]);
if (k === key) {
return v;
}
}
}
}
return null;
}

/**
* 单点登录逻辑 在页面token发送到后端进行验证
* @param callback
*/
export function sso(callback) {
const token = getSsoString('sso');
if (token != null) {
req(BASE_WEB_API.SSO, { token }, null, { validateError: true })
.then(response => {
// do something....
if (callback != null) {
callback();
}
})
.catch(e => {
console.error('failed sso --> ', e);
if (callback != null) {
callback();
}
});
} else if (callback != null) {
callback();
}
}

后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* 跳转到猎豹系统
*
* @param response
* @throws Exception
*/
@PostMapping(value = "/cheetah", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String cheetah(@RequestBody SSOVO ssovo,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
try {
// 验证license
if (!licenseService.isValid()) {
LOGGER.error("license is invalid");
return validateTokenError(request, LICENSE_ERROR_MSG);
}
//解析token
Context.Token userToken = Context.getUserInfoFromToken(ssovo.getToken());
if (isNullOrEmpty(userToken.getUserName()) || isNullOrEmpty(userToken.getPassword())) {
LOGGER.warn("token is invalid:{}", ssovo.getToken());
return validateTokenError(request);
}
LOGGER.info("当前单点登录的用户信息为:{}", JSON.toJSONString(userToken));
//验证内置用户是否存在,不存在则创建
SSOUserVO user = ssoService.checkUser(userToken.getUserName(), Context.getCmsContext());
if (user != null) {
// 执行登录
user.setPassword(userToken.getPassword());
return ssoLogin(request, response, user);
}
//异常时跳转到登录页
return validateTokenError(request);
} catch (Exception e) {
LOGGER.error("sso登录失败:{}", e.getMessage());
return validateTokenError(request);
}
}

private String validateTokenError(HttpServletRequest request) {
return validateError(request, SSO_VERIFICATION_ERROR_MSG);
}

private String validateTokenError(HttpServletRequest request, String msg) {
return validateError(request, msg);
}

private String validateError(HttpServletRequest request, String msg) {
HttpSession session = request.getSession();
if (session != null) {
//使session失效
session.invalidate();
}
SSOErrorVO errorVo = new SSOErrorVO(SSO_VERIFICATION_ERROR, msg);
return JSON.toJSONString(errorVo);
}
/**
* 执行登录
*
* @param request
* @param response
* @param userToken
* @return
* @throws IOException
* @throws ServletException
*/
private String ssoLogin(HttpServletRequest request, HttpServletResponse response, SSOUserVO userToken) throws IOException, ServletException {
try {
//登录
UsernamePasswordAuthenticationToken authReq
= new UsernamePasswordAuthenticationToken(userToken.getUserName(), userToken.getPassword());
authReq.setDetails(new WebAuthenticationDetails(request));
Authentication auth = authenticationManagerBean.authenticate(authReq);
SecurityContextHolder.getContext().setAuthentication(auth);
HttpSession session = request.getSession(true);
// 永不超时
session.setMaxInactiveInterval(-1);
//TODO 静态导入
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
baymaxLoginSuccessHandler.onAuthenticationSuccess(request, response, auth);
} catch (AuthenticationException failed) {
LOGGER.warn(
"sso: InternalAuthenticationServiceException occurred while trying to authenticate the user.",
failed);
SecurityContextHolder.clearContext();
baymaxAuthenticationFailureHandler.onAuthenticationFailure(request, response, failed);
validateTokenError(request);
}

return null;
}

/**
* 根据用户名,获取用户的token
*
* @param userName
* @param response
* @return
*/
@RequestMapping(value = "/getToken/{userName}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String getToken(@PathVariable(value = "userName", required = false) String userName, HttpServletResponse response) {

try {
return Context.createToken(userName, PasswordUtil.getPlaintextPwd());
} catch (Exception e) {
LOGGER.error("获取token失败:{}", e.getMessage());
formatErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
return null;
}
}

private void formatErrorResponse(HttpServletResponse response, int httpCode, String errorMsg) {
response.setStatus(httpCode);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try (PrintWriter out = response.getWriter();) {
String errorMsgVo = JSON.toJSONString(ImmutableMap.of("code", SSO_GET_TOKEN_ERROR, "message", errorMsg));
out.write(errorMsgVo);
out.flush();
} catch (IOException ex) {
LOGGER.warn("get token :{}", ex.getMessage());
}
}

处理400异常避免出现白页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author Gamehu
* @description 接管400异常,个性化错误提示
* @date 2019/12/19
*/
@RestControllerAdvice(assignableTypes = SSOController.class)
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
@Component
public class SSO400ExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object defaultErrorHandler(Exception e) {
log.warn("---SSO验证异常--- ERROR: {}", e.getMessage());
return ImmutableMap.of("code", SSO_VERIFICATION_ERROR, "message", e.getMessage());
}
}

引伸阅读: