[gulimall] 用户登录与注册相关

1. 注册

基本逻辑

  • 1)数据格式校验,以及用户名是否已存在
  • 2)核对验证码
  • 3)MD5加盐对密码加密
  • 4)写入数据库,返回登录页

1.1 数据格式校验

通过注解可以给前端传递过来的值进行校验,例如@NotEmpty, @Length等。

但是这个注解必须配合 @Valid 使用,完成对参数的校验。

而校验的结果,也会自动封装到 BindingResult 类型中,通过这个参数可以很方便的对错误的参数进行处理。hasErrors() 可以判断是否有参数校验错误,如果有,可以通过 getFieldsErrors() 方法获取错误列表。

@Data
public 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;

}


@PostMapping(value = "/register")
public String register(@Valid UserRegisterVo vos, BindingResult result, RedirectAttributes attributes) {
    //如果有错误回到注册页面
    if (result.hasErrors()) {
        Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        attributes.addFlashAttribute("errors",errors);
        //效验出错回到注册页面
        return "redirect:http://auth.gulimall.com/reg.html";
    }
    ......
}

1.2 核对验证码

第一种:阿里云短信验证码

这种方案在发送验证码时将验证码存入redis,再通过短信发送给用户,最后用输入的验证码与redis中的验证码匹配。这种方案需要注意接口防刷,60s内只能发送一次验证码

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

    //1、接口防刷
    String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
    if (!StringUtils.isEmpty(redisCode)) {
        //活动存入redis的时间,用当前时间减去存入redis的时间,判断用户手机号是否在60s内发送验证码
        long currentTime = Long.parseLong(redisCode.split("_")[1]);
        if (System.currentTimeMillis() - currentTime < 60000) {
            //60s内不能再发
            return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
        }
    }

    //2、验证码的再次效验 redis.存key-phone,value-code
    int code = (int) ((Math.random() * 9 + 1) * 100000);
    String codeNum = String.valueOf(code);
    String redisStorage = codeNum + "_" + System.currentTimeMillis();

    //存入redis,防止同一个手机号在60秒内再次发送验证码
    stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone,
                                          redisStorage, 10, TimeUnit.MINUTES);
	//这里调用短信服务
    thirdPartFeignService.sendCode(phone, codeNum);

    return R.ok();
}

第二种:验证码的插件(忘了是啥)

1.3 MD5加盐对密码加密

spring提供了MD5加盐对密码加密的方法

	// Spring 盐值加密
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    //加密
    String encode = bCryptPasswordEncoder.encode("123456");
    //判断密码和加密后的密文是否匹配
    boolean matches = bCryptPasswordEncoder.matches("123456", "$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S");

1.4 保存数据

2. 登录

2.1. 有状态登录

为了保证客户端cookie的安全性,服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。

例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的sessionId。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。

缺点是什么?

  • 服务端保存大量数据,增加服务端压力
  • 分布式环境下session在不同服务器间得同步

2.2. 无状态登录

微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:

  • 服务端不保存任何客户信息
  • 客户端的每次请求必须自己提交相关身份信息

带来的好处是什么呢?

  • 服务端可以任意的迁移和伸缩
  • 减小服务端存储压力

无状态登录流程

  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
  • 以后每次请求,客户端都携带认证的token
  • 服务的对token进行解密,判断是否有效。

token的安全性

token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。一般采用JWT + RSA非对称加密

JWT

JWT包含三部分数据:

  • Header:头部,通常头部有两部分信息:

    • token类型:JWT
    • 加密方式:base64(HS256)
  • Payload:载荷,就是有效数据,一般包含下面信息:

    • 用户身份信息(注意,这里因为采用base64编码,可解码,因此不要存放敏感信息)
    • 注册声明:如token的签发时间,过期时间,签发人等

    这部分也会采用base64编码,得到第二部分数据

  • Signature:签名,是整个数据的认证信息。根据前两步的数据,再加上指定的密钥(secret)(不要泄漏,最好周期性更换),通过base64编码生成。用于验证整个数据完整和可靠性

@Service
@EnableConfigurationProperties({JwtProperties.class})
public class AuthService {

    @Autowired
    private GmallUmsClient umsClient;

    @Autowired
    private JwtProperties jwtProperties;

    public void accredit(String loginName, String password, HttpServletRequest request, HttpServletResponse response) {

        try {
            // 1. 完成远程请求,获取用户信息
            ResponseVo<UserEntity> userEntityResponseVo = this.umsClient.queryUser(loginName, password);
            UserEntity userEntity = userEntityResponseVo.getData();

            // 2. 判断用户信息是否为空
            if (userEntity == null) {
                throw new UserException("用户名或者密码有误!");
            }

            // 3. 把用户id及用户名放入载荷
            Map<String, Object> map = new HashMap<>();
            map.put("userId", userEntity.getId());
            map.put("username", userEntity.getUsername());

            // 4. 为了防止jwt被别人盗取,载荷中加入用户ip地址
            String ipAddress = IpUtil.getIpAddress(request);
            map.put("ip", ipAddress);

            // 5. 制作jwt类型的token信息
            String token = JwtUtil.generateToken(map, this.jwtProperties.getPrivateKey(), this.jwtProperties.getExpire());

            // 6. 把jwt放入cookie中
            CookieUtil.setCookie(request, response, this.jwtProperties.getCookieName(), token, this.jwtProperties.getExpire() * 60);
            // 7.用户昵称放入cookie中,方便页面展示昵称
            CookieUtil.setCookie(request, response, this.jwtProperties.getUnick(), userEntity.getNickname(), this.jwtProperties.getExpire() * 60);

        } catch (Exception e) {
            e.printStackTrace();
            throw new UserException("用户名或者密码出错!");
        }
    }
}

2.3单点登录

同域下的单点登录

比如我们有个域名叫做:a.com,同时有两个业务系统分别为:app1.a.com和app2.a.com。还需要设立一个登录系统,叫做:sso.a.com。

在sso.a.com中登录,其实是在sso.a.com的服务端的session中记录了登录状态,同时在浏览器端(Browser)的sso.a.com下写入了Cookie。但是仅仅这样是不能让app1.a.com和app2.a.com登录的:

  1. Cookie是不能跨域的,我们Cookie的domain属性是sso.a.com,在给app1.a.com和app2.a.com发送请求是带不上的。
  2. sso、app1和app2是不同的应用,它们的session存在自己的应用内,是不共享的。

那么我们如何解决这两个问题呢?

  1. sso登录以后,可以将Cookie的域设置为顶域,即.a.com,这样所有子域的系统都可以访问到顶域的Cookie。

  2. 3个系统的Session共享。共享Session的解决方案有很多,例如:Spring-Session。

不同域下的单点登录

比如我们有个域名叫做:a.com,同时有两个业务系统分别为:app1.a.com和app2.a.com。还需要设立一个登录系统,叫做:sso.a.com。

具体流程如下:

用户访问app1系统

  1. 用户访问app1系统,发现用户还没有登录,跳转到SSO登录系统。
  2. 用户在sso系统进行登录,将登录状态写入SSO的session,浏览器的SSO域下写入Cookie。并且生成
    一个token,然后跳转到app1系统,同时将token作为参数传递给app1系统。
  3. app1系统拿到token后,从后台向SSO发送请求,验证ST是否有效。
  4. 验证通过后,app1系统将登录状态写入session并设置app1域下的Cookie。app1登录完成

用户访问app2系统

  1. 用户访问app2系统,app2系统没有登录,跳转到SSO。
  2. 由于SSO已经登录了,不需要重新登录认证。
  3. SSO生成token,浏览器跳转到app2系统,并将token作为参数传递给app2。
  4. app2拿到token,后台访问SSO,验证ST是否有效。
  5. 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。app2登录完成
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值