尚筹网10用户登录
用户登录
目标
检查账号密码正确后将用户信息存入session,表示用户已登陆.
思路
代码:创建MemberLoginVO类
package com.example.entity.vo; import java.io.Serializable; public class MemberLoginVO implements Serializable{ private static final long serialVersionUID = 1L; private String email; private String userName; private Integer id; public MemberLoginVO() { } public MemberLoginVO(Integer id, String userName,String email ) { this.email = email; this.userName = userName; this.id = id; }
代码:执行登陆
@RequestMapping("/auth/member/do/login") public String login(@RequestParam("loginacct") String loginacct, @RequestParam("userpswd") String userpswd, ModelMap modelMap, HttpSession httpSession) { // 1.调用远程接口根据登录账号查询MemberPO对象 ResultEntity<MemberPO> resultEntity = mySQLRemoteService.getMemberPOByLoginAcctRemote(loginacct); if (ResultEntity.FAILED.equals(resultEntity.getResult())) { modelMap.addAttribute(ConstantUtil.ATTR_NANE_MESSAGE, resultEntity.getMessage()); return "member-login"; } MemberPO memberPO = resultEntity.getData(); if (memberPO == null) { modelMap.addAttribute(ConstantUtil.ATTR_NANE_MESSAGE, ConstantUtil.MESSAGE_LOGIN_FAILED); return "member-login"; } // 2.比较密码 String userpswdDataBase = memberPO.getUserPswd(); BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); boolean matchesResult = bCryptPasswordEncoder.matches(userpswd, userpswdDataBase); if (!matchesResult) { modelMap.addAttribute(ConstantUtil.ATTR_NANE_MESSAGE, ConstantUtil.MESSAGE_LOGIN_FAILED); return "member-login"; } // 3.创建MemberLoginVO对象存入Session域 MemberLoginVO memberLoginVO = new MemberLoginVO(memberPO.getId(), memberPO.getUserName(), memberPO.getEmail()); httpSession.setAttribute(ConstantUtil.ATTR_NAME_LOGIN_MEMBER, memberLoginVO); return "redirect:/auth/member/to/center/page"; }
代码:退出登录
@RequestMapping("/auth/member/logout") public String logout(HttpSession httpSession){ httpSession.invalidate(); return "redirect:/"; }
注意:@RequestBody最好加上,有时会识别不了.
缓存功能
1、调用API时,针对不需要实时的功能可以使用本地先缓存,当达到一定时间的时候再去刷新服务器请求
会员登录功能延伸
会话控制回顾
HTTP无状态,所有参数都当成字符串,不会记忆请求之间的关系
因此需要cookie和session把同一个用户关联起来
cookie的工作机制
- 服务器端返回Cookie(键值对)信息给浏览器
- java代码:response.addCookie(cookie对象)
- HTTP响应消息头:set-Cookie:cookie的名字=cookie的值
- 浏览器接受服务器返回的cookie,以后每一次请求都会把cookie带上
- http请求消息头:Cookie:Cookie放入名字=cookie的值
Session的工作机制
- 获取session对象:request.getsession()
- 检查当前请求是否携带了JSESSION这个Cookie
- 带了:根据这个JSEEIONID在服务器查找对应的session对象
- 能找到:就把找到的session对象返回
- 没找到:新建session对象返回,同时返回JSESSIONID的Cookie
- 没带:新建session对象返回,同时返回JSESSIONID的cookie
- 带了:根据这个JSEEIONID在服务器查找对应的session对象
- 检查当前请求是否携带了JSESSION这个Cookie
session共享
在分布式和集群环境下,每个具体模块运行在单独的Tomcat上,而session是被不同Tomcat所“隔开”,所以不能互通,会导致程序在运行时,用户会话数据发送错误.有的服务器上有,有的服务器上没有.
解决方案探索
1、session同步
问题1:造成session在各个服务器上“同量保存”.tomcat保存了1G,tomcatB也得保存1G.数据量太大会导致tomcat的性能下降
问题2:数据同步对性能有一定影响.
2、将session数据存储在Cookie中
- 做法:所有会话数据在浏览器端使用cookie保存,服务器不存储任何会话数据.
- 好处:服务器端大大减轻了数据存储的压力.不会有session不一致问题
- 缺点:
-
- cookie能够存储的数据非常有限.一般是4kb.不能存储丰富的数据
- cookie数据在浏览器端存储,很大程度上不受服务器端控制,如果浏览器端清理cookie,相关数据会丢失.
3、反向代理hash一致性
问题1:具体一个浏览器,专门访问某一个具体服务器,如果服务器宕机则会丢失数据.存在单点故障风险.
问题2:仅仅适用于集群范围内,超出集群范围,负载均衡服务器无效.
4、后端统一存储session数据(正解)
- 思路:若需要session时,都到redis找.
- 后端存储session数据时,一般需要使用redis这样的内存数据库,而一般不采用mysql这样的关系型数据库.原因如下:
- session数据存储比较频繁,内存访问速度快.
- session有过期时间.redis这样的内存数据库能够比较方便实现过期释放.
优点
访问速度比较快.虽然需要经过网络访问,但是现在硬件条件已经能够达到网络访问比硬盘反问还要快
硬盘访问速度:200M/s
网络访问速度:1G/s
redis可以配置主从复制集群,不担心单点故障.
SpringSession使用
环境:Spring Boot
导入坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 引入 springboot&springsession 整合场景 --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
编写配置
# redis 配置
spring.redis.host=192.168.56.100
spring.redis.jedis.pool.max-idle=100
# springsession 配置
spring.session.store-type=redis
注意:存入session域的实体类对象需要支持序列化!!
为什么需要持久化
- 客户端访问了某个能开启会话功能的资源, web服务器就会创建一个与该客户端对应的HttpSession对象,每个HttpSession对象都要站用一定的内存空间。如果在某一时间段内访问站点的用户很多,web服务器内存中就会积累大量的HttpSession对象,消耗大量的服务器内存,即使用户已经离开或者关闭了浏览器,web服务器仍要保留与之对应的HttpSession对象,在他们超时之前,一直占用web服务器内存资源。
- web服务器通常将那些暂时不活动但未超时的HttpSession对象转移到文件系统或数据库中保存,服务器要使用他们时再将他们从文件系统或数据库中装载入内存,这种技术称为Session的持久化。
- 将HttpSession对象保存到文件系统或数据库中,需要采用序列化的方式将HttpSession对象中的每个属性对象保存到文件系统或数据库中;将HttpSession对象从文件系统或数据库中装载如内存时,需要采用反序列化的方式,恢复HttpSession对象中的每个属性对象。所以存储在HttpSession对象中的每个属性对象必须实现Serializable接口
Session的持久化的作用:
- 提高服务器内存的利用率,保证那些暂停活动的客户端在会话超时之前继续原来的会话
- 在多台web服务器协同对外提供服务的集群系统中,使用Session的持久化技术,某台服务器可以将其中发生改变的Session对象复制给其他服务器。保证了在某台服务器停止工作后可以由其他服务器来接替它与客户端的会话
- 在一个web应用程序重启时,服务器也会持久化该应用程序中所有HttpSession对象,保证客户端的会话活动仍可以继续。
Spring Session基本原理
非侵入式
概括:Spring Session从底层全方位接管了tomcat对session的管理.
SpringSession需要完成的任务
1、拦截request请求,包装后发送到redis存储
2、响应:handler响应给浏览器,springSession包装后响应给客户端
3、再一次请求时,把请求的cookie(SessionID)与Redis服务器的sessionID进行对比即可
sessionRepositoryFilter
- 利用Filter原理,在每次请求到达目标方法之前,将原生HttpServletRequest/HttpServletResponse对象包装为SessionRepositoryRequest/ResponseWrapper.
- 包装request对象时要做到:包装后和包装前类型兼容.所谓类型兼容:“包装得到的对象instanceof包装前类型”返回true.
- 只有做到了类型的兼容,后面使用包装过的对象才能够保持使用方法不变.包装过的对象类型兼容、使用方法不变,才能实现“偷梁换柱”.
但是如果直接实现HttpServletRequest接口,我们又不知道如何实现各个抽象方法.这个问题可以借助原始被包装的对象来解决.
HttpSessionStrategy
封装session的存取策略:cookie还是http headers等方式:
sessionRepository
指定存取/删除/过期session操作的repository
redisOperationSessionRepository
使用redis将session保存维护起来
登陆检查
目标
把项目中必须登陆才能访问的功能保护起来,如果没有登陆就访问则跳转到登陆页面.
思路
代码:设置session共享
服务器1,zuul网关
<!-- 引入 springboot&redis 整合场景 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 引入 springboot&springsession 整合场景 --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
Application.yml
spring:
application:
name: adom-crowd-auth
thymeleaf:
prefix: classpath:/templates/
suffix: .html
# 配置session共享
redis:
host: localhost
session:
store-type: redis
服务器2,authentication-consumer微服务
<!-- 引入 springboot&redis 整合场景 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 引入 springboot&springsession 整合场景 --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
Application.yml
server:
port: 4000
spring:
application:
name: adom-crowd-auth
thymeleaf:
prefix: classpath:/templates/
suffix: .html
session:
store-type: redis
代码:准备不需要登陆检查的资源
特定请求地址、静态资源
准备好放行的资源
public static final Set<String> PASS_RES_SET = new HashSet<>(); static { PASS_RES_SET.add("/"); PASS_RES_SET.add("/auth/member/to/reg/page"); PASS_RES_SET.add("/auth/member/to/login/page"); PASS_RES_SET.add("/auth/member/do/login"); PASS_RES_SET.add("/auth/do/member/register"); PASS_RES_SET.add("/auth/member/logout"); PASS_RES_SET.add("/auth/member/send/short/message.json"); } public static final Set<String> STATIC_RES_SET = new HashSet<>();
判断当前请求是否为静态资源
// 判断是否是静态资源 public static boolean judgeCurrentServletPathWetherStaticResource(String servletPath) { if (servletPath == null || servletPath.length() == 0) { throw new RuntimeException(ConstantUtil.MESSAGE_STRING_INVALIDATE); } String[] split = servletPath.split("/"); // 第一个是/前,后是第一个地址,可能是静态地址 String firstLevelPath = split[1]; return STATIC_RES_SET.contains(firstLevelPath); }
代码zuulFilter
创建ZuulFilter类
shouldFilter方法判断是否过滤
public boolean shouldFilter() { // 1.获取RequestContext对象 RequestContext requestContext = RequestContext.getCurrentContext(); // 2.通过RequestContext对象获取当前请求对象(框架底层是借助ThreadLocal从当前线程上获取事先绑定的request对象) HttpServletRequest request = requestContext.getRequest(); // 3.获取servletPath String servletPath = request.getServletPath(); // 4.判定是否放行 boolean containsResult = AccessPassResource.PASS_RES_SET.contains(servletPath); if (containsResult) { // 不拦截 return false; } //5、判断是否为静态资源 boolean judgeStaticResult = AccessPassResource.judgeCurrentServletPathWetherStaticResource(servletPath); if (judgeStaticResult) { return false; } return true; }
run方法执行过滤
没有登陆则跳转到登陆页面
public Object run() throws ZuulException { //1、获取当前请求对象 RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); //2、获取当前Session对象 HttpSession session = request.getSession(); //3、从session对象中获取已登录的用户 Object loginMember = session.getAttribute(ConstantUtil.ATTR_NAME_LOGIN_MEMBER); //4、判断loginMember是否为空 if (loginMember == null) { // 5、从requestContext对象中获取Response对象 HttpServletResponse response = requestContext.getResponse(); // 6.将提示消息存入session域 session.setAttribute(ConstantUtil.ATTR_NANE_MESSAGE, ConstantUtil.MESSAGE_ACCESS_FORBIDEN); try { //7、重定向到登陆页面 response.sendRedirect("/auth/member/to/login/page"); } catch (IOException e) { e.printStackTrace(); } } return null; }
filterType方法
表示在目标微服务执行之前执行
@Override public String filterType() { // 在目标微服务执行前过滤 return "pre"; }
代码:登陆页面读取session域
代码:Zuul中的特殊设置
为了能够让整个过程中保持session工作正常,需要加入配置:
zuul:
ignored-services: "*"
sensitive-headers: "*" #在Zuul向其他微服务重定向时保持原本头信息(请求头、响应头),不然session会带不过去
routes:
crowd-portal: #路由名
service-id: adom-crowd-auth #服务名称
path: /** #映射地址,这里一定要使用两个"*"号,不然"/"路径后面的多层路径将无法访问