title: SSO实践 author: Gamehu tags: - sso categories: - 工作 date: 2019-12-27 17:49:00 --- {% asset_img florian-olivo-4hbJ-eymZ1o-unsplash.jpg Photo by Florian Olivo on Unsplash %} # OK,正式说明了 SSO的说明网上有很多我就不在这儿丢人了。找了张小图SSO的作用一目了然。 {% asset_img whatis-single_sign_on-h.png https://searchsecurity.techtarget.com/definition/single-sign-on %} 以下主要记录一下我在产品中SSO的实践案例。 ## 案例1 **案例1** 是比较标准的基于OpenID方式的SSO,用Node.js写的。 案例1没什么说的,网上样例很多,如果有兴趣可以看下我之前写的,不过比较老了,也是第一次写nodejs。 {% blockquote OpenID+MongoDB实现的数据交换中心 https://github.com/WebHu/async_data_exchange_center.git %} {% endblockquote %} ### **案例2** 则是非标的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 %}