现象:
web系统早已从久远单系统过度到现在的多系统,面对多系统时难道要用户一个一个的登录,如下图:
系统的负责性由系统自己负责,而不是需要用户负责,无论多复杂的系统的对用户而言就是一个整体,也就是说用户访问web应用群要像单系统一样一次登录,一次注销就ok,如图:
单系统登录的解决方案虽然完美,单系统登录的核心是cookie,cookie会携带会话id在浏览器与web服务器保持会话,但是有一定限制就是cookie域(一般与网站的域名所对应),http访问网站是会携带与这个网站域说匹配的cookie,而不是所有cookie,早期的单点登录就是基于cookie共享,但是可行性不好,首先这种方式要保证群应用的顶级域名一致,其次要保证服务器开发技术要一致不然cookie的key值(tomcat为sessionid)不一致,无法保证会话,共享cookie不支持跨语言的技术平台登录,如java、php、.net,还有cookie本身就是不安全的
所以需要一种更好的解决方案也就是单点登录
名词解释:
单点登录:单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录
相对于单系统登录,sso提供一个认证中心,只有认证中心接受用户名/或密码等安全信息,子系统不提供登录入口,只接受认证中心的间接授权,间接授权使用令牌实现(token),当用户登录某一个子系统,会转到认证中心,认证中心会验证用户名/密码等安全信息,验证通过生成令牌(token),生成的令牌会作为参数发送给各个子系统,子系统根据拿到的令牌(token)访问受保护的资源,当操作子系统其它资源时都会拿这个令牌(token)到认证中心验证,这个过程就是单点登录,如下图:
上图描述:
1、当用户通过浏览器访问系统1,拦截器发现用户并没有登录,跳转到认证中心,并将请求地址作为参数
2、sso认证中心首先会检查是否携带token,携带token跳转到token验证,没有就跳转到认证中心登陆界面
3、认证通过后生产t oken,产生全局会话,并拼接url(加上token)重定向到系统1地址
4、系统1地址会携带token,之后再重新执行token验证方法,验证通过直接跳转到系统1首页
5、访问系统2时,将携带token,此时执行token验证方法,验证通过后放行
以下就一步一步实现单点登录,使用开发工具idea、使用技术springBoot+redis+springSession+jsp实现
1、新建项目使用spring.io提供的模板
2、首先要解决springBoot访问jsp问题,springBoot官方建议使用 theamleaf,如果使用想要使用jsp就要添加如下几个依赖
3、加入spring-redis主键依赖
工程搭建完毕,编码实现,首先是ssoserver(因为是简易版的所以sso-client与sso-server组合在一起)
sso-server:
1、拦截子系统的未等录的请求,跳转至认证中心
2、校验携带的token是否有效
3、拦截子系统注销请求,销毁会话
4、验证用户登陆信息
5、创建令牌
6、创建与子系统间的会话
下面来实现ssoserver吧
ssoserver的工程构成:
1、springSession配置
2、ssoserver作用就是拦截子系统的请求所以使用filter实现:
public class SsoFilter implements Filter { //sso认证 private final String SSO_SERVER_URL = "http://localhost:8080/sso/auth"; //sso token 验证 private final String SSO_VERIFI_URL="http://localhost:8080/sso/verifi"; //sso注销 private final String SSO_LOGINOUT_URL="http://localhost:8080/sso/logout"; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { //检查是否携带token. HttpServletRequest httpServletRequest =(HttpServletRequest) servletRequest; HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; String token = httpServletRequest.getParameter("token"); Mapinfo = new HashMap<>(); //token不为空就验证 if(token != null){ boolean flag = verifi(servletRequest,SSO_VERIFI_URL,token); if (flag){ filterChain.doFilter(servletRequest,servletResponse); return; }else{ info.put("info",token+"无效"); info.put("code","10010"); httpServletResponse.getWriter().write((Json2String(info))); return; } } //如果有效并且已登录就放行到下个过滤器 HttpSession session=httpServletRequest.getSession(); if (session.getAttribute("flag")!=null && (boolean)session.getAttribute("flag") == true){ filterChain.doFilter(servletRequest,servletResponse); return; } //获取注销的标记销毁全局会话 String loginOut = httpServletRequest.getParameter("logout"); if (loginOut == "true"){ httpServletResponse.sendRedirect(SSO_LOGINOUT_URL); } //没有token就跳转到认证中心 //当前请求地址 String callBackUrl = httpServletRequest.getRequestURL().toString(); StringBuilder authUrl = new StringBuilder(); authUrl.append(SSO_SERVER_URL).append("?callBackUrl=").append(callBackUrl); httpServletResponse.sendRedirect(authUrl.toString()); } @Override public void destroy() { } //验证token方法 private boolean verifi(ServletRequest servletRequest,String url,String token){ StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(url).append("?token=").append(token); //封装的ResTemplateUtil工具类发送get请求 String result = RestTemplateUtil.get(servletRequest,stringBuilder.toString(),null); JSONObject jsonObject=JSONObject.parseObject(result); if ( jsonObject.getString("code").equals("200")){ return true; }else{ return false; } } private String Json2String(Object obj){ if (obj == null){ return null; } String jsonString= JSONObject.toJSONString(obj); return jsonString; }
3、restTemplateutil封装,RestTemplate、 ResponseEntity、HttpEntity等用法自行百度:
public class RestTemplateUtil { private static RestTemplate restTemplate = new RestTemplate(); public static String get(ServletRequest request,String url,Mapparams){ ResponseEntity responseEntity = request(request,url,HttpMethod.GET,params); return responseEntity.getBody(); } public static String post(ServletRequest request,String url,Map params){ ResponseEntity responseEntity = request(request,url,HttpMethod.POST,params); return responseEntity.getBody(); } private static ResponseEntity request(ServletRequest request, String url, HttpMethod method,Map parmas){ HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpHeaders httpHeaders = new HttpHeaders(); Enumeration enumerations = httpServletRequest.getHeaderNames(); while (enumerations.hasMoreElements()){ String key =(String)enumerations.nextElement(); String value = httpServletRequest.getHeader(key); httpHeaders.add(key,value); } HttpEntity httpEntity = new HttpEntity (parmas == null?null: JSONObject.toJSONString(parmas),httpHeaders); ResponseEntity res = restTemplate.exchange(url,method,httpEntity,String.class); return res; } }
4、认证与验证中心:
public class AuthController { //认证并生成令牌(token) @RequestMapping("/auth") public String auth(String userName, String password,String callBackUrl, Model model, HttpSession session,HttpServletRequest request){ //生成token String token = UUID.randomUUID().toString().substring(0,16); if (userName == null&&password == null){ return "login"; } if ("admin".equals(userName) && "admin".equals(password)){ //记录登录状态 session.setAttribute("flag",true); session.setAttribute("token",token); return "index"; }else{ model.addAttribute("error","用户名或密码错误"); return "login"; } } //验证令牌(token) @RequestMapping("/verifi") @ResponseBody public JSONObject vifer(HttpServletRequest request,HttpSession session){ //获取token String authToken = request.getParameter("token"); String token = (String) session.getAttribute("token"); JSONObject jsonObject = new JSONObject(); if (token.equals(authToken) && token !=null){ jsonObject.put("code","200"); jsonObject.put("info","token认证成功"); }else{ jsonObject.put("code","400"); jsonObject.put("info","token认证失败"); } return jsonObject; } //注销 @RequestMapping("/logout") @ResponseBody public JSONObject logOut(HttpServletRequest request){ JSONObject jsonObject = new JSONObject(); HttpSession session = request.getSession(); if (session == null){ }else { session.invalidate(); jsonObject.put("info","注销成功"); jsonObject.put("code", HttpStatus.OK); } return jsonObject; }
子系统需要使用ssoserver中核心的过滤器类,配置此过滤拦截请求
注册过滤器
@Configuration public class SSoFilterConfig { @Bean public FilterRegistrationBean filterRegistrationBean(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setName("ssoFilter"); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("paramName", "paramValue"); filterRegistrationBean.setFilter(ssoFilter()); return filterRegistrationBean; } @Bean public SsoFilter ssoFilter(){ return new SsoFilter(); } }
其他子系统配置与此一致
另外
使用springBoot建立能访问jsp的多模块工程还需要注意两个地方的配置,如图:
到此sso单点登陆项目搭建完成,代码已上传到码云,地址:https://gitee.com/TangThomas/sso
参考文章:https://www.cnblogs.com/ywlaker/p/6113927.html
http://tengj.top/2017/03/13/springboot5/