验证用户登录的两种方式
目录
前言
一、Cookie 和 Session
1. 保存用户信息
2. 校验用户信息
3. 局限性
二、令牌
1. 令牌机制的原理
2. 令牌
3. 令牌的生成
4. 令牌的发放
5. 令牌的校验
6. 令牌的优势
前言
用户登录可以通过两种方式实现,一种使用 Cookie 和 Session 的方式,另外一种可以通过令牌的方式,下面分别介绍这两种方式的实现;
一、Cookie 和 Session
1. 保存用户信息
Cookie 是浏览器保存用户信息的机制,Session 是会话,是服务器保存用户信息的机制;
Cookie 中存储的信息通常是 Session ID,通过 Session ID 可以在服务器端找到 Session;
Cookie 和 Session 判断用户是否登录的过程:
用户登录时,可以将用户信息保存到 Session 中;
客户端每次访问服务器时,都会带上 Cookie;
服务器通过 Cookie 中保存的 Session ID,找到相应的 Session;
服务器从 Session 中拿到保存的用户信息:
- 如果保存的用户信息不为空,证明用户已经登录过,可以访问其它资源;
- 如果保存的用户信息为 null,证明用户没有登录,可以跳转到用户界面,提示用户登录;
登录时使用 Cookie 和 Session 保存用户信息:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserInfoController {@Autowiredprivate UserInfoService userInfoService;@RequestMapping("/login")public Result<Object> login(String userName, String password, HttpSession session){// 1. 校验参数log.info("/user/login接收到参数 userName = {}, password = {}", userName, password);if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){log.error("用户名或密码为空");return Result.failure("用户名或密码为空");}// 2. 根据用户名获取用户信息UserInfo userInfo = userInfoService.selectUserInfoByName(userName);if(userInfo == null){log.error("用户不存在");return Result.failure("用户不存在");}// 3. 校验用户密码if(!password.equals(userInfo.getPassword())){log.error("密码错误");return Result.failure("密码错误");}// 4. 存储用户信息到会话中userInfo.setPassword("");session.setAttribute(Constant.SESSION_USERINFO_KEY, userInfo);return Result.success(true);}
}
提交用户名和密码的时候,生成一个 Cookie:
使用 Fiddler 抓包,服务器响应时,响应头会带上 Set-Cookie:
2. 校验用户信息
访问其它资源时,每次先判断登录状态:
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1. 获取会话HttpSession session = request.getSession();// 2. 从会话中获取用户信息UserInfo userInfo = (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);// 3. 判断用户是否登录if(userInfo == null){return false;}return true;}
}
3. 局限性
由于 Session 是保存在服务器的内存中;
当服务器不是单台服务器,而是一个集群的情况下,客户端请求到达负载均衡装置时,会被随机分配给集群中的任意一台服务器;
这台服务器有可能不是当初登录的那一台服务器,因此 Session 中并没有保存用户信息,就会拦截用户请求,登录就会失败;
因此使用 Cookie 和 Session 的机制解决判断用户是否登录,就需要解决在集群环境下的问题。
二、令牌
为了避免 Cookie 和 Session 在集群环境下的判断用户登录问题,可以使用令牌机制;
下面以 JWT 令牌为例进行演示。
1. 令牌机制的原理
客户端登录时,服务器生成一个令牌,即一个字符串;
服务器将生成的令牌发送给客户端;
客户端保存这个令牌,后续每次登录都带上这个令牌;
服务器收到客户端请求后,先校验这个令牌,校验成功后表示已经登录,校验失败时表示未登录;
2. 令牌
令牌通常分为 3 个部分:
- 第一部分为 header,保存的是令牌的类型等信息;
- 第二部分为 body,保存的是用户的信息(非敏感信息),例如用户名,用户编号等;
- 第三部分为用户的签名,用于校验用户令牌是否为真,而不是伪造的;
3. 令牌的生成
先生成一个密钥,后续生成令牌时,需要使用这个密钥进行签名,验证令牌时,也需要使用这个密钥验证令牌是否为正;
生成的密钥通常是一个对象,是一个结构化的二进制数据,为了方便保存,可以将这个对象通过 base64 编码成一个字符串,这个字符串也是密钥;
public void genKey(){// 1. 生成 keyKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);// 2. 对 key 进行 Base64 编码String secretKey = Encoders.BASE64URL.encode(key.getEncoded());System.out.println(secretKey);}
生成令牌时,使用这个字符串密钥,通过 base64 解码,还原成原来的对象密钥;
再结合密钥的类型,用户信息,使用对象密钥进行签名,生成一个令牌;
@Slf4j
public class JwtUtil {// 过期时间private static final long EXPIRATION_TIME = 60 * 60 * 1000;// 生成的字符串private static final String secretKey = "Lgo2v38vK8lNfzwbDW7ZmlZ9Sps5TF98JOcnjbs7f4g";// 字符串再解码成 keyprivate static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));public static String genJwtToken(Map<String, Object> claim){// 1. 调用 jwt 的 api,生成 token - 生成 token 需要密钥,用于生成签名String jwtToken = Jwts.builder().setClaims(claim).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)).signWith(key).compact();return jwtToken;}
}
4. 令牌的发放
用户登录成功,将令牌作为结果返回给客户端,客户端可以将密钥保存在 Cookie 中,也可以将密钥保存在本地内存中;
@RequestMapping("/login")public Result<Object> login(String userName, String password, HttpSession session){// 1. 校验参数log.info("/user/login接收到参数 userName = {}, password = {}", userName, password);if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){log.error("用户名或密码为空");return Result.failure("用户名或密码为空");}// 2. 根据用户名获取用户信息UserInfo userInfo = userInfoService.selectUserInfoByName(userName);if(userInfo == null){log.error("用户不存在");return Result.failure("用户不存在");}// 3. 校验用户密码if(!SecurityUtil.verify(password, userInfo.getPassword())){log.error("密码错误");return Result.failure("密码错误");}// 4. 生成 tokenMap<String, Object> claim = new HashMap<>();claim.put(Constant.TOKEN_USER_ID, userInfo.getId());claim.put(Constant.TOKEN_USER_NAME, userInfo.getUserName());String token = JwtUtil.genJwtToken(claim);// 5. 每次登录后都更新数据库 password 列的值Integer updateResult = userInfoService.updatePssword(SecurityUtil.encrypt(password), userInfo.getId());return Result.success(token);}
浏览器保存令牌:
5. 令牌的校验
客户端后续访问时,都会带上令牌,服务器收到请求后,使用相同的对象密钥,对令牌进行校验;
如果校验不通过,表示用户没有登录,校验通过表示用户已经登录过了;
public static Claims verifyJwt(String token){log.info("接收到 token: {}", token);// 1. 生成一个用于校验的对象JwtParser parser = Jwts.parserBuilder().setSigningKey(key).build();// 2. 调用 api 进行校验Claims claims = null;try{claims = parser.parseClaimsJws(token).getBody();if(claims == null){log.error("token 不正确");return null;}}catch(Exception e){log.error("解析 token 失败");return null;}return claims;}
每次访问保护的资源时,都需要进行校验:
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1. 从请求中拿到 tokenString token = request.getHeader(Constant.USER_LOGIN_TOKEN);// 2. 校验 tokenClaims claims = JwtUtil.verifyJwt(token);if(claims == null){response.setStatus(401);return false;}return true;
}
6. 令牌的优势
使用令牌判断用户登录状态时,不需要引入额外的资源,在原有的机器上实现即可;
在集群环境中使用令牌,不管是哪个服务器给发方的令牌,其余的服务器,都具备校验用户 token的能力,因为都能获取生成的字符串密钥,解码生成对象密钥;