基于SpringSecurity+JWT的微服务鉴权解决方案
发布日期:2021-05-08 05:28:35 浏览次数:21 分类:精选文章

本文共 5633 字,大约阅读时间需要 18 分钟。

背景

公司正在进行一个旧项目权鉴改造。目标是让任意一端登录(web、app、h5)后,可以携带对应的token来请求访问后台服务资源。

达到目的

用户通过任意一端登录后,可以获得对应的token,并利用这个token来访问后台服务资源。

传统认证流程

互联网服务的用户认证一般遵循以下流程:

  • 用户向服务器发送用户名和密码。
  • 服务器验证用户名和密码,成功后在当前对话(session)中保存相关数据,如用户角色、登录时间等。
  • 服务器返回一个session_id,并将其写入用户的Cookie。
  • 用户随后的每一次请求,都会通过Cookie将session_id发送到服务器。
  • 服务器收到session_id后,通过前期保存的数据确定用户身份。
  • 传统模式的问题

    这种认证模式存在以下问题:

  • Cookie存储限制:Cookie的存储空间有限制(通常为4k),无法存储大量数据。
  • Cookie的有效范围限制:Cookie仅在当前域名下有效,在分布式环境或前后端分离项目中难以使用。
  • 跨端互操作性问题:在app端内嵌网页互相跳转时,原生跳转或内嵌网页跳转会导致与服务端的交互不兼容。
  • Session数据共享问题:单机环境没有问题,但在高流量情况下,单台服务器无法承接流量,需要进行集群处理,导致Session数据共享问题。
  • Session共享问题的解决方法

    针对Session共享问题,可以采取以下解决方法:

  • Session粘性(仅供了解)

    • 通过Session粘性技术,确保同一用户的请求在同一服务器上处理。
    • 例如,Nginx负载均衡中使用hash算法进行负载均衡。
  • Session复制(仅供了解)

    • 实现Session复制,使得集群中的各服务器相互保存Session数据。
    • Tomcat本身支持基于IP组播的Session复制方式,但可能会带来网络开销和带宽消耗。
  • 统一存储Session数据

    • 集群中的各节点Session数据统一存储到第三方存储设备(如Redis、MySQL)。
    • 每个节点获取Session数据时,从集中存储设备中获取,而不是从本地内存获取。
    • 这种方式无论是哪个节点新增或修改Session数据,都会反映到集中存储设备。
  • Cookie Based方法

    • 基于token的方式,不依赖容器本身的Session机制。
    • 服务端根据一定算法生成token并发送给客户端。
    • 客户端每次请求都携带token,服务端接收token后进行验证和解密,获取关键数据进行处理。
  • JWT(JSON Web Tokens)

    JWT是一种基于token的认证方式,服务端不对token进行存储,而是通过签名算法验证并解密token。

    传统授权流程
  • 用户登录时,服务端生成JWT token,并保存到Redis(以唯一值如“org+userId”作为key)。
  • 用户携带access_token和refresh_token进行后续请求。
  • 服务端接收JWT token后,先验证token的有效性,再解密token获取关键数据进行处理。
  • 公钥私钥授权流程
  • 生成JWT token时,使用非对称加密方式(如RSA)进行签名。
  • 解密JWT token时,通过公钥验证签名,获取原始数据。
  • JWT验证流程
  • 服务端接收JWT token,调用unJwtToken方法进行解密。
  • 解密后获取username、userId、orgId等信息。
  • 根据业务需求进一步验证用户信息。
  • 改造代码落地

    旧代码

    @GetMapping("/user")
    public Object list(Principal user, String sso_cookie) {
    if (verifyCookie) {
    String ssoCookie = SpringUtil.getSsoCookie(user);
    if (StringUtil.isBlank(sso_cookie) || !sso_cookie.equals(ssoCookie)) {
    throw new BusinessException(ResultCode.TOKEN_INVALID);
    }
    }
    return user;
    }

    新代码

    private LoginVo packetTokenVo(LoginVo tokenVo, LoginDto dto) {
    if (StringUtils.isNotBlank(dto.getClient())) {
    String orgId = dto.getOrgId() == null ? null : dto.getOrgId().toString();
    int userType = dto.getUsertype() == null ? 0 : Integer.parseInt(dto.getUsertype());
    JwtAccessToken jwtAccessToken = new JwtAccessToken(dto.getUserName(), orgId, dto.getIdCard(), 1, dto.getClient(), userType);
    String accessTokenStr = JwtUtil.jwtAccessTokenHM256(jwtAccessToken, JwtUtil.SECRET);
    JwtRefreshToken jwtRefreshToken = new JwtRefreshToken(dto.getUserName(), dto.getIdCard(), 1, dto.getClient(), userType, orgId);
    String refreshTokenStr = JwtUtil.jwtRefreshTokenHM256(jwtRefreshToken, JwtUtil.SECRET);
    cacheAccessTokenAndRefreshToken(atkey, accessTokenStr, rtkey, refreshTokenStr);
    tokenVo.setAccessToken(accessTokenStr);
    tokenVo.setRefreshToken(refreshTokenStr);
    }
    return tokenVo;
    }
    @GetMapping("/validate")
    public Object validate(String jwt, String sso_cookie) {
    if (verifyCookie) {
    JwtToken rawJwtToken = JwtUtil.unJwtToken(jwt);
    if (rawJwtToken == null || StringUtils.isEmpty(rawJwtToken.getClientId())) {
    throw new BusinessException(ResultCode.TOKEN_INVALID);
    }
    String accessKeyStr = TokenKeyUtil.getAccessTokenKey(verifyJwtToken.getClientId(), verifyJwtToken.getAppType(), verifyJwtToken.getUserId(), verifyJwtToken.getOrgId());
    Long validation = (Long) redisTemplate.execute(validateAccessTokenScript, Arrays.asList(accessKeyStr), jwt);
    if (!accessKeyStr.equals(jwt)) {
    throw new BusinessException(ResultCode.TOKEN_INVALID);
    }
    }
    return jwt;
    }

    加密解密代码

    生成JWT token

    public static String jwtAccessTokenHM256(JwtAccessToken jwtAccessToken, String secret) {
    try {
    Algorithm algorithm = Algorithm.HMAC256(secret);
    String token = JWT.create()
    .withClaim("username", jwtAccessToken.getUsername())
    .withClaim("userId", jwtAccessToken.getUserId())
    .withClaim("orgId", jwtAccessToken.getOrgId())
    .withClaim("appType", jwtAccessToken.getAppType())
    .withClaim("clientId", jwtAccessToken.getClientId())
    .withClaim("tokenType", jwtAccessToken.getTokenType())
    .withClaim("userType", jwtAccessToken.getUserType())
    .withIssuer(ISSUER)
    .withIssuedAt(new Date())
    .sign(algorithm);
    return token;
    } catch (Exception exception) {
    logger.error("jwt处理异常:", exception);
    throw new RuntimeException();
    }
    }

    解密JWT token

    public static JwtToken unJwtToken(String token) {
    try {
    DecodedJWT jwt = JWT.decode(token);
    Map
    claimMap = jwt.getClaims();
    String username = claimMap.get("username").asString();
    String userId = claimMap.get("userId").asString();
    String orgId = claimMap.get("orgId").asString();
    int appType = claimMap.get("appType").asInt();
    String clientId = claimMap.get("clientId").asString();
    String tokenType = claimMap.get("tokenType").asString();
    int userType = claimMap.get("userType").asInt();
    JwtToken jwtToken = null;
    if (TokenType.ACCESS_TOKEN.equals(tokenType)) {
    jwtToken = new JwtAccessToken(username, orgId, userId, appType, clientId, userType);
    } else if (TokenType.REFRESH_TOKEN.equals(tokenType)) {
    jwtToken = new JwtRefreshToken(username, userId, appType, clientId, userType, orgId);
    }
    return jwtToken;
    } catch (Exception e) {
    logger.error("jwt处理异常:", e);
    throw new BusinessException("jwt解析错误");
    }
    }

    总结

    JWT实际上定义了一种数据加密与签名的规范,可以实现单点登录和数据传输及验签功能。该方案无法传递敏感信息(如密码),因为JWT中的部分内容可以解密,但不能修改。

    上一篇:neo区块链钱包sdk(nodejs版)
    下一篇:springboot 上传文件-feign內部調用

    发表评论

    最新留言

    路过按个爪印,很不错,赞一个!
    [***.219.124.196]2025年04月05日 18时31分57秒