常见单机和分布式应用的登录校验解决方案

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)基本概念,可以参考我以前的一篇文章,在此不过多赘述

    密码加密与微服务鉴权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

② 代码地址:常见单机和分布式应用的登录校验解决方案

③ 兄弟们可以自己尝试请求登录,看是否成功。出错的可以留言或者私信共同探讨。

posted @ 2020-05-20 15:16  RookieMZL  阅读(402)  评论(0编辑  收藏  举报