一、注册
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。