title: SSO实践 author: Gamehu tags:
{% asset_img florian-olivo-4hbJ-eymZ1o-unsplash.jpg Photo by Florian Olivo on Unsplash %}
SSO的说明网上有很多我就不在这儿丢人了。找了张小图SSO的作用一目了然。
{% asset_img whatis-single_sign_on-h.png https://searchsecurity.techtarget.com/definition/single-sign-on %}
以下主要记录一下我在产品中SSO的实践案例。
案例1 是比较标准的基于OpenID方式的SSO,用Node.js写的。
案例1没什么说的,网上样例很多,如果有兴趣可以看下我之前写的,不过比较老了,也是第一次写nodejs。
{% blockquote OpenID+MongoDB实现的数据交换中心 https://github.com/WebHu/async_data_exchange_center.git %}
{% endblockquote %}
则是非标的SSO,用Java+javascript写的。
案例2虽然不是非标的,不过整体流程是具备的,比较适用特定编码场景(Spring Security+OpenID),可能有需要的同学,反正我是没在网上找到这类案例。
{% asset_img sso.png 场景说明 %}
客户现场的系统A需要登入到我们提供的系统B,没有单独用户中心即也不存在用户同步,客户要求的是能无缝登入,所以解决办法有用户则直接登入无用户则创建后再登入,登录效果与从登录页面发起的登录一样,所以token解析后用Security的方式执行登录。
/**
* 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();
}
}
/**
* 跳转到猎豹系统
*
* @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异常避免出现白页
/**
* @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());
}
}
{% blockquote OpenID versus OAuth from the user’s perspective
http://cakebaker.42dh.com/2008/04/01/openid-versus-oauth-from-the-users-perspective/ %} {% endblockquote %}
{% blockquote OAUTH-OPENID: YOU’RE BARKING UP THE WRONG TREE IF YOU THINK THEY’RE THE SAME THING http://softwareas.com/oauth-openid-youre-barking-up-the-wrong-tree-if-you-think-theyre-the-same-thing/ %} {% endblockquote %}
{% blockquote What's the difference between OpenID and OAuth? https://stackoverflow.com/questions/1087031/whats-the-difference-between-openid-and-oauth %} {% endblockquote %}