认证服务的问题及解决方案
问题1:发送验证码
发送验证码要注意的问题
发送验证码要注意的两个问题
1、接口防刷
接口防刷就是因为发送验证码的api接口是可以通过查看页面元素看的到的
上面就可以看到发送验证码的js代码中的请求地址,可以恶意的通过postman不断的发送这个请求来消耗短信验证码的资源,更准确来说是为了防止同一个手机号疯狂的发这个请求不断的消耗发送短信的资源
2、验证码再次校验
在发送了一次验证码后通过js的限制的确不能再点发送验证码
这个是通过js实现的超链接不可用来实现不能重复发送验证码
但是刷新页面后还是可以对同一个手机号再次发送验证码,也就是我可以先对一个手机号发送一次验证码,这时虽然验证码发送链接不可用点了,但是我刷新页面后还是可以对同个手机号继续发送验证码,并且还是在60s内
解决方案
/** * 短信验证码 * @param phone * @return */ @ResponseBody @GetMapping(value = "/sms/sendCode") public R sendCode(@RequestParam("phone") String phone) { /** * 接口防刷 */ //把验证码从缓存中提取出来 //(缓存中验证码的格式sms:code:123456789->123456_1646981054661 ,123456789是手机号,123456表示验证码,1646981054661表示存入缓存的时间) String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone); if (!StringUtils.isEmpty(redisCode)) { //把存入缓存的验证码的值给提取出来(格式是123456_1646981054661 ,123456表示验证码,1646981054661表示存入缓存的时间) long currentTime = Long.parseLong(redisCode.split("_")[1]); //活动存入redis的时间,用当前时间减去存入redis的时间,判断用户手机号是否在60s内发送验证码 if (System.currentTimeMillis() - currentTime < 60000) { //60s内不能再发 // BizCodeEnum.SMS_CODE_EXCEPTION=10002 BizCodeEnum.SMS_CODE_EXCEPTION = 10002 return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMessage()); } } /**验证码再次检验 * 2、创建验证码存入redis.存key-phone,value-code */ int code = (int) ((Math.random() * 9 + 1) * 100000); String codeNum = String.valueOf(code); //存入缓存的验证码格式是123456_1646981054661 加系统时间是为了防止多次刷新验证码 String redisStorage = codeNum + "_" + System.currentTimeMillis(); //存入redis,防止同一个手机号在60秒内再次发送验证码(存入缓存的格式sms:code:123456789->123456 ,其中123456789是手机号,123456是验证码) stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone, redisStorage,10, TimeUnit.MINUTES);//AuthServerConstant.SMS_CODE_CACHE_PREFIX = sms:code: // String codeNum = UUID.randomUUID().toString().substring(0,6); thirdPartFeignService.sendCode(phone, codeNum); return R.ok(); }
只需要加个时间戳存到redis中,在实现这步之前先去redis中取,然后分割出时间戳看看是不是在60s,如果是那就报错,不是就执行下面的逻辑,上面两个问题都只需要加个时间戳然后判断这个手机号是否在60s内发送了验证码即可
问题2:微服务中Session
session可以看做是服务器内存,用来存储浏览器的发过来的信息,这些session都交给sessionManager来管理,同一个域名session有效,不同域名session不能共享
微服务中Session常见的两个问题
微服务中Session常见的两个问题
有多个同样的服务,域名相同,但是在负载均衡时各个服务中会session不同步问题,例如上面的两个会员服务,可能存用户信息的session存到了第一个会员服务中,然后下次这个用户登录的时候却发给了第二个会员服务,但是第二个会员服务中没有session,所以会出现session不同步问题
不同服务,session不能共享问题,因为域名不同,微服务中不同的服务有着不同的域名
解决方案思路
思路一
思路二
其实就是存到浏览器中
方案一
方案二
最终采取的解决方案
使用SpringSession的目的是来解决分布式session不同步不共享的问题,首先用户登录成功后存放用户信息的session会存到会员服务里,然后会员服务把session存到redis中,然后会员服务就会给浏览器发卡(就是如上图发送一个jsessionid=1的cookie给浏览器),由于默认发卡的域名是当前服务的域名,也就是会员服务的域名,这个域名发给浏览器,但是这个域名作用域只在会员服务中有效,作用域太小了,所以需要扩大域名为父域名.gulimall.com,这样所有的服务的域名都可以访问到这个session了
整合SpringSession来解决session不同步不共享的问题
使用SpringSession的目的是来解决分布式session不同步不共享的问题。使用SpringSession可以把session都存在redis中,这样就解决了session不同步的问题,然后扩大作用域,这就解决了session不共享的问题,SpringSession不需要显性的操作(也就是不需要用StringRedisTemplate类的方法来把session放到redis中去,啥都不用干,就正常的把数据放到HttpSession中就可),由于整合了SpringSession,所以放到HttpSession中的数据会自动的放到redis中去,由于配置了序列化,所以session会被序列化json字符串放到redis中去,然后前端某个服务要取这个session的时候也会自动的redis中取
注意:由于这里使用springsession的用的类型是redis,所以这springsession和redis都要一起加入依赖和配置(所以session会被存到Redis缓存中)
(1)导入依赖
<!-- 整合springsession 来解决分布式session不同步不共享的问题--> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <!-- 整合redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
(2)在application.properties配置文件里配置springsession
#配置springsession spring.session.store-type=redis server.servlet.session.timeout=30m #配置redis的ip地址 spring.redis.host=192.168.241.128
(3)在config配置中加入springSession配置类
package com.saodai.saodaimall.order.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; /** * springSession配置类(所有要使用session的服务的session配置要一致) */ @Configuration public class SessionConfig { /** * 配置session(主要是为了放大session作用域) * @return */ @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); //放大作用域 cookieSerializer.setDomainName("saodaimall.com"); cookieSerializer.setCookieName("SAODAISESSION"); return cookieSerializer; } /** * 配置Session放到redis存储的格式为json(其实就是json序列化) * @return */ @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } }
自定义一个配置类SessionConfig然后通过设置CookieSerializer来扩大Session的作用域,再配置json序列化
(4)在启动类上添加@EnableRedisHttpSession注解
package com.saodai.saodaimall.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; /** * 订单服务启动类 */ @EnableFeignClients @EnableRedisHttpSession @EnableDiscoveryClient @SpringBootApplication public class SaodaimallOrderApplication { public static void main(String[] args) { SpringApplication.run(SaodaimallOrderApplication.class, args); } }
SpringSession的原理
Spring-Session的实现就是设计一个过滤器SessionRepositoryFilter,每当有请求进入时,过滤器会首先将ServletRequest 和ServletResponse 这两个对象转换成Spring内部的包装类SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper对象,它使用了一个SessionRepositoryRequestWrapper类接管了Http Session并重写了getSession方法来实现了session的创建和管理工作。将原本需要由web服务器创建会话的过程转交给Spring-Session进行创建,本来创建的会话保存在Web服务器内存中,通过Spring-Session创建的会话信息可以保存第三方的服务中,如:redis,mysql等。Web服务器之间通过连接第三方服务来共享数据,实现Session共享!
@Order(SessionRepositoryFilter.DEFAULT_ORDER) public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); //使用HttpServletRequest 、HttpServletResponse和servletContext创建一个SessionRepositoryRequestWrapper SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext); SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper( wrappedRequest, response); try { filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { //保存session信息 wrappedRequest.commitSession(); } } } @Override public HttpSessionWrapper getSession(boolean create) { //获取当前Request作用域中代表Session的属性,缓存作用避免每次都从sessionRepository获取 HttpSessionWrapper currentSession = getCurrentSession(); if (currentSession != null) { return currentSession; } //查找客户端中一个叫SESSION的cookie,拿到sessionId,通过sessionRepository对象根据sessionId去Redis中查找 S requestedSession = getRequestedSession(); //如果从redis中查询到了值 if (requestedSession != null) { //客户端存在sessionId 并且未过期 if (getAttribute(INVALID_SESSION_ID_ATTR) == null) { requestedSession.setLastAccessedTime(Instant.now()); this.requestedSessionIdValid = true; currentSession = new HttpSessionWrapper(requestedSession, getServletContext()); currentSession.setNew(false); //将Session设置到request属性中 setCurrentSession(currentSession); return currentSession; } } else { // This is an invalid session id. No need to ask again if // request.getSession is invoked for the duration of this request if (SESSION_LOGGER.isDebugEnabled()) { SESSION_LOGGER.debug( "No session found by id: Caching result for getSession(false) for this HttpServletRequest."); } setAttribute(INVALID_SESSION_ID_ATTR, "true"); } //不创建Session就直接返回null if (!create) { return null; } if (SESSION_LOGGER.isDebugEnabled()) { SESSION_LOGGER.debug( "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for " + SESSION_LOGGER_NAME, new RuntimeException( "For debugging purposes only (not an error)")); } //执行到这了说明需要创建新的Session // 通过sessionRepository创建RedisSession这个对象 S session = SessionRepositoryFilter.this.sessionRepository.createSession(); session.setLastAccessedTime(Instant.now()); currentSession = new HttpSessionWrapper(session, getServletContext()); setCurrentSession(currentSession); return currentSession; } // 通过sessionRepository创建RedisSession这个对象 @Override public RedisSession createSession() { Duration maxInactiveInterval = Duration .ofSeconds((this.defaultMaxInactiveInterval != null) ? this.defaultMaxInactiveInterval : MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); RedisSession session = new RedisSession(maxInactiveInterval); session.flushImmediateIfNecessary(); return session; }