谷粒商城11——认证服务、短信验证、Gitee-OAuth 社交登录、分布式session

九、认证服务

新建一个认证服务模块,并注册到nacos

image-20220729135059035

  • 添加默认页面到templates/,添加静态资源到nginx/html/static

  • 配置本机域名映射

  • 配置网关路由

  • #将主机地址为auth.gulimall.com转发至gulimall-auth-server
    - id: gulimall_auth_host
      uri: lb://gulimall-auth-server
      predicates:
        - Host=auth.gulimall.com
    
  • 编写各个html之间的跳转逻辑

  • 利用SpringMVC的视图控制器完成页面跳转

  • @Configuration
    public class MyWebConfig implements WebMvcConfigurer {
    
    
        /**
         * 视图映射,控制页面跳转
         * @param registry
         */
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
    
            registry.addViewController("/login.html").setViewName("login");
            registry.addViewController("/reg.html").setViewName("reg");
    
        }
    }
    

1.短信验证码功能

前端点击自动倒计时六十秒、发送验证码按钮

<div class="register-box">
    <label class="other_label">验 证 码
        <input name=code maxlength="20" type="text" placeholder="请输入验证码" class="caa">
    </label>
    <a id="sendCode" class="">点击发送验证码</a>
    <div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors,'code')?errors.code:''):''}">
    </div>
</div>

$("#sendCode").click(function () {
		//如果有disabled,说明最近已经点过
		if($(this).hasClass("disabled")){

		}else {
			timeOutChangeStyle();
			//发送验证码
			var phone=$("#phoneNum").val();
			$.get("/sms/sendCode?phone="+phone,function (data){
				if (data.code!=0){
					alert(data.msg);
				}
			})
		}
	})

	let time = 60;
	function timeOutChangeStyle() {
		//开启倒计时后设置标志属性disable的
		$("#sendCode").attr("class", "disabled");
		if(time==0){
			$("#sendCode").text("点击发送验证码");
			time=60;
			$("#sendCode").attr("class", "");
		}else {
			$("#sendCode").text(time+"s后再次发送");
			time--;
			setTimeout("timeOutChangeStyle()", 1000);
		}
	}

2.短信验证模仿

没有备案的服务器,参考各种云api文档。这里简单写一下流程。

api-gateway-demo-sign-java/HttpUtils.java at master · aliyun/api-gateway-demo-sign-java (github.com)下载HttpUtils放到公共微服务模块

新建一个短信发送组件,可以配置短信发送的参数

package com.henu.soft.merist.thirdparty.component;

import com.henu.soft.merist.common.utils.HttpUtils;
import lombok.Data;
import org.apache.http.HttpResponse;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Controller;

import java.util.HashMap;
import java.util.Map;

@Data
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Controller
public class SmsComponent {

    private String host;
    private String path;
    private String appcode;

    public void sendCode(String phone,String code) {
//        String host = "http://dingxin.market.alicloudapi.com";
//        String path = "/dx/sendSms";
        String method = "POST";
//        String appcode = "你自己的AppCode";
        Map<String, String> headers = new HashMap<String, String>();
        //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
        headers.put("Authorization", "APPCODE " + appcode);
        Map<String, String> querys = new HashMap<String, String>();
        querys.put("mobile",phone);
        querys.put("param", "code:"+code);
        querys.put("tpl_id", "TP1711063");
        Map<String, String> bodys = new HashMap<String, String>();


        try {
            /**
             * 重要提示如下:
             * HttpUtils请从
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
             * 下载
             *
             * 相应的依赖请参照
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
             */
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
            System.out.println(response.toString());
            //获取response的body
            //System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

配置文件:

spring:
  cloud:
    alicloud:
      sms:
        host: http://dingxin.market.alicloudapi.com
        path: /dx/sendSms
        appcode: #

controller:

package com.henu.soft.merist.thirdparty.controller;

import com.henu.soft.merist.common.utils.R;
import com.henu.soft.merist.thirdparty.component.SmsComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/sms")
public class SmsSendController {
    
    @Autowired
    SmsComponent smsComponent;
    
    /**
     * 提供给别的服务进行调用
     * @param phone
     * @param code
     * @return
     */
    @ResponseBody
    @GetMapping(value = "/sendCode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) {

        //发送验证码
//        smsComponent.sendCode(phone,code);
        System.out.println(phone+code);
        return R.ok();
    }
}

3.验证码生成远程调用发送短信

由于验证码生成在认证模块,而短信发送验证码在第三方模块,所以需要feign远程调用

  • 导入open-feign的依赖,加上@EnableFeignClients 开启feign服务

  • 编写接口远程调用

  • package com.henu.soft.merist.auth.feign;
    
    import com.henu.soft.merist.common.utils.R;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @FeignClient("gulimall-third-party")
    public interface ThreadPartyFeignService {
    
        @ResponseBody
        @GetMapping("/sms/sendCode")
        public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
    }
    

由于向手机发送验证码服务是假的 所以这里在控制台输出

验证码生成+远程调用:

package com.henu.soft.merist.auth.controller;

import com.henu.soft.merist.auth.feign.ThreadPartyFeignService;
import com.henu.soft.merist.common.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.UUID;

@Controller
public class LoginController {

    @Autowired
    ThreadPartyFeignService threadPartyFeignService;

    /** 发送一个请求直接跳转到另一个页面
     * SpringMVC viewcontroller:将请求和页面映射过来
     */
    @GetMapping("/sms/sendCode")
    public R sendCode(@RequestParam("phone") String phone){
        String code = UUID.randomUUID().toString().substring(0, 5);
        //由于向手机发送验证码服务是假的 所以这里在控制台输出
        System.out.println("=====================code:"+code+"=====================");
        threadPartyFeignService.sendCode(phone, code);
        return R.ok();
    }
}

获取到验证码

image-20220729201131561

4.验证码防刷检验

这里要解决三个问题:

  1. 验证码的过期时间设置,同时完善注册时验证码的校验
  2. 验证码的路径是暴露的,会受到恶意人员不停地调用耗费资源
  3. 尽管发送验证码之后有60s的倒计时,但是刷新页面,重新输入手机号又可以发送验证码

image-20220729201714172

4.1 验证码的校验

这里由于验证码肯定不是永久有效的,所以将验证码存储到 redis 中。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

redis 配置

spring.redis.host=192.168.137.128
spring.redis.port=6379

设置验证码十分钟内有效

@RestController
public class LoginController {

    @Autowired
    ThreadPartyFeignService threadPartyFeignService;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    /** 发送一个请求直接跳转到另一个页面
     * SpringMVC viewcontroller:将请求和页面映射过来
     */
    @GetMapping("/sms/sendCode")
    public R sendCode(@RequestParam("phone") String phone){
        String code = UUID.randomUUID().toString().substring(0, 5);
        //由于向手机发送验证码服务是假的 所以这里在控制台输出
        System.out.println("=====================code:"+code+"=====================");

        //存储 手机号k + 验证码v 到redis  //public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
        //同时设置 在redis 中的过期时间  设置十分钟内有效
        stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,code,10,TimeUnit.MINUTES);
        threadPartyFeignService.sendCode(phone, code);
        return R.ok();
    }
}

4.2、4.3 验证码防刷

  • 为了防止验证码一直被刷,在 redis 存储的时候,加上时间
  • 每次获取验证码之前都要判断是否在 redis 中存在对应的key
    • 如果存在,判断是否过了60s,
      • 没过60s,提示60s后才能发送
      • 过了60s,发送验证码
    • 如果不存在,发送验证码

修改代码:

@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone){

    String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone);
    if (!StringUtils.isEmpty(redisCode)){
        long time = Long.parseLong(redisCode.split("_")[1]);
        if (System.currentTimeMillis() - time < 60000){
            return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
        }
    }

    String code = UUID.randomUUID().toString().substring(0, 5) + "_" + System.currentTimeMillis();
    //由于向手机发送验证码服务是假的 所以这里在控制台输出
    System.out.println("=====================code:"+code+"=====================");

    //存储 手机号k + 验证码v 到redis  //public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
    //同时设置 在redis 中的过期时间  设置十分钟内有效
    stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,code,10,TimeUnit.MINUTES);
    threadPartyFeignService.sendCode(phone, code);
    return R.ok();
}

另外就是,这里可以大量伪造手机号,进行访问,应该在获取验证码的时候加上图形验证。

5.注册功能

5.1 封装表单vo

新建一个 UserRegisterVo 封装提交表单的数据,同时进行参数校验。

使用@Long 校验长度,引入 hibernate.validator 参数校验。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.13.Final</version>
</dependency>

UserRegisterVo:

package com.henu.soft.merist.auth.vo;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

@Data
public class UserRegisterVo {
    @NotEmpty(message = "用户名必须提交")
    @Length(min = 6,max = 18,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 phoneNum;

    @NotEmpty(message = "用户名必须填写")
    private String code;
}

5.2 认证服务调用会员服务

校验验证码,并且远程调用会员服务进行注册(在数据库中插入数据)

@PostMapping("/register")
    public String register(@Valid UserRegisterVo vo, BindingResult result, RedirectAttributes redirectAttributes){
        if (result.hasErrors()){
            Map<String, String> errors = result.getFieldErrors().stream().collect(
                    Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage)
            );

//            model.addAttribute("errors",errors);
            redirectAttributes.addFlashAttribute("errors",errors);
            //校验出错,转发到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }

        //1.输入的验证码
        String code = vo.getCode();
        //获取存入redis中的验证码
        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
        if (!StringUtils.isEmpty(code)){
            if (code.equals(redisCode.split("_")[0])){
                //验证通过
                //删除验证码,令牌机制
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
                //进行真正的注册
                R register = memberFeignService.register(vo);
                if (register.getCode() == 0){
                    //成功
                    return "redirect:http://auth.gulimall.com/login.html";
                }else {
                    //失败
                    Map<String,String> errors = new HashMap<>();
                    TypeReference<String> typeReference = new TypeReference<String>(){};
                    String msg = register.getData(typeReference);
                    errors.put("msg",msg);
                    redirectAttributes.addFlashAttribute("errors",errors);
                    return "redirect:http://auth.gulimall.com/reg.html";
                }
            }
        }
        //输入验证码为空 or 校验验证码失败  走到这里
        //校验出错回到注册页面
        Map<String, String> errors = new HashMap<>();
        errors.put("code","验证码错误");
        redirectAttributes.addFlashAttribute("errors",errors);
        return "redirect:http://auth.gulimall.com/reg.html";

    }
@FeignClient("gulimall-member")
public interface MemberFeignService {
    @PostMapping("/register")
    public R register(@RequestBody UserRegisterVo userRegisterVo);
}

5.3 会员服务 注册

这一步包括 检验手机号、用户名是否被占用,密码MD5盐值加密,向数据库插入数据。

MD5&MD5盐值加密

  • MD5:
    • Message Digest algorithm 5,信息摘要算法
      • 压缩性:任意长度的数据,算出的MD5值长度都是固定的
      • 容易计算:从原数据计算出MD5值很容易
      • 抗修改性:对原数据进行任何改动,哪怕只修改一个字节,所得到的MD5值有很大区别
      • 强碰撞性:想找到两个不同的数据,使它们具有相同的MD5,是非常困难的
  • 加盐:
    • 通过生成随机数与 MD5 生化字符串进行组合
    • 数据库同时存储 MD5 值与 salt 值,验证正确性时用 salt 进行 MD5 即可
@Override
public void register(MemberRegisterVo vo) {
    //1.检查号码是否唯一
    checkPhoneUnique(vo.getPhone());
    //2.检查用户名是否唯一
    checkUserNameUnique(vo.getUserName());
    //3.信息唯一 进行插入
    MemberEntity memberEntity = new MemberEntity();
    //3.1 保存基本信息
    memberEntity.setUsername(vo.getUserName());
    memberEntity.setMobile(vo.getPhone());
    memberEntity.setPassword(vo.getPassword());
    //3.2 加密保存密码
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String encodePassword = passwordEncoder.encode(vo.getPassword());
    memberEntity.setPassword(encodePassword);
    //3.3 设置会员默认等级
    //3.3.1 找到会员默认等级
    MemberLevelEntity defaultLevel = memberLevelService.getOne(
            new QueryWrapper<MemberLevelEntity>().eq("default_status", 1));
    //3.3.2 设置会员等级为默认
    memberEntity.setLevelId(defaultLevel.getId());
    //4. 保存用户信息
    this.save(memberEntity);
}

private void checkPhoneUnique(String phone) throws PhoneNumExistException{
    Long count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
    if (count > 0){
        throw new PhoneNumExistException();
    }
}
private void checkUserNameUnique(String username) throws UserNameExistException{
    Long count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
    if (count > 0){
        throw new UserNameExistException();
    }
}

6.登录功能

6.1 普通账号密码登录

认证模块:

controller:

@PostMapping("/login")
public String login(UserLoginVo loginVo, RedirectAttributes redirectAttributes, HttpSession session){
    R login = memberFeignService.login(loginVo);
    if (login.getCode() == 0){
        String jsonString = JSON.toJSONString(login.get("memberEntity"));
        MemberResponseTo memberResponseTo = JSON.parseObject(jsonString, new TypeReference<MemberResponseTo>() {
        });
        //public static final String LOGIN_USER = "loginUser";
        session.setAttribute(AuthServerConstant.LOGIN_USER,memberResponseTo);
        return "redirect:http://gulimall.com";
    }else {
        String msg = (String) login.get("msg");
        Map<String,String> errors = new HashMap<>();
        errors.put("msg",msg);
        redirectAttributes.addFlashAttribute("errors",errors);
        return "redirect:http://auth.gulimall.com/login.html";
    }
}

feign:

@FeignClient("gulimall-member")
public interface MemberFeignService {
    @PostMapping("/member/member/register")
    public R register(@RequestBody UserRegisterVo userRegisterVo);

    @PostMapping("/member/member//login")
    public R login(@RequestBody UserLoginVo userLoginVo);
}

member 会员模块:

controller:

/**
 * 登录
 */
@PostMapping("/login")
public R login(@RequestBody MemberLoginVo memberLoginVo){
    MemberEntity entity = new MemberEntity();
    try {
        entity = memberService.login(memberLoginVo);
    }catch (PasswordWrongException e){
        // PASSWORD_WRONG_EXCEPTION(15004,"密码错误");
        return R.error(BizCodeEnume.PASSWORD_WRONG_EXCEPTION.getCode(), BizCodeEnume.PASSWORD_WRONG_EXCEPTION.getMsg());
    }
    return entity == null ?
        // USER_NOT_EXIST_EXCEPTION(15003,"用户不存在"),
     	R.error(BizCodeEnume.USER_NOT_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_NOT_EXIST_EXCEPTION.getMsg())
        : R.ok().put("memberEntity",entity);
}

serviceImpl:

@Override
public MemberEntity login(MemberLoginVo vo) {
    //账号:手机号 or 用户名
    String account = vo.getAccount();
    MemberEntity memberEntity = this.getOne(new QueryWrapper<MemberEntity>().eq("username", account).or().eq("mobile", account));
    if (memberEntity != null){
        checkPassword(memberEntity,vo.getPassword());
        return memberEntity;
    }
    return null;
}

@Override
public void checkPassword(MemberEntity memberEntity, String password) throws PasswordWrongException {
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    if (!passwordEncoder.matches(password,memberEntity.getPassword())){
        throw new PasswordWrongException();
    }
}

6.2 OAuth2.0-社交登录

参考博客:OAuth2.0 社交登录-Gitee springboot项目整合(微服务分布式)完整代码包括 数据库、前、后端_HotRabbit.的博客-CSDN博客

6.3 分布式Session -Spring Sesion原理、实战

参考博客:分布式session ——Spring Session原理、实战解决 子域之间的 session 共享问题、不同服务器 session 共享解决方案_HotRabbit.的博客-CSDN博客

7.单点登录 SSO

核心

  • 三个系统即使域名不同,也要给三个系统同步同一认证状态
  • 中央认证服务器、其他系统想要登录去中央服务器登录,登录成功之后跳转回来

image-20220803134219056

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HotRabbit.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值