打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Oauth2 JWT登出(黑名单方案)
userphoto

2022.09.22 江苏

关注

每日中文

人必须疯狂一次,不管故事是一个人,一个人,一次旅行,还是一个梦想。

一个人的一生要疯狂一次,无论是为一个人,一段感情,经历,还是一个梦想。

每日掏心话

就像人生今天,没有任何结果,而是自己,而不是由己。如果,只有自己努力了,努力了,就好了。

责编:乐乐 |来自blog.csdn.net/lbjfish/article/details/109321044

编程技术圈(ID:study_tech)第1716 期推文

往日回顾:本科、硕士、博士的奖学金(不要太形象)

      正文     

  大家好,我是小乐

说明

先说一下,个人不建议使用JWT这种token,还是建议使用RedisTokenStore方案,成熟。因为JWT是无状态的,这是它的优点,如果强方式还行给它加比较状态,那不如用Session方案。

不能控制出功能,是官方无法控制的JwtTokenStore.removeAccessToken一个方法;比如实现续签功能,因为同是不能控制的一种方法;一个,每次调用类似访问的重要,返回 JWT 的标记都不是一样的(是 jwt 算法,的时间,的用户,jwt 变)。

Oauth2官方提供JWT明明告诉你,也不建议做登出和续签,它适合后台管理系统的内部登录系统,如果几乎不用这种方式退出和续签的情况,也可以退出,也很简单,可以直接清掉浏览器上的token cookie

总结黑名单方案有具体实现方式(都是基于重新发布):

  • ResJwtAccessTokenConverter中新建子RedisJwtClaimsSetVerifier并实现JwtClaimsSetVerifier接口。主要是类verify()方法。

  • ResJwtAccessTokenConverterJWTfaultUserAuthenticationConverter主要是继承DefaultAccessTokenConverter类。中子类是方法并取得jti extractAuthentication()map.get(“jti”)

  • 实现过滤器LogoutAuthenticationFilter,在调登录接口实现前,判断jti。

那么我更容易实现第一个,已经可以比其他高,因为第一个是登录前判断好,实现方式,所以是auth2的登录逻辑代码,可以低效,选择还是看自己,遇到困难很简单。

黑名单方式是不适合扩展续签功能的,后面我将执行续签的方式,并解决续签方式,履行在线踢人功能。

扩展JwtTokenStore

如果白领storeAccessToken队需要方法,如果任务回复需要removeAccessToken方法,这里是挑选黑科技,那么当需要在removeAccessToken把方法拿来方法的时候,把替身是黑底redis内的,把钥匙当后台是值redis内。 “网上物联网平台”,获取一份惊喜礼包。

@Slf4j
public class CustomJwtTokenStore extends JwtTokenStore {
    private RedisUtil redisUtil;

    public CustomJwtTokenStore(JwtAccessTokenConverter jwtTokenEnhancer, RedisUtil redisUtil) {
        super(jwtTokenEnhancer);
        this.redisUtil = redisUtil;
    }


    /**
     * 这个暂时没用,因为我用的黑名单, 白名单需要
     * @param token
     * @param authentication
     */

    @Override
    public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        super.storeAccessToken(token, authentication);
    }

    @Override
    public void removeAccessToken(OAuth2AccessToken token) {
        if (token.getAdditionalInformation().containsKey('jti')) {
            String jti = token.getAdditionalInformation().get('jti').toString();
//            redisUtil.del(jtiRedisKey(jti));
            int expire = token.getExpiresIn();
            if(expire < 0){
                expire = 1;
            }
            redisUtil.set(jtiRedisKey(jti), token.getValue(), expire);
        }
        super.removeAccessToken(token);
    }

    private String jtiRedisKey(String jti) {
        return SecurityConstants.CACHE_EXPIRE_TOKEN_BLACKLIST + ':' + jti;
    }
}

如果必须证明是否存在许可,则需要证明是否存在许可,则提示前端是否存在许可。

下面来实现优化登出方式并分析我的改进。扩展:快速开发平台

方案一(验证()方法)

该类在ResJwtAccessTokenConverter中的子类中RedisJwtClaimsSetVerifier 。代码:

public class RedisJwtClaimsSetVerifier implements JwtClaimsSetVerifier {
 /**
  * 检查jti是否在Redis中存在(即是否过期),如过期则抛异常
  */

 @Override
 public void verify(Map<String, Object> claims) throws InvalidTokenException {
  if (claims.containsKey('jti')) {
   String jti = claims.get('jti').toString();
   Object value = redisUtil.get(jtiRedisKey(jti));
   if (value != null) {
                  //最好继承InvalidTokenException重新封装一个异常
    throw new BusinessException('Invalid token!');
   }
  }
 }

 private String jtiRedisKey(String jti) {
  return SecurityConstants.CACHE_EXPIRE_TOKEN_BLACKLIST + ':' + jti;
 }
}

该在黑指挥部解决问题,但发现问题,发现BusinessException(“Invalid token!”)异常,发现内部JwtAccessTokenConverter.decode(String token)最终会调出我上面的verify()方法,verify()结果发现异常,方法异常,无法解决问题,会返回我想要的异常信息(这是我的异常Invalid token!),会返回解码自定义返回的异常信息(Cannot convert access token to JSON),其根源如下:

//源码:
protected Map<String, Object> decode(String token) {
        try {
            Jwt jwt = JwtHelper.decodeAndVerify(token, this.verifier);
            String claimsStr = jwt.getClaims();
            Map<String, Object> claims = this.objectMapper.parseMap(claimsStr);
            if (claims.containsKey('exp') && claims.get('exp'instanceof Integer) {
                Integer intValue = (Integer)claims.get('exp');
                claims.put('exp'new Long((long)intValue));
            }

            this.getJwtClaimsSetVerifier().verify(claims);
            return claims;
        } catch (Exception var6) {
           //这里会捕获我的异常,返回它的异常信息
            throw new InvalidTokenException('Cannot convert access token to JSON', var6);  
        }
    }

要解决这种异常的信息,只需要在上面判断异常类型的异常信息时,才需要返回此类异常信息类型中的特定CustomAuthenticationEntryPoint异常BusinessException,可能会有一次异常的业务,最好继承InvalidTokenException一个异常的人物。

@Slf4j
public class CustomAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
       //判断我的异常信息(BusinessException('Invalid token!')) start -------
        if(e.getCause().getCause() instanceof BusinessException){  //最好继承InvalidTokenException重新封装一个异常
            ResponseUtil.responseWriter(false, objectMapper, response, e.getCause().getCause().getMessage(), HttpStatus.UNAUTHORIZED.value());
        }
       //判断我的异常信息(BusinessException('Invalid token!')) end -------
        ResponseUtil.responseWriter(false, objectMapper, response, e.getMessage(), HttpStatus.UNAUTHORIZED.value());
    }

}

该方案很好,但似乎有可能在鉴定库中删除,会下降。扩展:最全的java题

方案二

该类在ResJwtAccessTokenConverter中子类的子类JWTfaultUserAuthenticationConverter中。代码如下:

public class JWTfaultUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
   private Collection<? extends GrantedAuthority> defaultAuthorities;

   @Override
   public Authentication extractAuthentication(Map<String, ?> map) {
                //黑名单代码开始
    if(map.containsKey('jti')){
     String jti = (String) map.get('jti');
     Object value = redisUtil.get(jtiRedisKey(jti));
     if (value != null) {
      throw new InvalidTokenException('Invalid token!');
     }
    }
                //黑名单代码结束
    if (map.containsKey(USERNAME)) {
     Object principal = map.get(USERNAME);
    // Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
     LoginAppUser loginUser = new LoginAppUser();
     if (principal instanceof Map) {

      loginUser = BeanUtil.mapToBean((Map) principal, LoginAppUser.classtrue);
       

     }
     return new UsernamePasswordAuthenticationToken(loginUser, 'N/A', loginUser.getAuthorities());
    }
    return null;
   }
  }

该性能方案甚至是第一种方案,但至少比第一种方案合理一些。

牛啊逼私活必备的N个开源项目!收藏接!

方案三(过滤器)

该主要先判断在没有jti的token之前,重新判断是在没有jti的token,目录里有说明,直接返回给前端,如果没有单独走黑鉴定权流程,比较高。

该方案中没有分为四种情况,如果你使用网关(Spring Cloud Gateway)鉴权,则走普通过滤器,如果使用网关鉴权,则走网关WebFlux鉴权。

普通服务过滤器
/**
 * lbj
 * 各微服务 获取token时,先判断此token是否在黑名单(退出会保存在redis)中,如果在,则抛异常,否则不做处理
 */

@Slf4j
//@ConditionalOnProperty(name = 'myyshop.logoutFilter.enabled', havingValue = 'true')
public class LogoutAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader('Authorization');
        if(Strings.isBlank(token) || !token.startsWith(CommonConstant.BEARER_TYPE)){
            filterChain.doFilter(request, response);
        }

        String authHeaderValue = AuthUtils.extractToken(request);
        log.info('authHeaderValue={}', authHeaderValue);
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(authHeaderValue);
        if (accessToken.getAdditionalInformation().containsKey('jti')) {
            String jti = accessToken.getAdditionalInformation().get('jti').toString();
            Object value = redisUtil.get(jtiRedisKey(jti));
            if (value != null) {
                ResponseUtil.responseWriter(false, objectMapper, response, 'this is Invalid token!', HttpStatus.UNAUTHORIZED.value());
                return;
            }
        }
        filterChain.doFilter(request, response);
    }

    private String jtiRedisKey(String jti) {
        return SecurityConstants.CACHE_EXPIRE_TOKEN_BLACKLIST + ':' + jti;
    }
}
Spring Cloud 网关过滤器
/**
 * lbj
 * Webflux 各微服务 获取token时,先判断此token是否在黑名单(退出会保存在redis)中,如果在,则抛异常,否则不做处理
 */

public class LogoutAuthenticationWebFilter implements WebFilter {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private TokenStore tokenStore;


    @Override
    public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {

        ServerHttpRequest request = serverWebExchange.getRequest();
        String token = request.getHeaders().getFirst('Authorization');
        if(Strings.isBlank(token) || !token.startsWith(CommonConstant.BEARER_TYPE)){
            return webFilterChain.filter(serverWebExchange);
        }

        String authHeaderValue = token.substring(CommonConstant.BEARER_TYPE.length()).trim();
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(authHeaderValue);
        if (accessToken.getAdditionalInformation().containsKey('jti')) {
            String jti = accessToken.getAdditionalInformation().get('jti').toString();
            Object value = redisTemplate.opsForValue().get(jtiRedisKey(jti));
            if (value != null) {
                return WebfluxResponseUtil.responseFailed(serverWebExchange, HttpStatus.UNAUTHORIZED.value(), 'Webflux this is Invalid token!');
            }
        }
        return webFilterChain.filter(serverWebExchange);
    }

    private String jtiRedisKey(String jti) {
        return SecurityConstants.CACHE_EXPIRE_TOKEN_BLACKLIST + ':' + jti;
    }
}

有需要的同学,如果本文对您有帮助,也请帮忙点个 赞+在 看啦!❤️

你还有什么想要补充的吗?

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
认证鉴权与API权限控制在微服务架构中的设计与实现(二)
设计安全的API-JWT与OAuthor2
OAuth2.0实战!使用JWT令牌认证!
微信企业号开发步骤
应用JWT进行用户认证及Token的刷新
spring security oauth2 自定义实现令牌存储
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服