认证功能

发送短信验证码

发送短信验证码以前做过,使用腾讯云。在第三方服务项目中提供远程调用接口。

接口

  1. @RestController
  2. @RequestMapping("/sms")
  3. public class MsgController {
  4. @Autowired
  5. private MsgService msgService;
  6. @GetMapping("/sendMsg")
  7. public R sendMsg(@RequestParam("phone") String phone, @RequestParam("code") String code) {
  8. boolean b = msgService.sendMessage(phone, code);
  9. return b ? R.ok() : R.error();
  10. }
  11. }

实现类

  1. @Service
  2. public class MsgServiceImpl implements MsgService {
  3. @Override
  4. public boolean sendMessage(String phone, String code) {
  5. try {
  6. // 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
  7. // 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
  8. Credential cred = new Credential(ConstantMsgUtil.SECRET_ID, ConstantMsgUtil.SECRET_KEY);
  9. // 实例化一个http选项,可选的,没有特殊需求可以跳过
  10. HttpProfile httpProfile = new HttpProfile();
  11. httpProfile.setEndpoint(ConstantMsgUtil.END_POINT);
  12. // 实例化一个client选项,可选的,没有特殊需求可以跳过
  13. ClientProfile clientProfile = new ClientProfile();
  14. clientProfile.setHttpProfile(httpProfile);
  15. // 实例化要请求产品的client对象,clientProfile是可选的
  16. SmsClient client = new SmsClient(cred, "ap-guangzhou", clientProfile);
  17. // 实例化一个请求对象,每个接口都会对应一个request对象
  18. SendSmsRequest req = new SendSmsRequest();
  19. // 国内短信需要加上86
  20. String[] phoneNumberSet1 = {"86" + phone};
  21. req.setPhoneNumberSet(phoneNumberSet1);
  22. req.setSmsSdkAppId(ConstantMsgUtil.APP_ID);
  23. req.setSignName(ConstantMsgUtil.SIGN_NAME);
  24. req.setTemplateId(ConstantMsgUtil.TEMPLATE_ID);
  25. String[] templateParamSet1 = {code};
  26. req.setTemplateParamSet(templateParamSet1);
  27. // 返回的resp是一个SendSmsResponse的实例,与请求对象对应
  28. SendSmsResponse resp = client.SendSms(req);
  29. // 输出json格式的字符串回包
  30. System.out.println(SendSmsResponse.toJsonString(resp));
  31. return true;
  32. } catch (TencentCloudSDKException e) {
  33. System.out.println(e.toString());
  34. return false;
  35. }
  36. }
  37. }

其他信息参照谷粒学院项目。

发送短信验证码接口

  1. @GetMapping("/sms/sendCode")
  2. public R sendCode(@RequestParam("phone") String phone) {
  3. // TODO 接口防刷
  4. // 60s防止用户再次发送请求
  5. String redisCode = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + phone);
  6. if (!StringUtils.isEmpty(redisCode)) {
  7. long l = Long.parseLong(redisCode.split("_")[1]);
  8. if (System.currentTimeMillis() - l < 60000) { // 一分钟之内就不发送
  9. return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
  10. }
  11. }
  12. // 根据时间生成验证码,存入redis需要带上时间,发短信不需要
  13. // String code = UUID.randomUUID().toString().substring(0, 4);
  14. String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
  15. String codeToRedis = code + "_" + System.currentTimeMillis();
  16. // 存入redis
  17. redisTemplate.opsForValue().set(AuthConstant.SMS_CODE_CACHE_PREFIX + phone, codeToRedis, 10, TimeUnit.MINUTES);
  18. thirdPartyFeignService.sendMsg(phone, String.valueOf(code));
  19. return R.ok();
  20. }

注册

基本流程

参数校验

  1. /**
  2. * 注册使用的vo,使用JSR303校验
  3. */
  4. @Data
  5. public class UserRegisterVo {
  6. @NotEmpty(message = "用户名必须提交")
  7. @Length(min = 6, max = 19, message="用户名长度必须是6-18字符")
  8. private String userName;
  9. @NotEmpty(message = "密码必须填写")
  10. @Length(min = 6,max = 18,message = "密码长度必须是6—18位字符")
  11. private String password;
  12. @NotEmpty(message = "手机号必须填写")
  13. @Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "手机号格式不正确")
  14. private String phone;
  15. @NotEmpty(message = "验证码必须填写")
  16. private String code;
  17. }

异常结果封装+重定向(防刷+分布式session)

  1. @Controller
  2. public class RegisterController {
  3. @Autowired
  4. private StringRedisTemplate redisTemplate;
  5. @Autowired
  6. private MemberServiceClient memberServiceClient;
  7. /**
  8. * TODO 重定向携带数据,利用session原理。将数据放在session中。
  9. * 下一个页面使用session数据后,session就会失效
  10. * RedirectAttributes redirectAttributes:模拟重定向携带数据
  11. * TODO 分布式下的session问题
  12. *
  13. * @param userRegisterVo
  14. * @param bindingResult
  15. * @param redirectAttributes
  16. * @return
  17. */
  18. @PostMapping("/register")
  19. public String register(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult,
  20. // 重定向报错数据信息
  21. RedirectAttributes redirectAttributes) {
  22. if (bindingResult.hasErrors()) {
  23. Map<String, String> errors = bindingResult.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
  24. redirectAttributes.addFlashAttribute("errors", errors);
  25. // 校验出错,重定向
  26. return "redirect:http://auth.gulimalls.com/reg.html";
  27. }
  28. ...
  29. }
  30. }

验证码校验

  1. String code = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());
  2. if (!StringUtils.isEmpty(code)) {
  3. if (userRegisterVo.getCode().equals(code.split("_")[0])) {
  4. // 删除验证码
  5. redisTemplate.delete(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());
  6. // 远程调用注册服务
  7. UserRegisterTo userRegisterTo = new UserRegisterTo();
  8. BeanUtils.copyProperties(userRegisterVo, userRegisterTo);
  9. R r = memberServiceClient.register(userRegisterTo);
  10. if (r.getCode() == 0) {
  11. // 注册成功,回到登录页
  12. return "redirect:http://auth.gulimalls.com/login.html";
  13. } else {
  14. HashMap<String, String> errors = new HashMap<>();
  15. errors.put("msg", r.getData(new TypeReference<String>() {
  16. }));
  17. redirectAttributes.addFlashAttribute("errors", errors);
  18. return "redirect:http://auth.gulimalls.com/reg.html";
  19. }
  20. } else {
  21. HashMap<String, String> errors = new HashMap<>();
  22. errors.put("code", "验证码错误");
  23. redirectAttributes.addFlashAttribute("errors", errors);
  24. return "redirect:http://auth.gulimalls.com/reg.html";
  25. }
  26. } else {
  27. HashMap<String, String> errors = new HashMap<>();
  28. errors.put("code", "验证码错误");
  29. redirectAttributes.addFlashAttribute("errors", errors);
  30. return "redirect:http://auth.gulimalls.com/reg.html";
  31. }

认证模块_注册接口

  1. @Controller
  2. public class RegisterController {
  3. @Autowired
  4. private StringRedisTemplate redisTemplate;
  5. @Autowired
  6. private MemberServiceClient memberServiceClient;
  7. /**
  8. * TODO 重定向携带数据,利用session原理。将数据放在session中。
  9. * 下一个页面使用session数据后,session就会失效
  10. * RedirectAttributes redirectAttributes:模拟重定向携带数据
  11. * TODO 分布式下的session问题
  12. *
  13. * @param userRegisterVo
  14. * @param bindingResult
  15. * @param redirectAttributes
  16. * @return
  17. */
  18. @PostMapping("/register")
  19. public String register(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult,
  20. // 重定向报错数据信息
  21. RedirectAttributes redirectAttributes) {
  22. if (bindingResult.hasErrors()) {
  23. Map<String, String> errors = bindingResult.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
  24. redirectAttributes.addFlashAttribute("errors", errors);
  25. // 校验出错,重定向
  26. return "redirect:http://auth.gulimalls.com/reg.html";
  27. }
  28. String code = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());
  29. if (!StringUtils.isEmpty(code)) {
  30. if (userRegisterVo.getCode().equals(code.split("_")[0])) {
  31. // 删除验证码
  32. redisTemplate.delete(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());
  33. // 远程调用注册服务
  34. UserRegisterTo userRegisterTo = new UserRegisterTo();
  35. BeanUtils.copyProperties(userRegisterVo, userRegisterTo);
  36. R r = memberServiceClient.register(userRegisterTo);
  37. if (r.getCode() == 0) {
  38. // 注册成功,回到登录页
  39. return "redirect:http://auth.gulimalls.com/login.html";
  40. } else {
  41. HashMap<String, String> errors = new HashMap<>();
  42. errors.put("msg", r.getData(new TypeReference<String>() {
  43. }));
  44. redirectAttributes.addFlashAttribute("errors", errors);
  45. return "redirect:http://auth.gulimalls.com/reg.html";
  46. }
  47. } else {
  48. HashMap<String, String> errors = new HashMap<>();
  49. errors.put("code", "验证码错误");
  50. redirectAttributes.addFlashAttribute("errors", errors);
  51. return "redirect:http://auth.gulimalls.com/reg.html";
  52. }
  53. } else {
  54. HashMap<String, String> errors = new HashMap<>();
  55. errors.put("code", "验证码错误");
  56. redirectAttributes.addFlashAttribute("errors", errors);
  57. return "redirect:http://auth.gulimalls.com/reg.html";
  58. }
  59. }
  60. }

远程调用注册

在member工程中与数据库交互,进行用户注册。

controller

  1. @PostMapping("/register")
  2. public R register(@RequestBody UserRegisterTo userRegisterTo) {
  3. try {
  4. memberService.register(userRegisterTo);
  5. } catch (PhoneExistException exception) {
  6. R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(), BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());
  7. } catch (UsernameExistException exception) {
  8. R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(), BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());
  9. }
  10. return R.ok();
  11. }

MD5加密

1.可逆加密与不可逆加密 可逆加密:通过密文根据算法可以推算出明文 不可逆加密:无法推算出明文 2.彩虹表 暴力破解所有值的MD5值存储到数据库,然后存储一个映射表,该映射表称为彩虹表 3.不可逆加密实现方法:MD5、MD5盐值加密 ======================================================================== MD5:信息摘要算法,只要一个字节发生变化,结果值就会变化 - Message Digest algorithm 5,信息摘要算法 ·压缩性:任意长度的数据,算出的MD5值长度都是固定的。 ·容易计算:从原数据计算出MD5值很容易。 ·抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。 ·强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。 ·不可逆 【百度网盘秒传功能:计算文件MD5值,如果在百度的服务器里能找到一个一模一样的,就可以使用这个】 ======================================================================== MD5盐值加密:【明文相同,盐值不同密文也不同,增加了彩虹表的难度】 ·通过生成随机数与MD5生成字符串进行组合 ·数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可 ========================================================================

案例

  1. ========================================================================
  2. MD5案例:
  3. String s = DigestUtils.md5Hex("123456");// e10adc3949ba59abbe56e057f20f883e
  4. System.out.println(s);
  5. ========================================================================
  6. MD5盐值案例:
  7. System.out.println(Md5Crypt.md5Crypt("123456".getBytes()));// 随机盐值,随机MD5值:【盐值:USI.JoH2】【MD5值:$1$USI.JoH2$6hK88QXt9ijipsa/VcnbR0】
  8. System.out.println(Md5Crypt.md5Crypt("123456".getBytes()));// 随机盐值,随机MD5值:【盐值:tCYQRfTB】【MD5值:$1$tCYQRfTB$thopJ/8DcRSObDwXuKxvn1】
  9. System.out.println(Md5Crypt.md5Crypt("123456".getBytes(), "$1$123"));// 固定盐值,固定MD5值:【盐值:123】【MD5值:$1$123$7mft0jKnzzvAdU4t0unTG1】
  10. System.out.println(Md5Crypt.md5Crypt("123456".getBytes(), "$1$123"));// 固定盐值,固定MD5值:【盐值:123】【MD5值:$1$123$7mft0jKnzzvAdU4t0unTG1】
  11. ========================================================================
  12. 使用springMD5+随机盐方法生成密文:
  13. BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
  14. String encodedPassword1 = passwordEncoder.encode("123456");//$2a$10$s0yQ/Tz1aiexGqQGBNgmDuUFpCPjMx8L7TvJ60i9mQSBEmNXbSFEO
  15. String encodedPassword2 = passwordEncoder.encode("123456");//$2a$10$eXhMUTIjoS4cpCB3FRjhlu0QYGwTRgh93CefQSk48hPpvQzzDAvIS
  16. System.out.println(passwordEncoder.matches("123456", encodedPassword1));// 校验结果true
  17. System.out.println(passwordEncoder.matches("123456", encodedPassword2));// 校验结果true
  18. ========================================================================

由于可以被暴力破解,所以采用盐值加密。

盐值加密

认证功能 - 图1

  1. @Override
  2. public void register(UserRegisterTo userRegisterTo) {
  3. MemberEntity memberEntity = new MemberEntity();
  4. // 设置默认等级
  5. MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
  6. memberEntity.setLevelId(memberLevelEntity.getId());
  7. // 检查手机号和用户名是否唯一
  8. this.existPhone(userRegisterTo.getPhone());
  9. this.existUsername(userRegisterTo.getUserName());
  10. memberEntity.setMobile(userRegisterTo.getPhone());
  11. memberEntity.setUsername(userRegisterTo.getUserName());
  12. memberEntity.setNickname(userRegisterTo.getUserName());
  13. // 存储密码,使用盐值加密(BCryptPasswordEncoder提供的类)
  14. BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
  15. String encode = bCryptPasswordEncoder.encode(userRegisterTo.getPassword());
  16. memberEntity.setPassword(encode);
  17. // TODO 其他信息
  18. baseMapper.insert(memberEntity);
  19. }

普通登录

接口

  1. @PostMapping("login")
  2. public String login(UserLoginVo userLoginVo, RedirectAttributes redirectAttributes, HttpSession session) {
  3. // 远程调用
  4. UserLoginTo userLoginTo = new UserLoginTo();
  5. BeanUtils.copyProperties(userLoginVo, userLoginTo);
  6. R r = memberServiceClient.login(userLoginTo);
  7. if (r.getCode() == 0) {
  8. // 登录成功
  9. MemberRespVo memberRespVo = r.getData("data", new TypeReference<MemberRespVo>() {
  10. });
  11. session.setAttribute(AuthConstant.LOGIN_USER, memberRespVo);
  12. return "redirect:http://gulimalls.com";
  13. } else {
  14. Map<String, String> errors = new HashMap<>();
  15. errors.put("msg", r.getData(new TypeReference<String>() {
  16. }));
  17. redirectAttributes.addFlashAttribute("errors", errors);
  18. return "redirect:http://auth.gulimalls.com/login.html";
  19. }
  20. }

远程调用member模块登录

controller

  1. @PostMapping("/login")
  2. public R login(@RequestBody UserLoginTo userLoginTo) {
  3. MemberEntity memberEntity = memberService.login(userLoginTo);
  4. if (memberEntity == null) {
  5. R.error(BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getCode(), BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getMsg());
  6. }
  7. return R.ok().setData(memberEntity);
  8. }

接口实现类

  1. @Override
  2. public MemberEntity login(UserLoginTo userLoginTo) {
  3. MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("username", userLoginTo.getLoginacct()).eq("phone", userLoginTo.getLoginacct()));
  4. if (memberEntity != null) {
  5. String password = userLoginTo.getPassword();
  6. BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
  7. String encode = bCryptPasswordEncoder.encode(password);
  8. if (encode.matches(memberEntity.getPassword())) {
  9. return memberEntity;
  10. } else {
  11. return null;
  12. }
  13. } else {
  14. // 登录失败
  15. return null;
  16. }
  17. }

社交登录

介绍

微博开放平台注册应用

开发平台首页:🔗

文档:🔗

接口:🔗

注册应用填写回调地址

认证功能 - 图2

流程

认证功能 - 图3

回调接口

  1. /**
  2. * 授权回调页
  3. *
  4. * @param code 根据code换取Access Token,且code只能兑换一次Access Token
  5. */
  6. @GetMapping("/oauth2.0/weibo/success")
  7. public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {
  8. // 1.根据code换取Access Token
  9. Map<String, String> headers = new HashMap<>();
  10. Map<String, String> querys = new HashMap<>();
  11. Map<String, String> map = new HashMap<>();
  12. map.put("client_id", "2516299543");
  13. map.put("client_secret", "58124c5db70121821d778d446af28096");
  14. map.put("grant_type", "authorization_code");
  15. map.put("redirect_uri", "http://auth.gulimalls.com/oauth2.0/weibo/success");
  16. map.put("code", code);
  17. HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", headers, querys, map);
  18. // 判断是否授权成功
  19. if (response.getStatusLine().getStatusCode() == 200) {
  20. // 获取到了 accesstoken(此时使用一个对象封装)
  21. String json = EntityUtils.toString(response.getEntity());
  22. WBSocialUserTo wbSocialUserTo = JSON.parseObject(json, WBSocialUserTo.class);
  23. // 远程调用登录功能
  24. R r = memberServiceClient.loginWB(wbSocialUserTo);
  25. if (r.getCode() == 0) {
  26. MemberRespVo memberRespVo = r.getData(new TypeReference<MemberRespVo>() {
  27. });
  28. log.info(memberRespVo.toString());
  29. session.setAttribute("loginUser", memberRespVo);
  30. // 跳回首页
  31. return "redirect:http://gulimalls.com";
  32. } else {
  33. // 登录失败,调回登录页
  34. return "redirect:http://auth.gulimalls.com/login.html";
  35. }
  36. } else {
  37. return "redirect:http://auth.gulimalls.com/login.html";
  38. }
  39. }

远程调用member模块处理登录

controller

  1. @PostMapping("/oauth2/login")
  2. public R loginWB(@RequestBody WBSocialUserTo wbSocialUserTo) {
  3. MemberEntity memberEntity = memberService.login(wbSocialUserTo);
  4. if (memberEntity == null) {
  5. R.error(BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getCode(), BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getMsg());
  6. }
  7. return R.ok().setData(memberEntity);
  8. }

实现类

  1. @Override
  2. public MemberEntity login(WBSocialUserTo wbSocialUserTo) {
  3. String uid = wbSocialUserTo.getUid();
  4. // 数据库中是否有用户
  5. MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
  6. if (memberEntity != null) {
  7. // 已注册
  8. MemberEntity entity = new MemberEntity();
  9. entity.setId(memberEntity.getId());
  10. // 更新token
  11. entity.setAccessToken(wbSocialUserTo.getAccess_token());
  12. entity.setExpiresIn(wbSocialUserTo.getExpires_in());
  13. baseMapper.updateById(memberEntity);
  14. memberEntity.setAccessToken(wbSocialUserTo.getAccess_token());
  15. memberEntity.setExpiresIn(wbSocialUserTo.getExpires_in());
  16. return memberEntity;
  17. } else {
  18. // 进行注册
  19. MemberEntity registerMember = new MemberEntity();
  20. try {
  21. Map<String, String> query = new HashMap<>();
  22. query.put("access_token", wbSocialUserTo.getAccess_token());
  23. query.put("uid", wbSocialUserTo.getUid());
  24. HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", new HashMap<String, String>(), query);
  25. if (response.getStatusLine().getStatusCode() == 200) {
  26. //查询成功
  27. String json = EntityUtils.toString(response.getEntity());
  28. JSONObject jsonObject = JSON.parseObject(json);
  29. String name = jsonObject.getString("name");
  30. String gender = jsonObject.getString("gender");
  31. String profileImageUrl = jsonObject.getString("profile_image_url");
  32. // 封装注册信息
  33. registerMember.setNickname(name);
  34. registerMember.setGender("m".equals(gender) ? 1 : 0);
  35. registerMember.setHeader(profileImageUrl);
  36. registerMember.setCreateTime(new Date());
  37. }
  38. } catch (Exception exception) {
  39. }
  40. // 不管是否出现异常,这三个字段都要保存
  41. registerMember.setSocialUid(wbSocialUserTo.getUid());
  42. registerMember.setAccessToken(wbSocialUserTo.getAccess_token());
  43. registerMember.setExpiresIn(wbSocialUserTo.getExpires_in());
  44. baseMapper.insert(registerMember);
  45. return registerMember;
  46. }
  47. }

Gitee登录

和weibo相差不大,略。

分布式Session共享问题

问题一:不能跨域名共享cookie

跨域情况下,cookie不共享

  1. 放大cookie的作用域
  2. 1.方法1:自己设置domain
  3. // 首次使用session时,spring会自动颁发cookie设置domain,所以这里手动设置cookie很麻烦,采用springsession的方式颁发父级域名的domain权限
  4. // Cookie cookie = new Cookie("JSESSIONID", loginUser.getId().toString());
  5. // cookie.setDomain("gulimall.com");
  6. // servletResponse.addCookie(cookie);
  7. 2.使用springsession设置domain放大作用域

问题二:集群下同一个服务不能跨JVM共享session

解决方式

认证功能 - 图4

session复制解决

认证功能 - 图5

客户端存储

认证功能 - 图6

hash一致性

认证功能 - 图7

问题三:分布式下不同服务共享session

统一存储

image.png

本项目使用springSession提供的session存储在redis中,解决session共享问题。

方案二:token令牌

使用redis共享存储 + springsecurity存token令牌,每个调用接口都带令牌访问

流程

查看文档

  1. 查看文档:
  2. https://docs.spring.io/spring-session/docs/2.2.1.RELEASE/reference/
  3. https://spring.io
  4. =》project
  5. =》springsession
  6. =》learn
  7. =》Reference Doc.
  8. =》3. Samples and Guides (Start Here)
  9. =》HttpSession with Redis Guide (Source 源码) + HttpSession with Redis Guide Guide 引导)

简介:【各模块都需要使用springsession】

  • 解决了子域cookie无法共享的问题,放大了cookie的作用域domain
  • 解决了跨JVM不能共享session的问题【问题2和问题3本质上是同一个问题,不同JVM不能共享session】,springsession采用redis统一存储的的方式解决了session共享问题

导入依赖

  1. <!--springsession解决session共享问题-->
  2. <dependency>
  3. <groupId>org.springframework.session</groupId>
  4. <artifactId>spring-session-data-redis</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-data-redis</artifactId>
  9. </dependency>

配置使用redis作为session存储

  1. spring:
  2. redis:
  3. host: 192.168.241.130
  4. port: 6379
  5. session:
  6. store-type: redis # 使用redis保存session
  7. timeout: 30m # 30分钟过期

此时授权回调页将信息保存在session中

  1. /**
  2. * 授权回调页
  3. *
  4. * @param code 根据code换取Access Token,且code只能兑换一次Access Token
  5. */
  6. @GetMapping("/oauth2.0/weibo/success")
  7. public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {
  8. // 1.根据code换取Access Token
  9. Map<String, String> headers = new HashMap<>();
  10. Map<String, String> querys = new HashMap<>();
  11. Map<String, String> map = new HashMap<>();
  12. map.put("client_id", "2516299543");
  13. map.put("client_secret", "58124c5db70121821d778d446af28096");
  14. map.put("grant_type", "authorization_code");
  15. map.put("redirect_uri", "http://auth.gulimalls.com/oauth2.0/weibo/success");
  16. map.put("code", code);
  17. HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", headers, querys, map);
  18. // 判断是否授权成功
  19. if (response.getStatusLine().getStatusCode() == 200) {
  20. // 获取到了 accesstoken(此时使用一个对象封装)
  21. String json = EntityUtils.toString(response.getEntity());
  22. WBSocialUserTo wbSocialUserTo = JSON.parseObject(json, WBSocialUserTo.class);
  23. // 远程调用登录功能
  24. R r = memberServiceClient.loginWB(wbSocialUserTo);
  25. if (r.getCode() == 0) {
  26. MemberRespVo memberRespVo = r.getData(new TypeReference<MemberRespVo>() {
  27. });
  28. session.setAttribute(AuthConstant.LOGIN_USER, memberRespVo);
  29. // 跳回首页
  30. return "redirect:http://gulimalls.com";
  31. } else {
  32. // 登录失败,调回登录页
  33. return "redirect:http://auth.gulimalls.com/login.html";
  34. }
  35. } else {
  36. return "redirect:http://auth.gulimalls.com/login.html";
  37. }
  38. }

注意此时session只是在一个服务的域名共享auth.gulimalls.com,所以此时要扩大范围,所以需要配置,同时也可以配置序列化存储的格式,配置为Json

  1. @Configuration
  2. public class GulimallSessionConfig {
  3. // 配置Session域名作用范围
  4. @Bean
  5. public CookieSerializer cookieSerializer() {
  6. DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
  7. defaultCookieSerializer.setDomainName("gulimalls.com");
  8. defaultCookieSerializer.setCookieName("GULISESSION");
  9. return defaultCookieSerializer;
  10. }
  11. // 使用fastjson的反序列化,存储json格式在redis中
  12. @Bean
  13. public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
  14. return new GenericJackson2JsonRedisSerializer();
  15. }
  16. }

MemberResponseVO实现序列化接口

原理:内存中的对象要序列化成一个二进制流 传输到 redis中存储

  1. public class MemberResponseVO implements Serializable
  1. 修改product模块gulimall首页,去除session中的loginUser
  2. <li>
  3. <a th:if="${session.loginUser != null}">欢迎, [[${session.loginUser.nickname}]]</a>
  4. <a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/login.html">你好,请登录</a>
  5. </li>

其他模块使用springSession也需要导包,配置。可以直接放在common中,但此项目还是分开。

SpringSession原理

认证功能 - 图9

过滤器使用了装饰者模式,封装原生的request和response

认证功能 - 图10