关于微服务鉴权的一种实现方式

关于微服务鉴权的一种实现方式,第1张

关于微服务鉴权的一种实现方式

此文章主要介绍客户端token与网关API的结合的鉴权实现方式。

这种方式要求每个请求的请求头或者参数里面必须携带token,所有请求必须经过网关,有效地隐藏了微服务。

目前比较常用的生成token的方式,一种是通过MD5的方式生成token,一种是JWT:base64编码信息+签名。

如选择MD5的方式生成token,借助redis作为缓存做判断,将token作为key,登录信息作为value,设置key失效时间做登录时效。

选择JWT,设置过期时间以及登录信息进行鉴权。

先来看第一种:

MD5的方式生成token
public class TokenGeneratorUtils {

    public static String generatevalue() {
        return generatevalue(UUID.randomUUID().toString());
    }

    private static final char[] HEX_CODE = "0123456789abcdef".toCharArray();

    public static String toHexString(byte[] data) {
        if (data == null) {
            return null;
        }
        StringBuilder r = new StringBuilder(data.length * 2);
        for (byte b : data) {
            r.append(HEX_CODE[(b >> 4) & 0xF]);
            r.append(HEX_CODE[(b & 0xF)]);
        }
        return r.toString();
    }

    public static String generatevalue(String param) {
        try {
            MessageDigest algorithm = MessageDigest.getInstance("MD5");
            algorithm.reset();
            algorithm.update(param.getBytes());
            byte[] messageDigest = algorithm.digest();
            return toHexString(messageDigest);
        } catch (Exception e) {
            throw new RenException("token invalid", e);
        }
    }
}

登录的时候保存在redis

private String refreshToken(SmsSysUserDTO userDTO) {
        //用户token
        String token;

        //当前时间
        Date now = new Date();
        //过期时间
        Date expireTime = new Date(now.getTime() + EXPIRE * 1000);

        //判断token是否过期
        if (userDTO.getExpireTime() == null || userDTO.getExpireTime().getTime() < System.currentTimeMillis()) {
            //token过期,重新生成token
            token = TokenGeneratorUtils.generatevalue();
        } else {
            token = userDTO.getToken();
        }
        userDTO.setToken(token);
        userDTO.setExpireTime(expireTime);
        userDTO.setLastLoginTime(new Date());
        smsSysUserService.saveOrUpdateDTO(userDTO);
        UserShareDTO userShareDTO = new UserShareDTO(userDTO.getId(), userDTO.getUsername(), token, SmsConstant.SmsSystem.SERVER.getValue());
        // 缓存token
        redisUtils.set(token, userShareDTO, EXPIRE);
        return token;
    }
JWT工具类

我的工具类主要是为了验证授权场景。

具体的业务场景:第三方系统跳转到我的系统,并且要将信息在我的系统首页展示出来,

具体的实现方案:

1、与第三方约定了一个密钥,利用AES对称加密先把信息和一个uuid通过post请求我的接口

2、我这边进行解密,解密成功则把信息存入数据库,并将uuid去封装JWT返回一个signId

3、第三方将signId和uuid代入url,前端解析url并请求我的鉴权接口,鉴权通过则拉取数据库中的信息进行展示

具体的JWT工具如下:

public class JwtTokenUtils implements Serializable {

    private static final long serialVersionUID = 1L;

    
    private static final String USERNAME = Claims.SUBJECT;

    public static final String USERNAME_VALUE = "ABCabc-username";

    
    private static final String SECRET = "TESTs-jwt";

    
    private static final String CREATED = "created";
    
    private static final String AUTHORITIES = "authorities";

    
    public static final long EXPIRE_TIME = 12 * 60 * 60 * 1000;


    
    public static final long EXPIRE = 5 * 60 * 1000;


    
    public static String generateToken(String uuid, Long expire) {
        Map claims = new HashMap<>(3);
        claims.put(USERNAME, USERNAME_VALUE);
        claims.put(CREATED, new Date());
        // 这里可以放一些鉴权用的信息
        claims.put(AUTHORITIES, uuid);
        return generateToken(claims, expire);
    }


    
    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }


    
    public static Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            throw new RenException("你的令牌无效或者过期");
        }
    }

    
    public static void verifyToken(String token, String uuid) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            throw new RenException("你的令牌无效或者过期");
        }
        if (isTokenExpired(token)) {
            throw new RenException("你的令牌无效或者过期");
        }
        String username = claims.getSubject();
        if (username == null) {
            throw new RenException("你的令牌无效或者过期");
        }
        Object authors = claims.get(AUTHORITIES);
        if (authors instanceof String) {
            String str = authors.toString();
            if (!str.equals(uuid)) {
                throw new RenException("你的令牌无效或者过期");
            }
        } else {
            throw new RenException("你的令牌无效或者过期");
        }
    }


    
    private static String generateToken(Map claims, long expire) {
        Date expirationDate = new Date(System.currentTimeMillis() + expire);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
    }

}
网关鉴权

以MD5生成token存redis为例:

具体步骤: 

1、过滤白名单

2、从请求头中拿token作为key,请求redis

3、封装参数

4、AES加密下发请求

5、封装当前登录用户对象,通过解析请求头中的参数来获取当前登录用户对象

@Component
@RefreshScope
public class GatewayFilter implements GlobalFilter, Ordered {

    @Autowired
    private RedisUtils redisUtils;

    
    @Value(value = "${white.urls}")
    private Set whiteUrls;

    
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        // 过滤白名单
        if (null != whiteUrls.stream().filter(path::contains).findAny().orElse(null)) {
             return chain.filter(exchange);
        }
        // 从header里取token
        String token = exchange.getRequest().getHeaders().getFirst("token");
        // 没有token 就阻止访问 抛未授权
        if (StringUtils.isBlank(token)) {
            return TokenException(exchange);
        }
        // 缓存token
        Map tokenMap = (Map) redisUtils.get(token);
        if (MapUtils.isEmpty(tokenMap)) {
            return TokenException(exchange);
        }
        if (MapUtils.isNotEmpty(tokenMap)) {
            // 封装 user
            UserShareDTO user = new UserShareDTO();
            user.setId(MapUtils.getLong(tokenMap, "id"));
            user.setToken(MapUtils.getString(tokenMap, "token"));
            user.setSystem(MapUtils.getString(tokenMap, "system"));
            user.setUsername(MapUtils.getString(tokenMap, "username"));
            if (StringUtils.isBlank(user.getSystem()) || StringUtils.isBlank(user.getToken())
                    || StringUtils.isBlank(user.getUsername())) {
                return TokenException(exchange);
            }
            return buildRequest(exchange, chain, JSON.toJSonString(user));
        }
         return chain.filter(exchange);
    }

    
    public Mono buildRequest(ServerWebExchange exchange, GatewayFilterChain chain, String auth) {
        String userAESStr = AESUtils.AESencode("abcdssdfsdfsafq", auth);
        ServerHttpRequest request = exchange.getRequest().mutate()
                .header("userInfo", userAESStr)
                .build();
        return chain.filter(exchange.mutate().request(request).build());
    }

    
    private Mono TokenException(ServerWebExchange exchange) {
        //1.获取响应对象
        ServerHttpResponse response = exchange.getResponse();
        //2.设置响应头类型
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        response.setStatusCode(HttpStatus.OK);
        JSonObject jsonObject = new JSonObject();
        jsonObject.put("msg", "你的令牌无效或者过期");
        jsonObject.put("code", HttpStatus.UNAUTHORIZED.value());
        jsonObject.put("data", false);
        DataBuffer wrap = response.bufferFactory().wrap(jsonObject.toJSonString().getBytes());
        return response.writeWith(Flux.just(wrap));
    }

    
    @Override
    public int getOrder() {
        return 0;
    }
}

当前登录用户对象工具

@Slf4j
public class ShareUtils {

    public static UserShareDTO user;

    public static UserShareDTO getUser() {
        updateInfo();
        return user;
    }

    public static synchronized void updateInfo() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String userInfoStr = request.getHeader("userInfo");
            String userInfo = AESUtils.AESdecode("abcdssdfsdfsafq", userInfoStr);
            if (StringUtils.isNotBlank(userInfo)) {
                user = JSON.parseObject(userInfo, UserShareDTO.class);
            } else {
                user = new UserShareDTO();
            }
            log.info("ShareUtils UserShareDTO ", user);
        } catch (Exception e) {
            log.error("ShareUtils HttpServletRequest request is null: " + e.getMessage());
            user = new UserShareDTO();
        }
    }
}

如果用这种鉴权方式,在feign调用的时候需要重写feign拦截器,会出现请求头丢失的情况

具体的解决方案可以参考我的这篇文章

我个人觉得这种鉴权方式灵活性较大,可以在网关redis鉴权后,通过参数的形式区分客户端、服务端或者内部请求、外部请求等等。同时可以结合mybaitis-plus做数据隔离。

烦请各路大神吗,指正点评一下这种方式的弊端或者需要改进的地方!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/zaji/5697082.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存