900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > SSO单点登录Java实现实例

SSO单点登录Java实现实例

时间:2018-10-24 22:27:10

相关推荐

SSO单点登录Java实现实例

随着公司系统的增加,每次新建一个项目是否还在做登录功能呢,还在做重复的工作?统一登录SSO你值得拥有。本文主要讲解,基于令牌token方式实现,SpringBoot工程下的SSO单点登录整合代码实例Demo,文末附源码地址。

1.环境准备

SSO认证中心服务( )客户端1()客户端2()

由于是Demo实例,这里若有要访问-需要修改一下本机host,添加如下映射

127.0.0.1 127.0.0.1 127.0.0.1

2.搭建SSO认证中心服务

问题来了,搭建一个SSO统一登录需要哪些功能?

统一登录页+接口校验令牌有效性接口(调用来源:子系统)校验登录状态接口(调用来源:子系统)统一退出(调用来源:子系统或退出认证中心服务)

这里就不贴Maven依赖了,主要讲解功能,详见文末附源码。

2.1统一登录页

创建一个统一登录页

<!DOCTYPE html><html><head><meta charset="UTF-8"><title>程序员小强-SSODemo</title></head><style>body.center {text-align: center;}</style><body class="center"><div><h1>SSO用户统一登录</h1></div><div><form name="loginForm" action="/sso/login" method="POST" accept-charset="UTF-8"><div><input placeholder="用户名" value="admin" name="username" type="text"/></div><div><input placeholder="密码" value="123456" name="password" type="password"/></div><div style="color: red"><span th:text="${msg}"></span></div><div><input name="redirectUrl" type="hidden" th:value="${redirectUrl}"/></div><input type="submit" value="登录"/><input type="reset" value="重置"/></form></div></body></html>

效果图

2.2统一登录接口

这里为了简化示例,未引入redis等缓存数据库,使用的 session存储登录信息

/*** 认证中心SSO统一登录方法*/@RequestMapping("/login")public String login(LoginParam loginParam, RedirectAttributes redirectAttributes,HttpSession session, Model model) {//Demo 项目此处模拟数据库账密校验if (!"admin".equals(loginParam.getUsername()) || !"123456".equals(loginParam.getPassword())) {model.addAttribute("msg", "账户或密码错误,请重新登录!");model.addAttribute("redirectUrl", loginParam.getRedirectUrl());return "login";}//登录成功//创建令牌String ssoToken = UUID.randomUUID().toString();//把令牌放到全局会话中session.setAttribute("ssoToken", ssoToken);//设置session失效时间-单位秒session.setMaxInactiveInterval(3600);//将有效的令牌-放到map容器中(存在该容器中的token都是合法的,正式环境建议redis或存库)SSOConstantPool.TOKEN_POOL.add(ssoToken);//未携带重定向跳转地址-默认跳转到认证中心首页if (StringUtils.isEmpty(loginParam.getRedirectUrl())) {return "index";}// 携带令牌到客户端redirectAttributes.addAttribute("ssoToken", ssoToken);log.info("[ SSO登录 ] login success ssoToken:{} , sessionId:{}", ssoToken, session.getId());// 跳转到客户端return "redirect:" + loginParam.getRedirectUrl();}

2.3校验令牌接口

在跳转到子系统后会携带令牌token,这时候需要调用以下接口来校验token的有效性

以下接口一共做了2件事情

校验token是否有效若有效-则记录了注册上来的子系统信息,用于统一注销时候使用

/*** 校验令牌是否合法** @param ssoToken 令牌* @param loginOutUrl 退出登录访问地址* @param jsessionid* @return 令牌是否有效*/@ResponseBody@RequestMapping("/checkToken")public String verify(String ssoToken, String loginOutUrl, String jsessionid) {// 判断token是否存在map容器中,如果存在则代表合法boolean isVerify = SSOConstantPool.TOKEN_POOL.contains(ssoToken);if (!isVerify) {log.info("[ SSO-令牌校验 ] checkToken 令牌已失效 ssoToken:{}", ssoToken);return "false";}//把客户端的登出地址记录起来,后面注销的时候需要根据使用(生产环境建议存redis或库)List<ClientRegisterModel> clientInfoList =puteIfAbsent(ssoToken, k -> new ArrayList<>());ClientRegisterModel vo = new ClientRegisterModel();vo.setLoginOutUrl(loginOutUrl);vo.setJsessionid(jsessionid);clientInfoList.add(vo);log.info("[ SSO-令牌校验 ] checkToken success ssoToken:{} , clientInfoList:{}", ssoToken, clientInfoList);return "true";}

2.4校验登录状态接口

当令牌校验返回失败时,子系统需要调用此接口

/*** 校验是否已经登录认证中心(是否有全局会话)* 1.若存在则携带令牌ssoToken跳转至目标页面* 2.若不存在则跳转到登录页面*/@RequestMapping("/checkLogin")public String checkLogin(String redirectUrl, RedirectAttributes redirectAttributes,Model model, HttpServletRequest request) {//从认证中心-session中判断是否已经登录过(判断是否有全局会话)Object ssoToken = request.getSession().getAttribute("ssoToken");// ssoToken为空 - 没有全局回话if (StringUtils.isEmpty(ssoToken)) {log.info("[ SSO-登录校验 ] checkLogin fail 没有全局回话 ssoToken:{}", ssoToken);//登录成功需要跳转的地址继续传递model.addAttribute("redirectUrl", redirectUrl);//跳转到统一登录页面return "login";}log.info("[ SSO-登录校验 ] checkLogin success 有全局回话 ssoToken:{}", ssoToken);//重定向参数拼接(将会在url中拼接)redirectAttributes.addAttribute("ssoToken", ssoToken);//重定向到目标系统return "redirect:" + redirectUrl;}

2.5统一退出接口

/*** 统一注销* 1.注销全局会话* 2.通过监听全局会话session时效性,向已经注册的所有子系统发起注销请求*/@RequestMapping("/logOut")public String logOut(HttpServletRequest request) {HttpSession session = request.getSession();log.info("[ SSO-统一退出 ] ....start.... sessionId:{}", session.getId());//注销全局会话, SSOSessionListener 监听器会处理后续操作request.getSession().invalidate();log.info("[ SSO-统一退出 ] ....end.... sessionId:{}", session.getId());return "logout";}

退出监听,当统一认证中心session销毁时,同时注销子系统

/*** session监听器** @author 程序员小强*/@Slf4j@WebListenerpublic class SSOSessionListener implements HttpSessionListener {/*** 销毁事件监听* <p>* 1.session超时的时候会调用* 2.手动调用session.invalidate()方法时会调用.** @param se*/@Overridepublic void sessionDestroyed(HttpSessionEvent se) {HttpSession session = se.getSession();String token = (String) session.getAttribute("ssoToken");log.debug("[ SSOSessionListener ] ...start..... sessionId:{},token:{}", session.getId(), token);//注销全局会话,SSOSessionListener监听类删除对应的信息session.invalidate();if (StringUtils.isEmpty(token)) {log.debug("[ SSOSessionListener ] token is null sessionId:{}", session.getId());return;}//清除存储的有效token数据SSOConstantPool.TOKEN_POOL.remove(token);//清除并返回已经注册的系统信息List<ClientRegisterModel> clientRegisterList = SSOConstantPool.CLIENT_REGISTER_POOL.remove(token);if (CollectionUtils.isEmpty(clientRegisterList)) {return;}for (ClientRegisterModel client : clientRegisterList) {if (null == client) {continue;}//取出注册的子系统,依次调用子系统的登出方法(通过会话ID退出子系统的局部会话)sendHttpRequest(client.getLoginOutUrl(), client.getJsessionid());log.info("[ SSOSessionListener ] 注销系统 url:{},Jsessionid:{}", client.getLoginOutUrl(), client.getJsessionid());}log.debug("[ SSOSessionListener ] ...end..... sessionId:{},token:{}", session.getId(), token);}/*** 发送退出登录请求* 模拟浏览器访问形式** @param reqUrl发送请求的地址* @param jesssionId 会话Id*/private static void sendHttpRequest(String reqUrl, String jesssionId) {try {//建立URL连接对象URL url = new URL(reqUrl);//创建连接HttpURLConnection conn = (HttpURLConnection) url.openConnection();//设置请求的方式(需要是大写的)conn.setRequestMethod("POST");//设置需要响应结果conn.setDoOutput(true);//通过设置JSESSIONID模拟浏览器端操作conn.addRequestProperty("Cookie", "JSESSIONID=" + jesssionId);//发送请求到服务器conn.connect();conn.getInputStream();conn.disconnect();} catch (Exception e) {log.error("[ sendHttpRequest ] exception >> reqUrl:{}", reqUrl, e);}}}

3.搭建客户端服务

问题来了,搭建一个客户端需要哪些功能?

拦截请求请求认证中心校验令牌有效性 有效则创建局部会话无效则继续请求认证中心登录 注销系统-请求认证中心统一注销

3.1核心请求拦截器实现

@Configurationpublic class WebConfig extends WebMvcConfigurationSupport {/*** 创建拦截器*/@BeanWebInterceptor webInterceptor() {return new WebInterceptor();}/*** 添加拦截器-进行拦截* addPathPatterns 添加拦截* excludePathPatterns 排除拦截**/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(this.webInterceptor()).addPathPatterns("/**").excludePathPatterns("/logOut");super.addInterceptors(registry);}/*** 返回值-编码 UTF-8*/@Beanpublic HttpMessageConverter<String> responseBodyConverter() {return new StringHttpMessageConverter(StandardCharsets.UTF_8);}@Overridepublic void configureContentNegotiation(ContentNegotiationConfigurer configurer) {configurer.favorPathExtension(false);}/*** 资源处理器-资源路径 映射** @param registry*/@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");}}

/*** 创建拦截器-拦截需要安全访问的请求* 方法说明* 1.preHandle():前置处理回调方法,返回true继续执行,返回false中断流程,不会继续调用其它拦截器* 2.postHandle():后置处理回调方法,但在渲染视图之前* 3.afterCompletion():全部后置处理之后,整个请求处理完毕后回调。** @author 程序员小强*/@Slf4jpublic class WebInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {log.info("[ WebInterceptor ] >> preHandle requestUrl:{} ", request.getRequestURI());//判断是否有局部会话HttpSession session = request.getSession();Object isLogin = session.getAttribute("isLogin");if (isLogin != null && (Boolean) isLogin) {log.debug("[ WebInterceptor ] >> 已登录,有局部会话 requestUrl:{}", request.getRequestURI());return true;}//获取令牌ssoTokenString token = SSOClientHelper.getSsoToken(request);//无令牌if (StringUtils.isEmpty(token)) {//认证中心验证是否已经登录(是否存在全局会话)SSOClientHelper.checkLogin(request, response);return true;}//有令牌-则请求认证中心校验令牌是否有效Boolean checkToken = SSOClientHelper.checkToken(token, session.getId());//令牌无效if (!checkToken) {log.debug("[ WebInterceptor ] >> 令牌无效,将跳转认证中心进行认证 requestUrl:{}, token:{}", request.getRequestURI(), token);//认证中心验证是否已经登录(是否存在全局会话)SSOClientHelper.checkLogin(request, response);return true;}//token有效,创建局部会话设置登录状态,并放行session.setAttribute("isLogin", true);//设置session失效时间-单位秒session.setMaxInactiveInterval(1800);//设置本域cookieCookieUtil.setCookie(response, SSOClientHelper.SSOProperty.TOKEN_NAME, token, 1800);log.debug("[ WebInterceptor ] >> 令牌有效,创建局部会话成功 requestUrl:{}, token:{}", request.getRequestURI(), token);return true;}}

4.源码介绍

源码地址:传送门

5.源码实例测试

由于是Demo实例,这里若有要访问-需要修改一下本机host,添加如下映射

127.0.0.1 127.0.0.1 127.0.0.1

添加好本机host映射后-分别启动

sso-server 域名使用内网域名(:8081)

sso-client1 域名使用内网域名(:8082)

sso-client2 域名使用内网域名(:8083)

5.1 客户端1登录

5.1.1在浏览器端访问 :8082

由于没有登录过任何一个系统,也没有全局会话,所以会跳转到统一认证中心进行登录,可以查看到redirectUrl就是我们要访问的:8082

点击登录(账密demo项目写死了,可以直接点击登录)

登录成功后会跳转到redirectUrl地址,并且携带ssoToken,这个时候客户端系统就可以请求认证中心校验ssoToken后创建内部的局部会话。

5.2 客户端2登录

由于客户端1已经登录,也就是说已经存在全局会话了,那么在访问客户端2的时候,其实无需登录的,只需要认证中心将最新的ssoToken携带过来就可以了。

浏览器输入 :8083

可以查看到统一认证中心直接返回了认证token

5.3 统一退出登录

这里退出客户端1,退出完成后查看客户端1 与客户端2的登录状态

这里继续访问:8082,会提示继续登录

访问:8083,会提示继续登录

说明统一退出登录成功了

查看一下监听的日志,也是注销了每一个局部会话

至此SSO统一登录实战讲解完毕。

需要完善的点还很多,比如服务端与客户端交互的时候可以通过加密或者加签方式防止数据被篡改。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。