微服务登陆注册认证另一种方案&分布式session

在这里插入图片描述

一。Auth服务

1.1LoginController(&验证码&注册&账密登录)

@Controller
public class LoginController {

    @Autowired
    ThirdPartFeignService thirdPartFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    MemberFeignService  memberFeignService;

    /**
     * 发送验证码
     */
    @ResponseBody
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone){
        /**
         * 1.接口防刷,防止同一个phone60秒内重复再次发送验证码
         * 2.验证码的再次校验,存redis,因为这个验证码不是永远存储的
         * 2.1.rediskey是一定包含手机号的,值是验证码
         * 2.2.如果是60秒以后,那么再发新的验证码
         *
         * 验证码发送频率过多的解决方案:
         * 1.在保存验证码的时候,再保存给当前验证码设置的系统时间,
         *          1.1.只要你想要发送验证码,按照redis里面的看有没有,
         *          1.2.如果有了,再看一下这个时间,如果这个时间还在60秒以内的,那就60以后再试
         */
        //TODO 1、接口防刷。
        String rediscode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if (!StringUtils.isEmpty(rediscode)) { //如果从redis当中拿到的验证码不为空
            long l = Long.parseLong(rediscode.split("_")[1]);//拿到redis存的时间
            //和当前系统的时间进行比较
            if (System.currentTimeMillis() -l <60000){ //60秒内不能再发验证码
                return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
            }
        }
        String code = UUID.randomUUID().toString().substring(0, 5);
        String substring = code+"_"+System.currentTimeMillis();  //+时间为了接口防刷操作
//redis是因为不能保证第三方接口一定发送了短信
        redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,substring,10, TimeUnit.MINUTES);
        thirdPartFeignService.sendCode(phone,code);
        return R.ok();
    }

    /**
     * 注册的控制器
     * RedirectAttributes 模拟重定向携带数据
     *  其原理:
     *      利用session原理,将数据放在session中,然后重定向到页面之后,再从session中取出来
     *      跳到下一个页面,然后取出数据以后,session里面的数据就会删掉,
     *      @Valid 数据校验
     *      BindingResult :校验结果
     */
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes){
        if (result.hasErrors()){ //如果校验出错
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,FieldError::getDefaultMessage));
          //存放错误消息,让前端感知并获取错误信息
            redirectAttributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/reg.html";//注册页
        }
        //1、校验验证码,从页面提交过来的验证码
        String code = vo.getCode();
        //获取到redis存的验证码
        String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
        if (!StringUtils.isEmpty(s)){ //redis里面存了这个手机号对应的验证码
            //,验证码正确,就进行截串,拿到第一个数据然后进行对比 redis存的s还带了系统时间,so分割
            if (code.equals(s.split("_")[0])){
                    //验证码通过以后,还必须要删除验证码,因为这样下次若还带着之前的验证码过来,就会验证失败
                    redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
                //验证码通过,  如果上面校验并无出错,就进行注册 ,调用远程服务gulimall-member进行注册
                R r = memberFeignService.regist(vo);
                if (r.getCode() == 0){ //成功就返回登陆页
                    return "redirect:http://auth.gulimall.com/login.html";
                }else { //失败就返回注册页,并且显示错误消息
                    Map<String, String> errors = new HashMap<>();
                    errors.put("msg",r.getData("msg ",new TypeReference<String>(){}));
                    redirectAttributes.addFlashAttribute("errors",errors);
                    return "redirect:http://auth.gulimall.com/reg.html";//注册页
                }
            }else { //验证不通过
                Map<String, String> errors = new HashMap<>();
                errors.put("code","验证码错误");
                //存放错误消息,让前端感知并获取错误信息
                redirectAttributes.addFlashAttribute("errors",errors);
                return "redirect:http://auth.gulimall.com/reg.html";//注册页
            }
        }else {  //redis存的验证码已过期
            Map<String, String> errors = new HashMap<>();
            errors.put("code","验证码错误");
            //存放错误消息,让前端感知并获取错误信息
            redirectAttributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/reg.html";//注册页
        }
    }

    /**
     * 账号密码登录请求
     * 提交的不是json数据,是K,V型数据,so不用加requestBody
     */
    @PostMapping("/login")
    public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session){
        //调用远程gulimall-member进行登录
        R login = memberFeignService.login(vo);
        if (login.getCode() == 0){
            //成功登录
            //提取出用户信息,
            MemberRespVo data = login.getData("data", new TypeReference<MemberRespVo>() {
            });
            //然后将提取出来的用户信息放到sessionsession.setAttribute(AuthServerConstant.LOGIN_USER,data);
            return "redirect:http://gulimall.com";
        }else {
            Map<String,String> errors = new HashMap<>();
            errors.put("msg",login.getData("msg",new TypeReference<String>(){}));//把错误消息放到mapredirectAttributes.addFlashAttribute("errors",errors); //提取出错误消息
            //失败就返回登录页
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }
    /**
     * 如果已经登录成功后,再点击登录页不用再登录,直接跳转到商城首页
     */
    @GetMapping("/login.html")
    public String loginPage(HttpSession session){
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);//拿到已登录的用户信息
        if (attribute==null){ //没登录,返回登录页
            return "login";
        }else { //登录了就直接跳回首页就行了
            return "redirect:http://gulimall.com";
        }

    }

}

Oauth2controller(微博登录)

@Controller
public class OAuth2Controller {

    @Autowired
    MemberFeignService memberFeignService;


    /**
     * 微博的成功登录后的回调方法
     */
    @GetMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code ,HttpSession session) throws Exception {
        Map<String,String> header = new HashMap<>();
        Map<String,String> query = new HashMap<>();
        //换取access Tokenmap里面封装数据
        Map<String,String> map = new HashMap<>();//封装数据用的
        map.put("","");//应用的id
        map.put("","");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");
        map.put("code",code);//社交登录一登录成功,code就会过来了,然后就去换取access Token
        //1.根据code码,换取一个access Token,只要能够换取access Token,就说明登录成功
        //方法参数解释:第一个参数:主机地址,,第二个参数:请求路径       第三个参数,请求方式      第四个参数 请求头   第五个参数 查询参数    第六个参数:请求体
        //执行成功这个doPost方法,就会有响应的数据
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", header, query, map);
        //解析响应的数据,处理access Token
       if(response.getStatusLine().getStatusCode()==200){  //获取响应状态行以及响应状态码,并且是判断是否换取成功
          //如果是200,那么就获取到了access Token
           String json = EntityUtils.toString(response.getEntity());//获取到响应体内容
           //将响应体的内容转换成对应的对象
           SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
           //拿到了access Token,并且还转换了响应的对象了,那么就是知道了当前是哪个社交用户登录成功
           //真正是不是登录成功,还得分一些情况
           // 1.如果当前用户如果是第一次进gulimall网站,就自动注册进来,(为当前这个社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员)
           //使用社交账号对数据库的用户表ums_member进行关联
           //如果这个社交用户,从来没有注册过gulimall网站,就进行注册,如果注册了就查出这个用户的整个详细信息
           //远程调用用户服务,接收socialUser这个社交用户,以此来判断是登录还是自动注册这个社交用户,相当于用id关联上某个本系统的用户信息,
           R oauthlogin = memberFeignService.oauthlogin(socialUser);
           if(oauthlogin.getCode() == 0){ //说明是成功的
                //提取登陆成功后的用户信息
               MemberRespVo data = oauthlogin.getData("data", new TypeReference<MemberRespVo>() {
               });
               log.info("登录成功:用户信息: {}",data.toString());
               /**
                * 第一次使用session,就命令浏览器保存相应的卡号,
                * 以后浏览器访问哪个网站就会带上这个网站的cookie
                * 子域之间:gulimall,auth.gulimall.com* 那么发卡的时候(指定域名为父域名),即使是子域系统发的卡,也能让父域直接使用
                */
               //TODO 1.默认发的令牌。keysession,值是唯一字符串。但是作用域是当前域,那么当前域解决不了session共享的问题,解决子域session共享问题
               //TODO 2.使用JSON的序列化方式来序列化对象数据到redissession.setAttribute("loginUser",data);
               //2.换取成功access Token,也即登录成功就跳到我们应用的首页
               return "redirect:http://gulimall.com";
           }else {//否则就是失败的
               return "redirect:http://auth.gulimall.com/login.html";//重定向到登录页,进行重新登录
           }
       }else {  //不成功,没有获取到access Token
           return "redirect:http://auth.gulimall.com/login.html";//重定向到登录页,进行重新登录
       }
    }

}

1.2Feign远程接口

MemberFeignService

@FeignClient("gulimall-member")
public interface MemberFeignService {

    @PostMapping("/member/member/regist")
    public R regist(@RequestBody UserRegistVo vo);

    @PostMapping("/member/member/login")
    public R login(@RequestBody UserLoginVo vo);
    
    @PostMapping("/member/member/oauth2/login")
    public R oauthlogin(@RequestBody SocialUser socialUser) throws Exception;
}

ThirdPartFeignService:

@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {

    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}

1.3entity:

UserregistryVo

public class UserRegistVo {

    @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 phone;

    @NotEmpty(message = "验证码必须填写")
    private String code;
}

SocialUser:

public class SocialUser {

    private String access_token;
    private String remind_in;
    private long expires_in;
    private String uid;
    private String isRealName;
}

UserLoginVo略

二。Member服务

2.1 MemberController登陆注册方法

 /**
     * 社交登录控制器
     * @return
     */
    @PostMapping("/oauth2/login")
    public R oauthlogin(@RequestBody SocialUser socialUser) throws Exception {
        MemberEntity entity = memberService.login(socialUser);
        if (entity!=null){ //登录成功
            return R.ok().setData(entity);
        }else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }
    }


    /**
     * 会员登录控制器
     * @return
     */
    @PostMapping("/login")
    public R login(@RequestBody MemberLoginVo vo){
      MemberEntity entity = memberService.login(vo);
      if (entity!=null){ //登录成功
          return R.ok().setData(entity);
      }else {
          return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
      }
    }


    /**
     * 注册会员控制器
     */
    @PostMapping("/regist")
    public R  regist(@RequestBody MemberRegistVo vo){ //请求体的json转成MemberLoginVo对象
        try {
            memberService.regist(vo);
        }catch (PhoneExistException e){
            return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
        }catch (UsernameExistException e){
            return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
        }

        return R.ok();
    }

2.2MemberServiceImpl登陆注册实现

public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {

    @Autowired
    MemberLevelDao memberLevelDao;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<MemberEntity> page = this.page(
                new Query<MemberEntity>().getPage(params),
                new QueryWrapper<MemberEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public void regist(MemberRegistVo vo) {
        MemberDao memberDao = this.baseMapper;
        MemberEntity entity = new MemberEntity();
        MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();//获取默认等级信息
        entity.setLevelId(levelEntity.getId());//设置默认等级
        //检查用户名邮箱和手机号是否唯一,让controller感知异常,此方法中设置异常机制
        checkPhoneUnique(vo.getPhone());
        checkUsernameUnique(vo.getUserName());

        entity.setMobile(vo.getPhone()); //设置手机号
        entity.setUsername(vo.getUserName());//设置用户名
        entity.setNickname(vo.getUserName());
        //密码加密才能存储
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());//原密码加密
        entity.setPassword(encode);

        memberDao.insert(entity);
    }

    /**
     * 检查手机号是否唯一 ,★异常记得在接口声明
     *
     * @param phone
     * @throws PhoneExistException
     */
    @Override
    public void checkPhoneUnique(String phone) throws PhoneExistException {
        MemberDao memberDao = this.baseMapper;
        Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (mobile > 0) { //说明数据库有这条记录,就来抛异常
            throw new PhoneExistException();
        }
    }

    /**
     * 检查用户名是否唯一
     *
     * @param username
     * @throws UsernameExistException
     */
    @Override
    public void checkUsernameUnique(String username) throws UsernameExistException {
        MemberDao memberDao = this.baseMapper;
        Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
        if (count > 0) { //说明数据库有这条记录,就来抛异常
            throw new UsernameExistException();
        }
    }

    /**
     * 登录逻辑
     *
     * @param vo
     * @return
     */
    @Override
    public MemberEntity login(MemberLoginVo vo) {
        String loginacct = vo.getLoginacct();
        String password = vo.getPassword();

        //1.去数据库查询数据
        MemberDao memberDao = this.baseMapper;
        MemberEntity entity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct).or().eq("mobile", loginacct));
        if (entity == null) {  //数据库没有这个账号,登录失败
            return null;
        } else {
            //获取数据库的密码字段
            String passwordDb = entity.getPassword();
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            boolean matches = passwordEncoder.matches(password, passwordDb);//密码比较和匹配,方法的第一个参数是要求传明文的密码,第二个参数是传加密后的密码
            if (matches) {//如果匹配成功,就说明登录成功了
                return entity;//把当前的用户返回
            } else { //如果匹配不成功,就说明登录失败了
                return null;
            }
        }
    }


    /**
     * 使用社交账号进行登录
     * 没登陆过就进行注册,注册的目的就是为了保存当前这个社交用户,在我们数据库对应的哪个用户id
     *
     * @param socialUser
     * @return
     */
    @Override
    public MemberEntity login(SocialUser socialUser) throws Exception {
        //登录和注册合并
        //首先判断这个用户到低以前登录过没
        String uid = socialUser.getUid();//社交账号当前登录这个网站的微博id
        //1.根据uid来判断当前社交用户是否已经登录过系统
        MemberDao memberDao = this.baseMapper;
        MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
        if (memberEntity != null) { //说明这个用户已经注册过的
            //一旦如果是已经注册过的,那么就更新令牌以及更新时间
            MemberEntity update = new MemberEntity();
            update.setId(memberEntity.getId());
            update.setAccessToken(socialUser.getAccess_token());
            update.setExpiresIn(socialUser.getExpires_in());
            memberDao.updateById(update);
            memberEntity.setAccessToken(socialUser.getAccess_token());
            memberEntity.setExpiresIn(socialUser.getExpires_in());
            return memberEntity;
        } else { //说明这个用户还没有注册过的
            //若本商城系统没有查到当前社交的用户对应的记录,就需要注册
            MemberEntity regist = new MemberEntity();
            //todo 即使远程查询出现有问题了也没关系,也不用管 ???
            try {
                //查询当前社交用户的社交账号信息(昵称,性别等信息)
                Map<String, String> query = new HashMap<>();//用于封装查询参数的
                query.put("access_token", socialUser.getAccess_token());
                query.put("uid", socialUser.getUid());
                //查出用户的详细信息
                HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", 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");//获取当前微博登录成功后的性别
                    //进行注册
                    regist.setNickname(name); //设置默认的昵称
                    regist.setGender("m".equals(gender) ? 1 : 0);
                }
            } catch (Exception e) {

            }
            //以后只要社交用户是第一次登陆,那么数据库里面就会有一条记录,那么相当于它就是注册进来了
            //这三条数据无论是否远程查询出问题都要设置??????
            regist.setSocialUid(socialUser.getUid());//设置当前微博登录成功后的Uid
            regist.setAccessToken(socialUser.getAccess_token());//设置当前登录后的令牌
            regist.setExpiresIn(socialUser.getExpires_in());//设置当前登录后的令牌过期时间
            //插入数据
            memberDao.insert(regist);
            //插入数据成功以后,说明这个用户也就登陆成功了
            return regist;
        }

    }
}

三。第三方短信服务略

四 分布式session(跨域共享session)

4.1session基本原理:

在这里插入图片描述
类似于银行卡的流程。每个银行只发属于本行的卡。(工商、农业。。。)

4.2session共享带来的两大问题:

在这里插入图片描述
一。 第一个服务器存了session,而第二个相同功能的服务器不知道
二。不同的服务怎么做

4.3 同级域的session共享方案:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文采用这种方式

在这里插入图片描述

★4.4 子域的session共享问题方案(思路:放大jsession作用域,让它在其他服务都生效)

在这里插入图片描述
1.自定义session作用域:整个父域
2.session统一存入redis,同时任何模块微服务统一带同一张卡,就可以session共享

4.5 配置分布式全局session

4.5.1 自定义springSession完成子域共享(此配置最好加在每个微服务上)

@Configuration
public class MySessionConfig {
    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");//放大jsession在整个系统域的作用域,这里是父域
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    /**
     * 序列化机制(在写每个实体类的时候用jdk的序列化机制也可)
     */
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

4.5.2所有微服务启动类加上@EnableRedisHttpSession注解

5.0配置文件加redisSession

spring.session.store-type=redis
server.servlet.session.timeout=30m

6.同公司不同系统SSO登录未完

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值