谷粒商城高级篇-认证服务的开发


一、短信验证功能

// 在gulimall-third-party中编写发送短信的逻辑代码如下
@Data
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Component
public class SmsComponent {

	private String host;

	private String path;

	private String skin;

	private String sign;

	private String appCode;

	public String sendSmsCode(String phone, String code){
		String method = "GET";
		Map<String, String> headers = new HashMap<String, String>();
		headers.put("Authorization", "APPCODE " + this.appCode);
		Map<String, String> querys = new HashMap<String, String>();
		querys.put("code", code);
		querys.put("phone", phone);
		querys.put("skin", this.skin);
		querys.put("sign", this.sign);
		HttpResponse response = null;
		try {
			response = HttpUtils.doGet(this.host, this.path, method, headers, querys);

			if(response.getStatusLine().getStatusCode() == 200){
				return EntityUtils.toString(response.getEntity());
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "fail_" + response.getStatusLine().getStatusCode();
	}
}


@Controller
@RequestMapping("/sms")
public class SmsSendController {

    @Autowired
    private SmsComponent smsComponent;

    /*** 提供给别的服务进行调用的
    只提供短信发送
	 */
    @GetMapping("/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code){
        if(!"fail".equals(smsComponent.sendSmsCode(phone, code).split("_")[0])){
            return R.ok();
        }
        return R.error(BizCodeEnum.SMS_SEND_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_SEND_CODE_EXCEPTION.getMsg());
    }
}

来思考一个问题,如果有恶意用户 反复提交短信请求怎么办?

  • 在redis中以phone-code为前缀将电话号码和验证码进行存储并将当前时间与code一起存储,如果调用时以当前phone取出的v不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息,60s以后再次调用,需要删除之前存储的phone-code,code存在一个过期时间,我们设置为10min,10min内验证该验证码有效
@ResponseBody
@GetMapping("/sms/snedcode")
public R sendCode(@RequestParam("phone") String phone){

    //存redis
    String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
    // 如果不为空说明反复提交了

    if(null != redisCode && redisCode.length() > 0){
        long CuuTime = Long.parseLong(redisCode.split("_")[1]);
        if(System.currentTimeMillis() - CuuTime < 60 * 1000){ // 60s
            return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
        }
    }
    // 生成验证码
    String code = UUID.randomUUID().toString().substring(0, 6);
    String redis_code = code + "_" + System.currentTimeMillis();
    // 缓存验证码
    stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, redis_code , 10, TimeUnit.MINUTES);
    try {// 调用第三方短信服务
        return thirdPartFeignService.sendCode(phone, code);
    } catch (Exception e) {
        log.warn("远程调出错");
    }
    return R.ok();
}

二、注册功能(略过)

三、登录功能

3.1 oauth

一般第三方登录流程

  1. 点击xx登录
  2. 引导用户跳转到授权页
  3. 用户主动点击授权,认证成功跳回之前网页
    ps: 其实上述说的流程就是oauth协议的过程
    在这里插入图片描述

3.2 整合第三方社交登录(以微博为例)

  • 直接百度搜索微博开放平台
  • 创建新应用xx,会得到APP KEY和APP Secret
  • 授权回调页:gulimall.com/oauthn2.0/weibo/success
  • 取消授权回调页:gulimall.com/oauthn2.0/weibo/fail
  • 具体步骤
  1. 点击首页的微博登录,此时需要引导用户到微博的授权页面
// 其实点击的时候访问的就是这个地址
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=授权后跳转的uri

// 伪代码
https://api.weibo.com/oauth2/authorize?
client_id=刚才申请的APP-KEY &
response_type=code&
redirect_uri=http://gulimall.com/success

  1. 如果用户同意授权(输入账号密码),带着code,页面跳转至我们指定登录成功的controller这时候会有一个code
  2. 拿着code去换accesstoken
// 伪代码
https://api.weibo.com/oauth2/access_token?
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
grant_type=authorization_code&
redirect_uri=YOUR_REGISTERED_REDIRECT_URI&
code=CODE
  1. 拿到他给的acesstoken 就可以对他给出的接口进行一些数据的获取 https://open.weibo.com/wiki/2/users/show

3.2.1 代码

// 认证成功的回调
@GetMapping("/weibo/success") 
public String weiBo(@RequestParam("code") String code, HttpSession session) throws Exception {

    // 根据code换取 Access Token
    Map<String,String> map = new HashMap<>();
    map.put("client_id", "1294828100");
    map.put("client_secret", "a8e8900e15fba6077591cdfa3105af44");
    map.put("grant_type", "authorization_code");
    map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
    map.put("code", code);
    Map<String, String> headers = new HashMap<>();

    // 去获取token
    HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);
    if(response.getStatusLine().getStatusCode() == 200){
        // 获取返回结果
        String json = EntityUtils.toString(response.getEntity());
        SocialUser socialUser = JSON.parseObject(json, SocialUser.class);

        // 进行保存数据库 要保存的主要有uid、token、expires_in
        R login = memberFeignService.login(socialUser);
        if(login.getCode() == 0){
            MemberRsepVo rsepVo = login.getData("data" ,new TypeReference<MemberRsepVo>() {});
            // 第一次使用session 命令浏览器保存这个用户信息 JESSIONSEID 每次只要访问这个网站就会带上这个cookie
            // 在发卡的时候扩大session作用域 (指定域名为父域名)
            // TODO 1.默认发的当前域的session (需要解决子域session共享问题)
            // TODO 2.使用JSON的方式序列化到redis
            //				new Cookie("JSESSIONID","").setDomain("gulimall.com");
            session.setAttribute(AuthServerConstant.LOGIN_USER, rsepVo);
            // 登录成功 跳回首页
            return "redirect:http://gulimall.com";
        }else{
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }else{
        return "redirect:http://auth.gulimall.com/login.html";
    }
}

@RequestMapping("/oauth2/login")
public R login(@RequestBody SocialUser socialUser) {
    MemberEntity entity=memberService.login(socialUser);
    if (entity!=null){
        return R.ok().put("memberEntity",entity);
    }else {
        return R.error();
    }
}

@Override 
public MemberEntity login(SocialUser socialUser) {

    // 微博的uid
    String uid = socialUser.getUid();
    // 判断社交用户登录过系统
    MemberDao dao = this.baseMapper;
    MemberEntity entity = dao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));

    MemberEntity memberEntity = new MemberEntity();
    if(entity != null){ // 注册过
        // 说明这个用户注册过, 修改它的资料
        // 更新令牌
        memberEntity.setId(entity.getId());
        memberEntity.setAccessToken(socialUser.getAccessToken());
        memberEntity.setExpiresIn(socialUser.getExpiresIn());
        // 更新
        dao.updateById(memberEntity);
        entity.setAccessToken(socialUser.getAccessToken());
        entity.setExpiresIn(socialUser.getExpiresIn());
        entity.setPassword(null);
        return entity;
    }else{ // 如果没有注册过
        HashMap<String, String> map = new HashMap<>();
        map.put("access_token", socialUser.getAccessToken());
        map.put("uid", socialUser.getUid());
        try {
            //  查询基本信息
            HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), map);
            if(response.getStatusLine().getStatusCode() == 200){
                // 查询成功
                String json = EntityUtils.toString(response.getEntity());
                // 解析json
                JSONObject jsonObject = JSON.parseObject(json);
                memberEntity.setNickname(jsonObject.getString("name"));
                memberEntity.setUsername(jsonObject.getString("name"));
                memberEntity.setGender("m".equals(jsonObject.getString("gender"))?1:0);
                memberEntity.setCity(jsonObject.getString("location"));
                memberEntity.setJob("ss");
                memberEntity.setEmail(jsonObject.getString("email"));
            }
        } catch (Exception e) {
            log.warn("第三方登录调用服务出错");
        }
        memberEntity.setStatus(0);
        memberEntity.setCreateTime(new Date());
        memberEntity.setBirth(new Date());
        memberEntity.setLevelId(1L);
        memberEntity.setSocialUid(socialUser.getUid());
        memberEntity.setAccessToken(socialUser.getAccessToken());
        memberEntity.setExpiresIn(socialUser.getExpiresIn());

        // 注册 -- 登录成功
        dao.insert(memberEntity);
        memberEntity.setPassword(null);
        return memberEntity;
    }
}

3.3 springsession(分布式session)

3.3.1 session

一般访问请求是这样session存储在服务端,jsessionId存在客户端,每次请求的时候带上jsessionid
问题:但是正常情况下session不可跨域,它有自己的作用范围(就是不能跨服务 )
在这里插入图片描述
JsessionId的参数Value(值) Domain(gulimall.com要放大域名作用域) Path(作用范围) Expires/Max-Age(过期时间)

  • 解决方案1.session复制
    在这里插入图片描述
  • 解决方案2.hash一致性
    在这里插入图片描述
  • 解决方案3.redis(推荐)
    在这里插入图片描述

3.3.2 SpringSession整合redis

//依赖
<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>

//配置
spring.session.store-type=redis
server.servlet.session.timeout=30m
spring.redis.host=192.168.56.10

// 启动类加上
@EnableRedisHttpSession //创建了一个springSessionRepositoryFilter ,负责将原生HttpSession 替换为Spring Session的实现
public class GulimallAuthServerApplication {

扩大session作用域

@Configuration
public class GulimallSessionConfig {

    @Bean // redis的json序列化
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

    @Bean // cookie
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("GULISESSIONID"); // cookie的键
        serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
        return serializer;
    }
}
// 这个配置需要放到每个微服务上

登录逻辑

@GetMapping({"/login.html","/","/index","/index.html"}) // auth
public String loginPage(HttpSession session){
    // 从session从获取loginUser
    Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);// "loginUser";
    System.out.println("attribute:"+attribute);
    if(attribute == null){
        return "login";
    }
    System.out.println("已登陆过,重定向到首页");
    return "redirect:http://gulimall.com";
}


@PostMapping("/login") 
public String login(UserLoginVo userLoginVo,
                    RedirectAttributes redirectAttributes,
                    HttpSession session){
    R r = memberFeignService.login(userLoginVo);
    if(r.getCode() == 0){
        MemberRespVo respVo = r.getData("data", new TypeReference<MemberRespVo>() {});
        // 放入session  // key为loginUser
        session.setAttribute(AuthServerConstant.LOGIN_USER, respVo);//loginUser
        log.info("\n欢迎 [" + respVo.getUsername() + "] 登录");
        // 登录成功重定向到首页
        return "redirect:http://gulimall.com";
    }else {
        HashMap<String, String> error = new HashMap<>();
        // 获取错误信息
        error.put("msg", r.getData("msg",new TypeReference<String>(){}));
        redirectAttributes.addFlashAttribute("errors", error);
        return "redirect:http://auth.gulimall.com/login.html";
    }
}

3.4 单点登录

常见 两个不同域名的想共享session 这肯定是不行的 session的域没这么大
解决方案:建一个公共的认证中心sso 其实可以简单把他理解为颁发凭证的地方 在这块登录了其他的地方就可以了
过程
1.a用户访问app1,app1有些功能是需要登录的a没登录
2. 这时候跳转到认证服务(sso登录系统),sso没登录,弹出登录页面
3. 用户填写信息,sso进行认证,成功写状态(session)
4. 认证服务登录成功会产生一个凭据(Service Ticket),然后调到app1,这时候将凭据作为参数传入
5. app1 拿到参数发请求到sso看是不是有效的
6. 验证通过后,app系统将登录状态写入session并设置app域下的Cookie
到上面为止一个跨域的单点登录就完成了 我们在看看app2的流程(app2 app1 sso服务不是在一个域的)

  1. 用户访问app2系统,app2系统没有登录,跳转到SSO。
  2. 由于SSO已经登录了,不需要重新登录认证。
  3. SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
  4. app2拿到ST,后台访问SSO,验证ST是否有效。
  5. 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
    用的开源sso的项目:https://gitee.com/xuxueli0323/xxl-sso

3.4.1 代码(略)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值