认证功能
发送短信验证码
发送短信验证码以前做过,使用腾讯云。在第三方服务项目中提供远程调用接口。
接口
@RestController@RequestMapping("/sms")public class MsgController {@Autowiredprivate MsgService msgService;@GetMapping("/sendMsg")public R sendMsg(@RequestParam("phone") String phone, @RequestParam("code") String code) {boolean b = msgService.sendMessage(phone, code);return b ? R.ok() : R.error();}}
实现类
@Servicepublic class MsgServiceImpl implements MsgService {@Overridepublic boolean sendMessage(String phone, String code) {try {// 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密// 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取Credential cred = new Credential(ConstantMsgUtil.SECRET_ID, ConstantMsgUtil.SECRET_KEY);// 实例化一个http选项,可选的,没有特殊需求可以跳过HttpProfile httpProfile = new HttpProfile();httpProfile.setEndpoint(ConstantMsgUtil.END_POINT);// 实例化一个client选项,可选的,没有特殊需求可以跳过ClientProfile clientProfile = new ClientProfile();clientProfile.setHttpProfile(httpProfile);// 实例化要请求产品的client对象,clientProfile是可选的SmsClient client = new SmsClient(cred, "ap-guangzhou", clientProfile);// 实例化一个请求对象,每个接口都会对应一个request对象SendSmsRequest req = new SendSmsRequest();// 国内短信需要加上86String[] phoneNumberSet1 = {"86" + phone};req.setPhoneNumberSet(phoneNumberSet1);req.setSmsSdkAppId(ConstantMsgUtil.APP_ID);req.setSignName(ConstantMsgUtil.SIGN_NAME);req.setTemplateId(ConstantMsgUtil.TEMPLATE_ID);String[] templateParamSet1 = {code};req.setTemplateParamSet(templateParamSet1);// 返回的resp是一个SendSmsResponse的实例,与请求对象对应SendSmsResponse resp = client.SendSms(req);// 输出json格式的字符串回包System.out.println(SendSmsResponse.toJsonString(resp));return true;} catch (TencentCloudSDKException e) {System.out.println(e.toString());return false;}}}
其他信息参照谷粒学院项目。
发送短信验证码接口
@GetMapping("/sms/sendCode")public R sendCode(@RequestParam("phone") String phone) {// TODO 接口防刷// 60s防止用户再次发送请求String redisCode = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + phone);if (!StringUtils.isEmpty(redisCode)) {long l = Long.parseLong(redisCode.split("_")[1]);if (System.currentTimeMillis() - l < 60000) { // 一分钟之内就不发送return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());}}// 根据时间生成验证码,存入redis需要带上时间,发短信不需要// String code = UUID.randomUUID().toString().substring(0, 4);String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));String codeToRedis = code + "_" + System.currentTimeMillis();// 存入redisredisTemplate.opsForValue().set(AuthConstant.SMS_CODE_CACHE_PREFIX + phone, codeToRedis, 10, TimeUnit.MINUTES);thirdPartyFeignService.sendMsg(phone, String.valueOf(code));return R.ok();}
注册
基本流程
参数校验
/*** 注册使用的vo,使用JSR303校验*/@Datapublic class UserRegisterVo {@NotEmpty(message = "用户名必须提交")@Length(min = 6, max = 19, message="用户名长度必须是6-18字符")private String userName;@NotEmpty(message = "密码必须填写")@Length(min = 6,max = 18,message = "密码长度必须是6—18位字符")private String password;@NotEmpty(message = "手机号必须填写")@Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "手机号格式不正确")private String phone;@NotEmpty(message = "验证码必须填写")private String code;}
异常结果封装+重定向(防刷+分布式session)
@Controllerpublic class RegisterController {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate MemberServiceClient memberServiceClient;/*** TODO 重定向携带数据,利用session原理。将数据放在session中。* 下一个页面使用session数据后,session就会失效* RedirectAttributes redirectAttributes:模拟重定向携带数据* TODO 分布式下的session问题** @param userRegisterVo* @param bindingResult* @param redirectAttributes* @return*/@PostMapping("/register")public String register(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult,// 重定向报错数据信息RedirectAttributes redirectAttributes) {if (bindingResult.hasErrors()) {Map<String, String> errors = bindingResult.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));redirectAttributes.addFlashAttribute("errors", errors);// 校验出错,重定向return "redirect:http://auth.gulimalls.com/reg.html";}...}}
验证码校验
String code = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());if (!StringUtils.isEmpty(code)) {if (userRegisterVo.getCode().equals(code.split("_")[0])) {// 删除验证码redisTemplate.delete(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());// 远程调用注册服务UserRegisterTo userRegisterTo = new UserRegisterTo();BeanUtils.copyProperties(userRegisterVo, userRegisterTo);R r = memberServiceClient.register(userRegisterTo);if (r.getCode() == 0) {// 注册成功,回到登录页return "redirect:http://auth.gulimalls.com/login.html";} else {HashMap<String, String> errors = new HashMap<>();errors.put("msg", r.getData(new TypeReference<String>() {}));redirectAttributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimalls.com/reg.html";}} else {HashMap<String, String> errors = new HashMap<>();errors.put("code", "验证码错误");redirectAttributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimalls.com/reg.html";}} else {HashMap<String, String> errors = new HashMap<>();errors.put("code", "验证码错误");redirectAttributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimalls.com/reg.html";}
认证模块_注册接口
@Controllerpublic class RegisterController {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate MemberServiceClient memberServiceClient;/*** TODO 重定向携带数据,利用session原理。将数据放在session中。* 下一个页面使用session数据后,session就会失效* RedirectAttributes redirectAttributes:模拟重定向携带数据* TODO 分布式下的session问题** @param userRegisterVo* @param bindingResult* @param redirectAttributes* @return*/@PostMapping("/register")public String register(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult,// 重定向报错数据信息RedirectAttributes redirectAttributes) {if (bindingResult.hasErrors()) {Map<String, String> errors = bindingResult.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));redirectAttributes.addFlashAttribute("errors", errors);// 校验出错,重定向return "redirect:http://auth.gulimalls.com/reg.html";}String code = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());if (!StringUtils.isEmpty(code)) {if (userRegisterVo.getCode().equals(code.split("_")[0])) {// 删除验证码redisTemplate.delete(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());// 远程调用注册服务UserRegisterTo userRegisterTo = new UserRegisterTo();BeanUtils.copyProperties(userRegisterVo, userRegisterTo);R r = memberServiceClient.register(userRegisterTo);if (r.getCode() == 0) {// 注册成功,回到登录页return "redirect:http://auth.gulimalls.com/login.html";} else {HashMap<String, String> errors = new HashMap<>();errors.put("msg", r.getData(new TypeReference<String>() {}));redirectAttributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimalls.com/reg.html";}} else {HashMap<String, String> errors = new HashMap<>();errors.put("code", "验证码错误");redirectAttributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimalls.com/reg.html";}} else {HashMap<String, String> errors = new HashMap<>();errors.put("code", "验证码错误");redirectAttributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimalls.com/reg.html";}}}
远程调用注册
在member工程中与数据库交互,进行用户注册。
controller
@PostMapping("/register")public R register(@RequestBody UserRegisterTo userRegisterTo) {try {memberService.register(userRegisterTo);} catch (PhoneExistException exception) {R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(), BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());} catch (UsernameExistException exception) {R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(), BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());}return R.ok();}
MD5加密
1.可逆加密与不可逆加密 可逆加密:通过密文根据算法可以推算出明文 不可逆加密:无法推算出明文 2.彩虹表 暴力破解所有值的MD5值存储到数据库,然后存储一个映射表,该映射表称为彩虹表 3.不可逆加密实现方法:MD5、MD5盐值加密 ======================================================================== MD5:信息摘要算法,只要一个字节发生变化,结果值就会变化 - Message Digest algorithm 5,信息摘要算法 ·压缩性:任意长度的数据,算出的MD5值长度都是固定的。 ·容易计算:从原数据计算出MD5值很容易。 ·抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。 ·强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。 ·不可逆 【百度网盘秒传功能:计算文件MD5值,如果在百度的服务器里能找到一个一模一样的,就可以使用这个】 ======================================================================== MD5盐值加密:【明文相同,盐值不同密文也不同,增加了彩虹表的难度】 ·通过生成随机数与MD5生成字符串进行组合 ·数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可 ========================================================================
案例
========================================================================MD5案例:String s = DigestUtils.md5Hex("123456");// e10adc3949ba59abbe56e057f20f883eSystem.out.println(s);========================================================================MD5盐值案例:System.out.println(Md5Crypt.md5Crypt("123456".getBytes()));// 随机盐值,随机MD5值:【盐值:USI.JoH2】【MD5值:$1$USI.JoH2$6hK88QXt9ijipsa/VcnbR0】System.out.println(Md5Crypt.md5Crypt("123456".getBytes()));// 随机盐值,随机MD5值:【盐值:tCYQRfTB】【MD5值:$1$tCYQRfTB$thopJ/8DcRSObDwXuKxvn1】System.out.println(Md5Crypt.md5Crypt("123456".getBytes(), "$1$123"));// 固定盐值,固定MD5值:【盐值:123】【MD5值:$1$123$7mft0jKnzzvAdU4t0unTG1】System.out.println(Md5Crypt.md5Crypt("123456".getBytes(), "$1$123"));// 固定盐值,固定MD5值:【盐值:123】【MD5值:$1$123$7mft0jKnzzvAdU4t0unTG1】========================================================================使用spring的MD5+随机盐方法生成密文:BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();String encodedPassword1 = passwordEncoder.encode("123456");//$2a$10$s0yQ/Tz1aiexGqQGBNgmDuUFpCPjMx8L7TvJ60i9mQSBEmNXbSFEOString encodedPassword2 = passwordEncoder.encode("123456");//$2a$10$eXhMUTIjoS4cpCB3FRjhlu0QYGwTRgh93CefQSk48hPpvQzzDAvISSystem.out.println(passwordEncoder.matches("123456", encodedPassword1));// 校验结果trueSystem.out.println(passwordEncoder.matches("123456", encodedPassword2));// 校验结果true========================================================================
由于可以被暴力破解,所以采用盐值加密。
盐值加密

@Overridepublic void register(UserRegisterTo userRegisterTo) {MemberEntity memberEntity = new MemberEntity();// 设置默认等级MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();memberEntity.setLevelId(memberLevelEntity.getId());// 检查手机号和用户名是否唯一this.existPhone(userRegisterTo.getPhone());this.existUsername(userRegisterTo.getUserName());memberEntity.setMobile(userRegisterTo.getPhone());memberEntity.setUsername(userRegisterTo.getUserName());memberEntity.setNickname(userRegisterTo.getUserName());// 存储密码,使用盐值加密(BCryptPasswordEncoder提供的类)BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();String encode = bCryptPasswordEncoder.encode(userRegisterTo.getPassword());memberEntity.setPassword(encode);// TODO 其他信息baseMapper.insert(memberEntity);}
普通登录
接口
@PostMapping("login")public String login(UserLoginVo userLoginVo, RedirectAttributes redirectAttributes, HttpSession session) {// 远程调用UserLoginTo userLoginTo = new UserLoginTo();BeanUtils.copyProperties(userLoginVo, userLoginTo);R r = memberServiceClient.login(userLoginTo);if (r.getCode() == 0) {// 登录成功MemberRespVo memberRespVo = r.getData("data", new TypeReference<MemberRespVo>() {});session.setAttribute(AuthConstant.LOGIN_USER, memberRespVo);return "redirect:http://gulimalls.com";} else {Map<String, String> errors = new HashMap<>();errors.put("msg", r.getData(new TypeReference<String>() {}));redirectAttributes.addFlashAttribute("errors", errors);return "redirect:http://auth.gulimalls.com/login.html";}}
远程调用member模块登录
controller
@PostMapping("/login")public R login(@RequestBody UserLoginTo userLoginTo) {MemberEntity memberEntity = memberService.login(userLoginTo);if (memberEntity == null) {R.error(BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getCode(), BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getMsg());}return R.ok().setData(memberEntity);}
接口实现类
@Overridepublic MemberEntity login(UserLoginTo userLoginTo) {MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("username", userLoginTo.getLoginacct()).eq("phone", userLoginTo.getLoginacct()));if (memberEntity != null) {String password = userLoginTo.getPassword();BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();String encode = bCryptPasswordEncoder.encode(password);if (encode.matches(memberEntity.getPassword())) {return memberEntity;} else {return null;}} else {// 登录失败return null;}}
社交登录
介绍
微博开放平台注册应用
开发平台首页:🔗
文档:🔗
接口:🔗
注册应用填写回调地址

流程

回调接口
/*** 授权回调页** @param code 根据code换取Access Token,且code只能兑换一次Access Token*/@GetMapping("/oauth2.0/weibo/success")public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {// 1.根据code换取Access TokenMap<String, String> headers = new HashMap<>();Map<String, String> querys = new HashMap<>();Map<String, String> map = new HashMap<>();map.put("client_id", "2516299543");map.put("client_secret", "58124c5db70121821d778d446af28096");map.put("grant_type", "authorization_code");map.put("redirect_uri", "http://auth.gulimalls.com/oauth2.0/weibo/success");map.put("code", code);HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", headers, querys, map);// 判断是否授权成功if (response.getStatusLine().getStatusCode() == 200) {// 获取到了 accesstoken(此时使用一个对象封装)String json = EntityUtils.toString(response.getEntity());WBSocialUserTo wbSocialUserTo = JSON.parseObject(json, WBSocialUserTo.class);// 远程调用登录功能R r = memberServiceClient.loginWB(wbSocialUserTo);if (r.getCode() == 0) {MemberRespVo memberRespVo = r.getData(new TypeReference<MemberRespVo>() {});log.info(memberRespVo.toString());session.setAttribute("loginUser", memberRespVo);// 跳回首页return "redirect:http://gulimalls.com";} else {// 登录失败,调回登录页return "redirect:http://auth.gulimalls.com/login.html";}} else {return "redirect:http://auth.gulimalls.com/login.html";}}
远程调用member模块处理登录
controller
@PostMapping("/oauth2/login")public R loginWB(@RequestBody WBSocialUserTo wbSocialUserTo) {MemberEntity memberEntity = memberService.login(wbSocialUserTo);if (memberEntity == null) {R.error(BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getCode(), BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getMsg());}return R.ok().setData(memberEntity);}
实现类
@Overridepublic MemberEntity login(WBSocialUserTo wbSocialUserTo) {String uid = wbSocialUserTo.getUid();// 数据库中是否有用户MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));if (memberEntity != null) {// 已注册MemberEntity entity = new MemberEntity();entity.setId(memberEntity.getId());// 更新tokenentity.setAccessToken(wbSocialUserTo.getAccess_token());entity.setExpiresIn(wbSocialUserTo.getExpires_in());baseMapper.updateById(memberEntity);memberEntity.setAccessToken(wbSocialUserTo.getAccess_token());memberEntity.setExpiresIn(wbSocialUserTo.getExpires_in());return memberEntity;} else {// 进行注册MemberEntity registerMember = new MemberEntity();try {Map<String, String> query = new HashMap<>();query.put("access_token", wbSocialUserTo.getAccess_token());query.put("uid", wbSocialUserTo.getUid());HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", new HashMap<String, String>(), query);if (response.getStatusLine().getStatusCode() == 200) {//查询成功String json = EntityUtils.toString(response.getEntity());JSONObject jsonObject = JSON.parseObject(json);String name = jsonObject.getString("name");String gender = jsonObject.getString("gender");String profileImageUrl = jsonObject.getString("profile_image_url");// 封装注册信息registerMember.setNickname(name);registerMember.setGender("m".equals(gender) ? 1 : 0);registerMember.setHeader(profileImageUrl);registerMember.setCreateTime(new Date());}} catch (Exception exception) {}// 不管是否出现异常,这三个字段都要保存registerMember.setSocialUid(wbSocialUserTo.getUid());registerMember.setAccessToken(wbSocialUserTo.getAccess_token());registerMember.setExpiresIn(wbSocialUserTo.getExpires_in());baseMapper.insert(registerMember);return registerMember;}}
Gitee登录
和weibo相差不大,略。
分布式Session共享问题
问题一:不能跨域名共享cookie
跨域情况下,cookie不共享
放大cookie的作用域1.方法1:自己设置domain// 首次使用session时,spring会自动颁发cookie设置domain,所以这里手动设置cookie很麻烦,采用springsession的方式颁发父级域名的domain权限// Cookie cookie = new Cookie("JSESSIONID", loginUser.getId().toString());// cookie.setDomain("gulimall.com");// servletResponse.addCookie(cookie);2.使用springsession设置domain放大作用域
问题二:集群下同一个服务不能跨JVM共享session
解决方式

session复制解决

客户端存储

hash一致性
问题三:分布式下不同服务共享session
统一存储

本项目使用springSession提供的session存储在redis中,解决session共享问题。
方案二:token令牌
使用redis共享存储 + springsecurity存token令牌,每个调用接口都带令牌访问
流程
查看文档
查看文档:https://docs.spring.io/spring-session/docs/2.2.1.RELEASE/reference/https://spring.io=》project=》springsession=》learn=》Reference Doc.=》3. Samples and Guides (Start Here)=》HttpSession with Redis Guide (Source 源码) + HttpSession with Redis Guide (Guide 引导)
简介:【各模块都需要使用springsession】
- 解决了子域cookie无法共享的问题,放大了cookie的作用域domain
- 解决了跨JVM不能共享session的问题【问题2和问题3本质上是同一个问题,不同JVM不能共享session】,springsession采用redis统一存储的的方式解决了session共享问题
导入依赖
<!--springsession解决session共享问题--><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
配置使用redis作为session存储
spring:redis:host: 192.168.241.130port: 6379session:store-type: redis # 使用redis保存sessiontimeout: 30m # 30分钟过期
此时授权回调页将信息保存在session中
/*** 授权回调页** @param code 根据code换取Access Token,且code只能兑换一次Access Token*/@GetMapping("/oauth2.0/weibo/success")public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {// 1.根据code换取Access TokenMap<String, String> headers = new HashMap<>();Map<String, String> querys = new HashMap<>();Map<String, String> map = new HashMap<>();map.put("client_id", "2516299543");map.put("client_secret", "58124c5db70121821d778d446af28096");map.put("grant_type", "authorization_code");map.put("redirect_uri", "http://auth.gulimalls.com/oauth2.0/weibo/success");map.put("code", code);HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", headers, querys, map);// 判断是否授权成功if (response.getStatusLine().getStatusCode() == 200) {// 获取到了 accesstoken(此时使用一个对象封装)String json = EntityUtils.toString(response.getEntity());WBSocialUserTo wbSocialUserTo = JSON.parseObject(json, WBSocialUserTo.class);// 远程调用登录功能R r = memberServiceClient.loginWB(wbSocialUserTo);if (r.getCode() == 0) {MemberRespVo memberRespVo = r.getData(new TypeReference<MemberRespVo>() {});session.setAttribute(AuthConstant.LOGIN_USER, memberRespVo);// 跳回首页return "redirect:http://gulimalls.com";} else {// 登录失败,调回登录页return "redirect:http://auth.gulimalls.com/login.html";}} else {return "redirect:http://auth.gulimalls.com/login.html";}}
注意此时session只是在一个服务的域名共享auth.gulimalls.com,所以此时要扩大范围,所以需要配置,同时也可以配置序列化存储的格式,配置为Json
@Configurationpublic class GulimallSessionConfig {// 配置Session域名作用范围@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();defaultCookieSerializer.setDomainName("gulimalls.com");defaultCookieSerializer.setCookieName("GULISESSION");return defaultCookieSerializer;}// 使用fastjson的反序列化,存储json格式在redis中@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer() {return new GenericJackson2JsonRedisSerializer();}}
MemberResponseVO实现序列化接口
原理:内存中的对象要序列化成一个二进制流 传输到 redis中存储
public class MemberResponseVO implements Serializable
修改product模块gulimall首页,去除session中的loginUser<li><a th:if="${session.loginUser != null}">欢迎, [[${session.loginUser.nickname}]]</a><a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/login.html">你好,请登录</a></li>
其他模块使用springSession也需要导包,配置。可以直接放在common中,但此项目还是分开。
SpringSession原理

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

