java-security拦截时同时调用多个接口,其中随机部分接口校验不通过问题的定位和解决

前言:

又是普通的一天。最近在搞权限配置,用的是Spring security,token是用的jwt,通过服务中心签发token,业务层进行权限的配置。

问题:

权限这块配置上去了,发现在同时调用多个请求的时候,经常会有1到n个接口报错,报错显示是认证未通过。并且单个接口测试都没问题。

先说最终的问题原因:

由于每次作token校验的时候,会使用jjwt的parse方法进行解析, 解析时需要传入rsa的公钥,而这公钥是需要每次进行临时写入系统文件之后再读取的,由于并发量高(也就几个),对同一文件的多个写入冲突了,导致获得公钥失败,从而导致解析失败。
以下是获得公钥的方法:

public static PublicKey getPublicKey() throws Exception {

        ClassPathResource resource = new ClassPathResource("rsa/rsa_public_key");
        // 临时目录
        String tempPath = System.getProperty("java.io.tmpdir") + System.currentTimeMillis() + ".rsa_public_key";
        File f = new File(tempPath);
        if (!f.exists()) {
            InputStream inputStream = null;
            try {
                inputStream = resource.getInputStream();
                IOUtils.copy(inputStream, new FileOutputStream(f));
                inputStream.close();
            } catch (IOException e) {
                System.out.println("fuckfuckfuck");
                e.printStackTrace();
            }
        }else{
            log.info("公钥已存在.公钥目录临时存储路径" + tempPath);
        }

        return getPublicKey(readFile(f));
    }

解决办法:

由于多次对同一文件进行写入,还得写入临时文件,效率还比较低,效率低就算了,还造成了这样的问题,索性改成创建静态变量维护byte[],这样就解决了:

 private static byte[] PUBLIC_KEY;

    static {
        ClassPathResource resource = new ClassPathResource("rsa/rsa_public_key");
        // 临时目录
        String tempPath = System.getProperty("java.io.tmpdir") + ".rsa_public_key";
        File f = new File(tempPath);
        if (!f.exists()) {
            try {
                IOUtils.copy(resource.getInputStream(), new FileOutputStream(f));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            PUBLIC_KEY = readFile(f);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
     public static PublicKey getPublicKey() throws Exception {
        return getPublicKey(PUBLIC_KEY);
    }

问题的定位、解决过程

人生就像一场旅行 不必在意目的地 在乎的是沿途的风景和看风景的心情-利群

同理,相比与一个问题的解决,其实问题的定位、和解决的过程也是很重要的,它能够指导你下次碰到其他问题时也能自己去解决。(这也是为什么我们要不断总结、反思)我在这次走了很多弯路,一度认为自己没法解决了,于是请教同事,才找到了解决问题的路,跟着这条路走,问题果然解决了,那么接下来再走一遍这条路。

我们security里添加了两个filter,应对两种不同的token参数,而这个情况是在用户token的过滤时发生的,看下代码:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Value("${jwt.header}")
    private String token_header;

    @Resource
    private JwtUtils jwtUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String auth_token = request.getHeader(this.token_header);
//        final String auth_token_start = "safept ";
//        if (StringUtils.isNotEmpty(auth_token) && auth_token.startsWith(auth_token_start)) {
//            auth_token = auth_token.substring(auth_token_start.length());
//        }
        if (StringUtils.isEmpty(auth_token)) {
            // 不按规范,不允许通过验证
            chain.doFilter(request, response);
//            auth_token = null;
            return;
        }

        String username = jwtUtils.getUsernameFromToken(auth_token);

        if (jwtUtils.containToken(username, auth_token) && username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            SysUser userDetail = jwtUtils.getUserFromToken(auth_token);
            RequestHolder.addUser(userDetail);
            if (jwtUtils.validateToken(auth_token, userDetail)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                logger.info(String.format("Authenticated userDetail %s, setting security context", username));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }
}

基于之前对security filter链的理解,可以知道,用户登陆认证失败,其实就是这边的authentication没有设置进去导致的,导致其没有set进去可能会是上面的三个判断失败导致:

 if (StringUtils.isEmpty(auth_token)) {
            // 不按规范,不允许通过验证
            chain.doFilter(request, response);
            return;
        }

token每次传入都正确,所以这边不会是原因。

剩下两个就是上面的两个if:

      if (jwtUtils.containToken(username, auth_token) && username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

第三个判断条件不用看,就是当目前无authentication时再set,如果有的话,其实就是已经获得用户登陆认证了。

以及

if (jwtUtils.validateToken(auth_token, userDetail)) {

然后看里面的几个方法:

jwtUtils.containToken(username, auth_token)
  public boolean containToken(String userName, String token) {
        if (userName != null && tokenMap.containsKey(userName) && tokenMap.get(userName).equals(token)) {
            return true;
        }
        return false;
    }

之前维护了一个tokenMap存储用户信息,实际上是没有必要的。并且在负载均衡的情况下可能会出问题,果断把这玩意给去了。

username != null
String username = jwtUtils.getUsernameFromToken(auth_token);
public SysUser getUserFromToken(String token) {
        SysUser userDetail;
        try {
            final Claims claims = getClaimsFromToken(token);
            long userId = Long.parseLong(String.valueOf(claims.get(CLAIM_KEY_USER_ID)));
            String username = claims.getSubject();
            List<String> authNameList = (List<String>) claims.get(CLAIM_KEY_AUTHORITIES);
            List<SysAuth> auths = new ArrayList<>();
            for (String s : authNameList) {
                SysAuth auth = new SysAuth();
                auth.setName(s);
                auths.add(auth);
            }
            userDetail = new SysUser();
            userDetail.setAuths(auths);
           业务代码省略...
        } catch (Exception e) {
            userDetail = null;
        }
        return userDetail;
    }

再看关键的getClaimsFromToken方法:

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

这边调用的是jjwt包里的parser方法,通过传入公钥进行token的签名认证,并获得body数据。看起来好像没什么问题,那么继续看getPublicKey()方法:
上面也已经列过了,这里再列一遍

public static PublicKey getPublicKey() throws Exception {

        ClassPathResource resource = new ClassPathResource("rsa/rsa_public_key");
        // 临时目录
        String tempPath = System.getProperty("java.io.tmpdir") + System.currentTimeMillis() + ".rsa_public_key";
        File f = new File(tempPath);
        if (!f.exists()) {
            InputStream inputStream = null;
            try {
                inputStream = resource.getInputStream();
                IOUtils.copy(inputStream, new FileOutputStream(f));
                inputStream.close();
            } catch (IOException e) {
                System.out.println("fuckfuckfuck");
                e.printStackTrace();
            }
        }else{
            log.info("公钥已存在.公钥目录临时存储路径" + tempPath);
        }

        return getPublicKey(readFile(f));
    }

可以发现,它是写通过ClassPathResource获得相对路径下的公钥文件,然后写入系统文件里,最终通过readFile方法拿到由公钥转换成的byte[],再调用getPublicKey方法,获得一个序列化的key。而这边的写入,可能就是线程不安全的!

别忘了validateToken方法:

 public Boolean validateToken(String token) {
//        final Date created = getCreatedDateFromToken(token);
        return (!isTokenExpired(token)
//                && !isCreatedBeforeLastPasswordReset(created, userDetail.getLastPasswordResetDate())
        );
    }
 private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

代码很好理解,最终也是会调用getClaimsFromToken方法去解析token。所以问题的核心就是getPublicKey()方法的线程不安全性。找到症结了,就可以对症下药了,药方已在上面开出。

总结:

碰到问题,要有追根溯源的意识,这点以后还要多多加强。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值