《谷粒商城》开发记录 10:注册和登录

一、注册

1 填写用户信息

用户在注册账号时,需要在注册页面上填写一些信息,包括:
● 基本信息:用户名、密码、验证码。
● 个人信息:姓名、手机号、邮箱等。

其中,基本信息一般是必填的,也是下文重点讨论的部分。
个人信息可以选择填或不填,如果不填,可以在注册完成后增加一个实名认证流程。

2 验证码

验证码的作用是分辨注册用户是人还是机器,能有效防止恶意注册。
常见的验证码的形式有图片验证码和短信验证码。

2.1 图片验证码

获取图片验证码的步骤如下:
1. 生成一个uuid。
    生成一个随机的验证码code,可以是4位的英文/数字。
    定义一个超时时间timeout,可以是300秒。
2. 以uuid为key,code为value,timeout为expire,向Redis中存入一条数据。
3. 创建一个字节数组输出流:java.io.ByteArrayOutputStream类的实例outputStream。
    以code为文本内容,创建一张图片:java.awt.image.BufferedImage类的实例bufferedImage。
    使用javax.imageio.ImageIO类的write方法,把bufferedImage写入outputStream。
4. 自定义一个图片验证码信息类ImageVerification,包含两个属性:
    String imageKey;
    byte[] Image;
    创建一个ImageVerification对象result。
    result.setImageKey(key);
    result.setImage(outputStream.toByteArray());
5. 将result返回。
6. 注意:在重新获取验证码时,最好删除之前已存入Redis的验证码信息(根据result里的key),防止攻击者短时间内发送大量请求造成内存溢出。

示例代码:
    public ImageVerification getImageVerificationCode() {
        String key = UUID.randomUUID().toString();
        String code = createCode(); // TODO createCode()
        int timeout = Integer.parseInt("300"); // TODO 从配置中读取超时时间
        redisClient.putWithExpire(key, code, timeout); // TODO RedisClient
        BufferedImage bufferedImage = createBufferedImage(code); // TODO createBufferedImage(code)
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            ImageIO.write(bufferedImage, "gif", outputStream); // TODO 从配置中读取文件类型
        } catch (IOException e) {
            e.printStackTrace();
        }
        ImageVerification result = new ImageVerification();
        result.setImageKey(key);
        result.setImage(outputStream.toByteArray());
       
        return result;
    }

2.2 短信验证码

1. 获取短信验证码。
    发动钞能力,上阿里云开通短信服务。然后进行API接入。
2. 验证码校验。
    在发送短信验证码时,以手机号phone为key,验证码code为value,向Redis中存入一条数据(如果key已存在就覆盖,超时时间可以设5分钟)。用户在页面上提交验证码后,在Redis中get(key),得到真实的code,与用户输入的验证码进行核对。
3. 验证码防刷:60秒内不可再次发送短信验证码。
    在向Redis中存入验证码信息时,在验证码code后面加上当前时间戳timestamp。每次请求发送短信验证码时,先在Redis中get(key),若返回值不为null,就用当前时间戳与查到的timestamp相减,若差值小于60秒,则抛出异常,提示发送短信验证码过于频繁。

3 密码加密保存

用户的密码通常不会在数据库中明文保存,在保存之前会对密码进行加密。
一种常用的加密方法是MD5盐值加密。

3.1 MD5算法

MD5:Message-Digest Algorlthm 5,一种信息摘要算法。它的特点包括:
● 无论多长的文本,计算得到的MD5值长度是固定的,都是128位(16字节)。
● 计算过程并不复杂。
● 对同样的文本进行多次计算,得到的MD5值是固定的。对文本进行任何改动,哪怕只修改1个字节,得到的MD5值都会发生很大的变化。
● 计算是不可逆的,不能从MD5值反推密码。

Apache提供了一个摘要工具类:org.apache.commons.codec.digest.DigestUtils,包含一套计算MD5值的方法:
● public static byte[] md5(byte[] data);
● public static byte[] md5(InputStream data) throws IOException;
● public static byte[] md5(String data);
● public static String md5Hex(byte[] data);
● public static String md5Hex(InputStream data) throws IOException;
● public static String md5Hex(String data);

3.2 MD5算法的安全性问题

MD5算法是一种可以被破解的算法,因此直接使用MD5算法对密码进行加密是不安全的。
MD5算法的破解方法实际上就是暴力法,定期地对一些可能用作密码的文本进行计算,把结果记录到MD5密码数据库中。时至今日,MD5密码数据库的数据量已经非常庞大了,大部分常用密码都可以通过MD5值反向查询到密码明文。

3.3 MD5盐值加密

为了防止入侵者通过MD5值反查密码明文,通常会在计算MD5值时进行加盐处理。
1. 保存密码时,先生成一个随机的16位数字,称为盐值,将盐值和密码相加后,通过计算得到MD5值。然后将盐值按一定规则掺入这个MD5值,形成一个数十位的字符串。
2. 验证密码时,从字符串中按规则提取出掺入其中的盐值,与用户输入的密码相加,再次计算MD5值。校验两次计算得到的MD5值是否一致。如果一致,则密码输入正确。

Apache提供了一个密码编码器类:org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder,包含了MD5盐值加密的API:  
● public String encode(CharSequence rawPassword);
用户在进行登录时,用来校验密码的API:
● public boolean matches(CharSequence rawPassword, String encodedPassword);

二、登录

1 登录校验

1. 从页面获取用户输入的username和password。
2. 根据username查询用户信息(可以创建一个CurrentUser类,把用户信息放在它的实例中),如果返回用户信息不为null,说明用户已注册。
3. 校验password。
4. 返回用户信息,页面重定向到系统主页。

2 记录登录状态

2.1 为什么要记录登录状态?

● 再次进入页面时,不需要再次登录。
● 对于一些请求,需要对当前操作用户进行鉴权。

2.2 如何记录登录状态?

使用session记录登录状态。
1. 获得session。
    在接口的入参中添加HttpServletRequest request,然后HttpSession session = request.getSession();
    如果request携带的cookie中包含了一个sessionId,则会根据这个sessionId查找并返回一个session,否则会创建一个新的session。
2. 【自动】当session被创建时,会自动创建一个cookie添加到响应数据中。
    Cookie cookie = new Cookie("JSESSIONID", session.getId()); 
    response.addCookie(cookie);
    浏览器在收到响应数据时,会将cookie保存在客户端,并在以后每次访问服务器时带上这个cookie。
3. 登录验证通过后,手动地向session中保存用户信息。
    session.setAttribute("currentUser", currentUser);
    这样,以后每次访问服务器时,都可以从session中获取到用户信息。

2.3 分布式session

在分布式架构中,每个服务都有可能部署在多个服务器节点上,这样就不能把session保存在服务器本地,而必须建立分布式session。
建立分布式session的思路是:把session信息保存在Redis中,所有服务器都到Redis中获取session。

我们具体的实现方案是Spring Session+Redis。
1. 添加Spring Session依赖。
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
2. 配置session类型为Redis。
    在配置文件中添加:spring.session.store-type=redis。
    还可以在配置文件中配置session的超时时间、命名空间、刷新策略。
    ● server.servlet.session.timeout=86400
    ● spring.session.redis.namespace=spring:session
    ● spring.session.redis.flush-mode=on-save
3. 配置Redis。
    配置Redis的IP地址和端口号,已配置。
4. 开启Spring Session。
    在服务启动类上添加注解@EnableRedisHttpSession。
5. 使用Spring Session。
    使用Spring Session的操作和使用原生态HttpSession是一样的。
6. 注意:在向Redis中保存session时,session中携带的数据必须是可序列化的。在登录业务中,session携带的数据是currentUser,所以CurrentUser类要实现Serializable接口。

2.4 自定义session配置

此时,我们还有两个问题没有解决。
● 我们在登录页auth.gulimall.com进行登录后,浏览器保存的cookie默认只在登录页生效,我们应该修改cookie的作用域,保证它在gulimall.com下的所有子域都生效。
● 对session进行序列化时,应序列化成JSON格式。

我们使用自定义session配置来解决这两个问题:
    @Configuration
    public class GulimallSessionConfig{
        /**
        * cookie序列化器
        * @return
        */
       @Bean
        public CookieSerializer cookieSerializer() {
            DefaultCookieSerializer serializer = new DefaultCookieSerializer();
            serializer.setDomainName("gulimall.com"); // 设置cookie作用域
            serializer.setCookieName("GULISESSION"); // 设置cookie名称
            return serializer;
        }
        /**
        * redis序列化器
        * @return
        */
        @Bean
        public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
            return new GenericFastJsonRedisSerializer();
        }
    }

3 社交登录

跳过。

4 单点登录

为了讲解单点登录,我们假设有:
● 单点登录服务器:ssoserver。
● 服务器1:server1。
● 服务器2:server2。
● 浏览器。

4.1 浏览器首次访问server1

1. 在浏览器上访问域名http://server1.com:8081/hello,请求被打到server1。
2. (过滤器生效)server1查询session1,没有查到任何用户信息。命令浏览器重定向到登录页http://ssoserver.com:8080/login.html?redirect_url=http://server1.com:8081/hello。
3. 请求被打到ssoserver,ssoserver返回静态页面资源login.html。
4. 浏览器展示登录页,用户输入用户名和密码进行登录。
5. 登录请求http://ssoserver.com:8080/doLogin?username=xxx&password=xxx&redirect_url=http://server1.com:8081/hello,请求被打到ssoserver。ssoserver先进行登录校验,校验通过后得到用户信息currentUser。
    生成一个uuid。以uuid为key,以currentUser为value,向Redis中插入一条数据。
    【server2免登录】创建一个cookie:new Cookie("sso_token", uuid); 将该cookie添加到response中。
    以uuid作为token,命令浏览器重定向到http://server1.com:8081/hello?token=xxxxxx。
    【server2免登录】浏览器保存cookie:sso_token=xxxxxx。
6. 请求被打到server1,server1判定token不为空,向ssoserver发送请求http://ssoserver.com:8080/userInfo?token=xxxxxx。
    ssoserver根据token从Redis中获取currentUser,返回给server1。
    server1将currentUser保存在session1中。
7. server1从session1中查到用户信息currentUser,执行/hello。

4.2 浏览器第二次访问server1

1. 在浏览器上访问域名http://server1.com:8081/hello,请求被打到server1。
2. server1从session1中查到用户信息currentUser,执行/hello。

4.3 浏览器首次访问server2

1. 在浏览器上访问域名http://server2.com:8082/world,请求被打到server2。
2. 请求没有携带token,server2无法获取用户信息,命令浏览器重定向到登录页http://ssoserver.com:8080/login.html?redirect_url=http://server2.com:8082/world。
3. 请求被打到ssoserver,携带cookie:sso_token=xxxxxx。ssoserver命令浏览器重定向到http://server2.com:8082/world?token=xxxxxx。
4. 请求被打到server2,server2判定token不为空,向ssoserver发送请求http://ssoserver.com:8080/userInfo?token=xxxxxx。
ssoserver根据token从Redis中获取currentUser,返回给server2。
server2将currentUser保存在session2中。
5. server2判定currentUser不为空,执行/world。

4.4 浏览器第二次访问server2

1. 在浏览器上访问域名http://server2.com:8082/world,请求被打到server2。
2. server2从session2中查到用户信息currentUser,执行/world。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值