(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 GulimallSessionConfig { /** * 配置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(); } }
(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; }
好文参考:Spring-Session实现session共享原理及解析_五霸哥的博客-CSDN博客_session共享如何实现
小技术
使用视图映射器来实现页面跳转
传统写法是在控制器里实现
自定义一个配置类来实现WebMvcConfigurer接口,然后重写addViewControllers方法来增加视图映射器
使用配置文件来动态配置属性值
这样就可以通过在配置文件里修改对应的值来改变属性值,核心注解是@ConfigurationProperties(prefix=""),@Data注解也要加
使用Feign远程调用服务
添加openFeign依赖并且在启动了通过@EnableFeignClients注解开启远程调用端即可用feign远程调用服务
定义一个远程调用的接口,通过@FeignClient注解来指定调用哪个服务,把第三方服务控制器的方法签名拿过来即可,注意路径一定要写对,特别是如果有父路径不要忘了写
在需要远程调用的服务器里注入刚写的远程接口,然后调用就可,例如这里是认证中心调用第三方服务的发生验证码的接口
使用异常机制
/** * 会员注册 */ @Override public void register(MemberUserRegisterVo vo) { MemberEntity memberEntity = new MemberEntity(); //设置默认等级 MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel(); memberEntity.setLevelId(levelEntity.getId()); //设置其它的默认信息 //检查用户名和手机号是否唯一。感知异常,异常机制(异常机制就是问题就抛出具体异常,没问题就继续执行下面的语句) checkPhoneUnique(vo.getPhone()); checkUserNameUnique(vo.getUserName()); memberEntity.setNickname(vo.getUserName()); memberEntity.setUsername(vo.getUserName()); //密码进行MD5盐值加密(盐值加密同一个数据的每次加密结果是不一样的,通过match方法来密码校验) // (注意这里不能用md5直接加密放数据库,因为彩虹表可以破解md5,所谓彩虹表就是通过大量的md5数据反向退出md5 // 注意MD5是不可逆,但是可暴力通过彩虹表破解) BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String encode = bCryptPasswordEncoder.encode(vo.getPassword()); memberEntity.setPassword(encode); memberEntity.setMobile(vo.getPhone()); memberEntity.setGender(0); memberEntity.setCreateTime(new Date()); //保存数据 this.baseMapper.insert(memberEntity); } /** * 检查手机号是否重复的异常机制方法 * @param phone * @throws PhoneException */ @Override public void checkPhoneUnique(String phone) throws PhoneException { Long phoneCount = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone)); //usernameCount > 0表示手机号已经存在 if (phoneCount > 0) { throw new PhoneException(); } } /** * 检查用户名是否重复的异常机制方法 * @param userName * @throws UsernameException */ @Override public void checkUserNameUnique(String userName) throws UsernameException { Long usernameCount = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName)); //usernameCount > 0表示用户名已经存在 if (usernameCount > 0) { throw new UsernameException(); } }
上面定义检查用户名和电话号码的异常机制方法的具体实现
/** * 会员注册功能 * @param vo * @return */ @PostMapping(value = "/register") public R register(@RequestBody MemberUserRegisterVo vo) { try { memberService.register(vo); } catch (PhoneException e) { //BizCodeEnum.PHONE_EXIST_EXCEPTION=存在相同的手机号 15002 return R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnum.PHONE_EXIST_EXCEPTION.getMessage()); } catch (UsernameException e) { //BizCodeEnum.USER_EXIST_EXCEPTION=商品库存不足 21000 return R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(),BizCodeEnum.USER_EXIST_EXCEPTION.getMessage()); } return R.ok(); }
使用异常机制的原因就是希望控制器能够发现并处理异常
package com.saodai.saodaimall.member.exception; public class UsernameException extends RuntimeException { public UsernameException() { super("存在相同的用户名"); } } package com.saodai.saodaimall.member.exception; public class PhoneException extends RuntimeException { public PhoneException() { super("存在相同的手机号"); } }
把上面两个单独的异常抽取出来封装成异常类
使用MD5盐值加密
加密
先创建一个加密器BCryptPasswordEncoder,然后调用他的encode方法把需要加密的密码放进去就会自动生成一串加密后的值
注意同一个密码每次生成的值是不一样的
解密
从数据库里面拿到加密的数据后调用matches方法就可以匹配两个密码是否一致,如果一致那就返回true,不一致返回false,前面是password是旧密码(没加密的密码),后面的passwordDb是数据库加密的密码