常见单机和分布式应用的登录校验解决方案
1、常见单机和分布式应用下登录校验
单机 tomcat 应⽤用登录检验
- sesssion保存在浏览器和应用服务器会话之间。
- 用户登录成功,服务端会保存一个 session,服务器会给客户端分发一个 sessionID 作为标识。
- 客户端会把 sessionID 保存在 cookie 中,每次请求都会携带这个 sessionId。
分布式应用中 session共享
- 真实的应用不可能单节点部署,所以就有个多节点登录session共享的问题需要解决。
- tomcat支持 session 共享,但是有广播风暴;用户量大的时候,占用资源严重,不推荐。
- 使用 redis 存储 token 思路:
- 服务端使用 UUID 生成随机64位或128位 token,放入 redis 中,然后返回给客户端并存储在cookie中。
- 用户每次访问都携带此 token,服务端去 redis 中校验是否有此用户即可。
分布式应用中使用 JWT 解决方案
-
优点
- 生产的 token 可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库。
- 存储在客户端,不占用服务端的内存资源。
-
缺点
- token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,如用户权限,密码等。
- 如果没有服务端存储,则不能做登录失效处理,除非服务端改密钥。
-
对于JSON Web Token(JWT)基本概念,可以参考我以前的一篇文章,在此不过多赘述
2、登录拦截器案例代码演示
技术栈:SpringBoot 2.1.6.RELEASE,Mybatis
开发工具:IDEA、Java8、Postman
2.1、基本 SpringBoot 项目的创建、引入相关依赖、SQL语句,请参考文末代码地址即可。
2.1、用户生成 、解析 token 的工具类和用户密码工具类(使用 MD5)
/**
* Jwt工具类
* 注意点:
* 1、生成的token, 是可以通过base64进行解密出明文信息
* 2、base64进行解密出明文信息,修改再进行编码,则会解密失败
* 3、无法作废已颁布的token,除非改秘钥
*/
public class JWTUtils {
// 过期时间,一周
private static final long EXPIRE = 60000 * 60 * 24 * 7;
// 加密秘钥
public static final String SECRET = "DouBi666";
// 令牌前缀
public static final String TOKEN_PREFIX = "RookieMZL";
// subject
private static final String SUBJECT = "BigBoy";
/**
* 根据用户信息,生成令牌
*
* @param user
* @return
*/
public static String geneJWT(User user) {
String token = Jwts.builder().setSubject(SUBJECT)
.claim("id", user.getId())
.claim("name", user.getName())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.signWith(SignatureAlgorithm.HS256, SECRET).compact();
token = TOKEN_PREFIX + token;
return token;
}
/**
* 校验token的方法
*
* @param token
* @return
*/
public static Claims checkJWT(String token) {
Claims claims = Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
return claims;
}
}
===============================================================================================
public class CommonUtils {
/**
* MD5 加密
* @param data
* @return
*/
public static String MD5(String data) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
2.1、配置拦截路径和放行路径
public class LoginInterceptor implements HandlerInterceptor {
/**
* 进入到controller之前的方法
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
// 前端可以把 token 放在 Header 或者参数中
String accessToken = request.getHeader("token");
// 如果 Header 中没有就从参数中获取
if (accessToken == null) {
accessToken = request.getParameter("token");
}
if (StringUtils.isNotBlank(accessToken)) {
Claims claims = JWTUtils.checkJWT(accessToken);
if (claims == null) {
// 登陆过期,重新登陆
sendJsonMessage(response, new Result(false, StatusCode.ERROR, "登录过期,重新登录!"));
return false;
}
Integer id = (Integer) claims.get("id");
String name = (String) claims.get("name");
request.setAttribute("user_id", id);
request.setAttribute("name", name);
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
sendJsonMessage(response, new Result(false, StatusCode.ERROR, "登录过期,重新登录!"));
return false;
}
/**
* 响应json数据给前端:登陆不成功返回 Json 数据
*
* @param response
* @param obj
*/
private void sendJsonMessage(HttpServletResponse response, Object obj) {
try {
ObjectMapper objectMapper = new ObjectMapper();
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print(objectMapper.writeValueAsString(obj));
writer.close();
response.flushBuffer();
} catch (Exception e) {
e.printStackTrace();
}
}
}
===============================================================================================
/**
* 拦截器配置:配置拦截路径和放行路径
* <p>
* 不用权限可以访问url /api/v1/pub/
* 要登录可以访问url /api/v1/pri/
*/
@Configuration
@EnableWebMvc
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截全部
registry.addInterceptor(loginInterceptor()).addPathPatterns("/api/v1/pri/*/*/**")
//设置不拦截的路径
.excludePathPatterns("/api/v1/pri/user/login", "/api/v1/pri/user/register");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
2.3、基本的代码逻辑很简单,就是注册、登陆。
① 登陆成功后颁发给客户端 token ,下次登陆的时候在有效的时间段带着 token 访问即可,如果 token 时间失效,则重新登陆颁发 token ,周而复始。
② 代码逻辑比较简单,请参考文末代码地址即可。
③ 使用 postman 测试案例演示。
-
注册(会对手机号是否注册进行验证)
可以看到用户已经注册成功,如果使用相同的 phone 再次注册,则会提示手机号码被注册
-
注册成功后用户登录,颁发给客户端 token
-
使用服务端颁发的 token 登陆
-
如果使用错误的 token 登陆或者过期的 token,会提示用户重新登陆
3、总结
① 对于基本的概念请参考我的文章:密码加密与微服务鉴权JWT
② 代码地址:常见单机和分布式应用的登录校验解决方案
③ 兄弟们可以自己尝试请求登录,看是否成功。出错的可以留言或者私信共同探讨。