文章目录

  • 基于Session的身份认证
    • 简介
    • 实现
  • 分布式Session共享
    • 简介
    • 实现
  • 单点登陆
    • 简介
    • 实现过程
    • 总结

基于Session的身份认证

简介

对于一个Web应用,客户端每次请求时,服务器都会打开一个新的会话,而且服务器不会维护客户端的上下文信息,因此如何管理客户端会话是必须要解决的问题。我们知道HTTP是无状态的协议,所以它提供了一种机制,通过Session来保存上下文信息,为每个用户分配一个sessionId,并且每个用户收到的sessionId都不一样,变量的值保存在服务器端。Session是以cookie或URL重写为基础的,默认使用cookie来实现,系统会创造一个名为JSESSIONID的值输出到cookie中。当用户从客户端向服务端发起HTTP请求时,会携带有sessionId的cookie请求, 这样服务端就能根据sessionId进行区分用户了。

实现

只需要简单定义一个Filter,进行拦截非登录请求,然后确认当前请求的Session中是否能够拿到用户信息,如果能拿到用户信息,那么就是登录状态,否则,认定当前请求无效,将请求转发到登录页面即可

//定义登录过滤器
public class LoginFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse)servletResponse;Object user = request.getSession().getAttribute(CommonConstants.USER_ATTR);;String requestUrl = request.getServletPath();//非登陆页面并且不是登陆状态if (!requestUrl.startsWith("/login")&& null == user) {//则拒绝当前请求,请求转发到登陆页面            request.getRequestDispatcher("/login").forward(request,response);return ;}filterChain.doFilter(request,servletResponse);}@Overridepublic void destroy() {}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}
}

之后通过@Bean方式注入一个FilterRegistrationBean实例,并为它设置Filter属性,指定自定义的Filter实例,这样自定义的Filter才能在程序中生效

@Configuration
public class WebMvcConfig {//将过滤器添加到请求中@Beanpublic FilterRegistrationBean sessionFilterRegistration() {FilterRegistrationBean registration = new FilterRegistrationBean();registration.setFilter(new LoginFilter());registration.addUrlPatterns("/*");registration.addInitParameter("paramName", "paramValue");registration.setName("loginFilter");registration.setOrder(1);return registration;}
}

分布式Session共享

简介

随着分布式架构的演进,单个服务器已经不能满足系统的需要了,通常都会把系统部署在多台服务器上,通过Nginx负载均衡把请求分发到其中的一台服务器上,这样很可能同一个用户的请求被分发到不同的服务器上,因为Session是保存在服务器上的,那么很有可能第一次请求访问的服务器A,创建了Session,但是第二次访问到了服务器B,这时就会出现取不到Session的情况。因此要在集群环境下使用,最好的解决办法就是使用Session共享

实现

整个实现的核心点就是通过定义一个Request请求对象的Wrapper包装类,负责对当前Request请求的Session获取逻辑进行重写,将Session信息交由Redis进行存储和管理,包括从Redis获取Session信息以及认证成功后将Session信息提交到Redis中。因为实现比较简单,就不分析了,具体实现可以看下面代码的注释:

//request请求的包装类
public class RedisRequestWrapper extends HttpServletRequestWrapper {private volatile boolean committed = false;private String uuid = UUID.randomUUID().toString();private RedisSession session;private RedisTemplate redisTemplate;public RedisRequestWrapper(HttpServletRequest request,RedisTemplate redisTemplate) {super(request);this.redisTemplate = redisTemplate;}/*** 提交session信息到redis*/public void commitSession() {//避免请求重复提交sessionif (committed) {return;}committed = true;RedisSession session = this.getSession();if (session != null && null != session.getAttrs()) {//将session信息存入redisredisTemplate.opsForHash().putAll(session.getId(),session.getAttrs());}}/*** 创建新session*/public RedisSession createSession() {//从cookie中获得JSESSIONIDString sessionId = CookieUtil.getRequestedSessionId(this);Map<String,Object> attr ;if (null != sessionId){//通过sessionid从redis缓存中,获取session信息attr = redisTemplate.opsForHash().entries(sessionId);} else {//随机生成一个sessionidsessionId = UUID.randomUUID().toString();attr = new HashMap<>();}//session成员变量持有session = new RedisSession();session.setId(sessionId);session.setAttrs(attr);return session;}/*** 获取session*/public RedisSession getSession() {return this.getSession(true);}public RedisSession getSession(boolean create) {if (null != session){return session;}return this.createSession();}/*** 确认是否登陆*/public boolean isLogin(){Object user = getSession().getAttribute(SessionFilter.USER_INFO);return null != user;}}
public class CookieUtil{public static final String COOKIE_NAME_SESSION = "jsession";/***  从请求的cookie中获取sessionid*/public static String getRequestedSessionId(HttpServletRequest request) {Cookie[] cookies = request.getCookies();if (cookies == null) {return null;}for (Cookie cookie : cookies) {if (cookie == null) {continue;}//确认是否存在sessionidif (!COOKIE_NAME_SESSION.equalsIgnoreCase(cookie.getName())) {continue;}return cookie.getValue();}return null;}/*** 将session信息保存到cookie中*/public static void onNewSession(HttpServletRequest request,HttpServletResponse response) {HttpSession session = request.getSession();String sessionId = session.getId();Cookie cookie = new Cookie(COOKIE_NAME_SESSION, sessionId);cookie.setHttpOnly(true);cookie.setPath(request.getContextPath() + "/");//指定一级域名cookie.setDomain("xxx.com");cookie.setMaxAge(Integer.MAX_VALUE);response.addCookie(cookie);}}
//自定义session,实现HttpSession接口
public class RedisSession implements Serializable,HttpSession {private String id;private Map<String,Object> attrs;...
}

自定义Filter,用于拦截请求,根据Session信息来确认是否为登录状态

//自定义filter,用来拦截非登录请求
public class SessionFilter implements Filter {public static final String USER_INFO = "user";private RedisTemplate redisTemplate;public void setRedisTemplate(RedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}@Overridepublic void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse)servletResponse;RedisRequestWrapper redisRequestWrapper = new RedisRequestWrapper(request,redisTemplate);String requestUrl = request.getServletPath();//非登陆页面并且不是登陆状态if (!"/toLogin".equals(requestUrl)&& !requestUrl.startsWith("/login")&& !redisRequestWrapper.isLogin()) {//拒绝请求,跳转登陆页面request.getRequestDispatcher("/toLogin").forward(redisRequestWrapper,response);return ;}try {filterChain.doFilter(redisRequestWrapper,servletResponse);} finally {//提交session信息到redis中redisRequestWrapper.commitSession();}}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void destroy() {}
}

通过@Bean方法向容器中注入Filter,使自定义Filter生效

@Configuration
public class SessionConfig {@Beanpublic FilterRegistrationBean sessionFilterRegistration(SessionFilter sessionFilter) {FilterRegistrationBean registration = new FilterRegistrationBean();registration.setFilter(sessionFilter);registration.addUrlPatterns("/*");registration.addInitParameter("paramName", "paramValue");registration.setName("sessionFilter");registration.setOrder(1);return registration;}//注入自定义过滤器@Beanpublic SessionFilter sessionFilter(RedisTemplate redisTemplate){SessionFilter sessionFilter = new SessionFilter();sessionFilter.setRedisTemplate(redisTemplate);return sessionFilter;}
}
@Controller
public class IndexController {@GetMapping("/toLogin")public String toLogin(Model model,RedisRequestWrapper request) {UserForm user = new UserForm();user.setUsername("username");user.setPassword("password");user.setBackurl(request.getParameter("url"));model.addAttribute("user", user);return "login";}@PostMapping("/login")public void login(@ModelAttribute UserForm user,RedisRequestWrapper request,HttpServletResponse response) throws IOException, ServletException {request.getSession().setAttribute(SessionFilter.USER_INFO,user);//将session信息保存到cookie中CookieBasedSession.onNewSession(request,response);//重定向到index页面response.sendRedirect("/index");}@GetMapping("/index")public ModelAndView index(RedisRequestWrapper request) {ModelAndView modelAndView = new ModelAndView();modelAndView.setViewName("index");modelAndView.addObject("user", request.getSession().getAttribute(SessionFilter.USER_INFO));request.getSession().setAttribute("test","123");return modelAndView;}
}

单点登陆

简介

较大的企业内部,一般都有很多的业务支持系统为其提供相应的管理和 IT 服务。通常来说,每个单独的系统都会有自己的安全体系和身份认证系统。进入每个系统都需要进行登录,这样的局面不仅给管理上带来了很大的困难,对客户来说也极不友好。那么如何让客户只需登陆一次,就可以进入多个系统,而不需要重新登录呢。“单点登录”就是专为解决此类问题的。其大致思想流程如下:通过一个 ticket 进行串接各系统间的用户信息

实现过程

在每一个需要身份认证的服务中,定义一个SSOFilter用于拦截非登录请求。对于每个拦截的请求,会先从当前请求的Session中确认是否能够拿到用户信息,拿不到用户信息又会确认当前请求中是否携带ticket票据这个参数,如果携带就会尝试从Redis中根据该票据拿到用户信息。如果最终都获取不到用户信息就会被重定向到SSO登录服务的登录页面进行登录处理

public class SSOFilter implements Filter {private RedisTemplate redisTemplate;public static final String USER_INFO = "user";public SSOFilter(RedisTemplate redisTemplate){this.redisTemplate = redisTemplate;}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse)servletResponse;Object userInfo = request.getSession().getAttribute(USER_INFO);;//如果未登陆,则拒绝请求,转向登陆页面String requestUrl = request.getServletPath();if (!"/toLogin".equals(requestUrl)&& !requestUrl.startsWith("/login")&& null == userInfo) {String ticket = request.getParameter("ticket");//有票据,则使用票据去尝试拿取用户信息if (null != ticket){userInfo = redisTemplate.opsForValue().get(ticket);}//无法得到用户信息,则去CAS服务的登陆页面if (null == userInfo){response.sendRedirect("http://cas.com:8080/toLogin?url="+request.getRequestURL().toString());return ;}/*** 将用户信息,加载进session中*/request.getSession().setAttribute(SSOFilter.USER_INFO,userInfo);//登录成功需要将ticket从redis中删除redisTemplate.delete(ticket);}filterChain.doFilter(request,servletResponse);}@Overridepublic void destroy() {}}

在SSO登录服务中,只需要简单定义一个Filter,进行拦截非登录请求,然后确认当前请求的Session中是否能够拿到用户信息,如果能拿到用户信息,那么就是登录状态,否则,认定当前请求无效,将请求转发到登录页面即可

public class LoginFilter implements Filter {public static final String USER_INFO = "user";@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse)servletResponse;Object userInfo = request.getSession().getAttribute(USER_INFO);;//非登陆页面并且不是登陆状态String requestUrl = request.getServletPath();if (!"/toLogin".equals(requestUrl)&& !requestUrl.startsWith("/login")&& null == userInfo) {//则拒绝当前请求,请求转发到登陆页面 request.getRequestDispatcher("/toLogin").forward(request,response);return ;}filterChain.doFilter(request,servletResponse);}@Overridepublic void destroy() {}}

在SSO登录处理过程中,当请求已认证登录成功后,会先生成一个ticket票据,并将ticket票据和用户信息存放到Redis中,然后重定向回原先请求服务的Url,并携带上ticket票据参数

@Controller
public class IndexController {@Autowiredprivate RedisTemplate redisTemplate;@GetMapping("/toLogin")public String toLogin(Model model,HttpServletRequest request) {Object userInfo = request.getSession().getAttribute(LoginFilter.USER_INFO);//不为空,则是已登陆状态if (null != userInfo){String ticket = UUID.randomUUID().toString();redisTemplate.opsForValue().set(ticket,userInfo,2, TimeUnit.SECONDS);return "redirect:"+request.getParameter("url")+"?ticket="+ticket;}UserForm user = new UserForm();user.setUsername("username");user.setPassword("password");user.setBackurl(request.getParameter("url"));model.addAttribute("user", user);return "login";}@PostMapping("/login")public void login(@ModelAttribute UserForm user,HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {request.getSession().setAttribute(LoginFilter.USER_INFO,user);//登陆成功,创建用户信息票据String ticket = UUID.randomUUID().toString();//将ticket和用户信息写入到redis中redisTemplate.opsForValue().set(ticket,user,20, TimeUnit.SECONDS);//重定向,回原请求的url,并携带ticket信息if (null == user.getBackurl() || user.getBackurl().length()==0){response.sendRedirect("/index");} else {response.sendRedirect(user.getBackurl()+"?ticket="+ticket);}}@GetMapping("/index")public ModelAndView index(HttpServletRequest request) {ModelAndView modelAndView = new ModelAndView();modelAndView.setViewName("index");modelAndView.addObject("user", request.getSession().getAttribute(LoginFilter.USER_INFO));request.getSession().setAttribute("test","123");return modelAndView;}
}

总结

  1. 用户访问服务A某个页面时,服务A发现自己未登录,重定向到CAS单点登录服务,CAS服务也发现未登录,则跳转到相应的登录页面
  2. 用户输入用户名和密码登录成功后,CAS服务进行认证,将登录状态记录CAS服务的session中,并写入当前CAS服务域名下的Cookie中
  3. CAS服务登录完成后会生成一个Ticket票据,并将Ticket和用户信息记录到Redis中,之后再重定向回服务A,同时将Ticket作为参数传递给服务A
  4. 服务A拿到Ticket后,从Redis中进行查找,查询Ticket对应的用户信息,之后服务A再将登录状态写入session并设置到服务A域名下的Cookie中
  5. 至此,单点登录就完成了,之后再访问服务A时,服务A就是登录状态的
  6. 当有一个新的服务B被用户访问时,服务B发现自己也未登录,此时也重定向到CAS单点登录服务,但是此时CAS服务发现已经登录了,此时就不需要进行登录认证
  7. CAS服务会生成一个Ticket票据,并将Ticket和用户信息记录到Redis中,之后再重定向回服务B,同时将Ticket作为参数传递给服务B
  8. 服务B拿到Ticket后,从Redis中进行查找,查询Ticket对应的用户信息,之后服务B再将登录状态写入session并设置到服务B域名下的Cookie中
  9. 因此服务B不需要进行登录过程,就能完成用户登录认证

由于本人能力有限,分析不恰当的地方和文章有错误的地方的欢迎批评指出,非常感谢!

分布式Session共享和单点登录实现相关推荐

  1. C#session共享+redis_技术干货分享:基于SpringBoot+Redis的Session共享与单点登录

    categories: 架构 author: mrzhou tags: SpringBoot redis session 单点登录 基于SpringBoot+Redis的Session共享与单点登录 ...

  2. 基于Session共享的单点登录或通行证系统方案

    本文主要描述如何基于Session共享来实现单点登录. 假设有两个应用www.example.com, passport.example.com.本文以SpringSession和Redis来实现相关 ...

  3. 分布式Session共享解决方案

    转载自 分布式Session共享解决方案 Session是服务器用来保存用户操作的一系列会话信息,由Web容器进行管理.单机情况下,不存在Session共享的情况,分布式情况下,如果不进行Sessio ...

  4. 分布式Session共享的4类技术方案,与优劣势比较

    分布式Session共享的4类技术方案,与优劣势比较 什么是session 什么是session一致性问题? 分布式session Session一致性解决方案 1.什么是session 服务器为每个 ...

  5. Spring Boot(十一)Redis集成从Docker安装到分布式Session共享

    2019独角兽企业重金招聘Python工程师标准>>> 一.简介 Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并 ...

  6. shiro分布式session共享

    当我们开发的程序需要跑在多个tomcat容器或者多台机器上时,shiro的默认session存储就不能满足我们的需求了,其中shiro默认的session是存储在运行jvm内存中的,使用的Abstra ...

  7. java redis实现session共享_redis实现分布式session共享

    在讲解redis分布式session共享之前,我们先聊聊tomcat中session管理机制,包括:请求过程中session操作,sessionid解析过程,servlet获取session流程,以及 ...

  8. 分布式Session共享(二):tomcat+memcached实现session共享 - 萝卜兔子 - 博客园

    分布式Session共享(二):tomcat+memcached实现session共享 - 萝卜兔子 - 博客园 http://www.cnblogs.com/notDog/p/5341219.htm ...

  9. Cookie Session跨站无法共享问题(单点登录解决方案)

    单点登录 什么是单点登录,举个例子,如果你登录了msn messenger,访问hotmail邮件就不用在此登录. 一般单点登录都需要有一个独立的登录站点,一般具有独立的域名,专门的进行注册,登录,注 ...

最新文章

  1. 德国市占率第一的科沃斯携最新扫地机器人亮相IFA展
  2. python opencv图像对比度增强_图像增强、锐化, Python-OpenCV 来实现 4 种方法!
  3. 手把手实现YOLOv3(一)
  4. Python循环定时服务功能(相似contrab)
  5. 小到年货大到产业,刘村长的扶贫模式有点厉害!
  6. 在jsp文件中通过超链接访问servlet_Eclipse中创建Servlet
  7. python获取网页源码被拒绝_Python3 请求网页源码 目标计算机积极拒绝,无法连接...
  8. 比量iOS6/iOS7, 3.5inch/4.0inch
  9. 锁定Mac的键盘:连击5次option键
  10. CocosCreator2.3.1切换场景出现Failed to load scene ‘xxx‘ because ‘xxx‘ is already being loaded问题的解决方案
  11. 杭州的互联网公司总结
  12. 参考平面及其高度_遥感影像中建筑物平面及高度信息提取方法
  13. jee6 学习笔记 5 - Struggling with JSF2 binding GET params
  14. 关于SI (系统集成)
  15. 计算机专业,真的这么赚钱吗?
  16. 笔记本wifi热点设置
  17. python:mplfinance 画K线图
  18. 破除对于XP半开连接数限制的误解
  19. vue---双向绑定
  20. 安装包制作工具 SetupFactory API清单

热门文章

  1. windows启动tomcat乱码问题
  2. 微信公众号内下载pdf等文件,受微信所限制,安卓和IOS不同处理方式(最最最优版)
  3. 力扣146题 LRU 缓存机制
  4. PaddlePaddle飞桨论文复现营——3D Residual Networks for Action Recognition学习笔记
  5. Windows各版本符号表离线下载
  6. configure: error: no acceptable C compiler found in $PATH 问题解决
  7. 怎样将PDF文档进行翻译?PDF文档翻译简单方法介绍
  8. [C语言]倒序输出字符串
  9. 如何把canvas元素作为网站背景总结详解
  10. VBA基础,工作簿workbook相关的方法和属性