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()方法的线程不安全性。找到症结了,就可以对症下药了,药方已在上面开出。

总结:

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

<think>好的,我现在需要回答用户的问题:如何在Java中实现接口调用前的权限校验,用户还提到希望了解具体的方法或框架。首先,我需要整理已有的知识,并结合提供的引用内容来构建一个全面的回答。 首先,用户提供的引用内容提到了几种方法,比如使用Spring Security与JWT集成,通过过滤器进行权限校验,以及在Gateway中设置GlobalFilter来处理请求头。这些都是常见的方法。我需要将这些方法系统地组织起来,确保回答结构清晰,符合用户的要求。 接下来,我应该考虑同的实现方式,可能包括框架的使用自定义过滤器。例如,Spring Security是一个强大的安全框架,可以处理认证授权。结合JWT(JSON Web Token)可以实现无状态的权限验证,这在分布式系统中非常有用。另外,自定义拦截器或过滤器也是一种常见做法,特别是在需要更细粒度控制。此外,API网关(如Spring Cloud Gateway)的全局过滤器可以统一处理权限校验,这对于微服务架构来说很重要。 然后,我需要确保回答中提到的每种方法都有具体的步骤或代码示例,这有助于用户理解如何实际操作。例如,引用[2]中提到了Spring Security集成JWT的案例,可以引用该博客作为参考。同时,引用[3]中的GlobalFilter实现也是一个很好的例子,说明如何在网关层面进行权限校验。 另外,用户可能对同的场景有同的需求,比如有些接口需要公开访问,而有些需要鉴权。这候,结合引用[3]中的ignore-url配置Feign的内部调用处理,可以解释如何排除特定接口的权限校验同时保证内部服务调用的安全。 需要注意的是,用户希望回答中使用中文,并且行内数学表达式用$...$格式,但在这个问题中可能太需要数学公式,所以可能需要涉及。但需要检查是否有其他格式要求,比如独立公式使用$$...$$,但这里可能也适用。确保代码块使用正确的格式,如示例中的Python代码,所以在Java示例中也需正确使用代码块。 最后,生成相关问题,这需要根据回答内容提出可能的延伸问题,帮助用户进一步学习。例如,如何选择适合的权限校验框架,或者如何集成JWT与Spring Security等。 总结回答的结构可能是: 1. 使用安全框架(如Spring Security + JWT) 2. 自定义拦截器/过滤器 3. API网关统一鉴权 4. 注解驱动权限控制 5. 结合具体案例(如引用中的Gateway过滤器) 每种方法需要简要说明实现步骤,并附上代码示例或配置示例,同时引用提供的参考资料。例如,在提到Spring Security,引用[2]中的博客链接作为进一步阅读材料。在讲解网关过滤器,引用[3]中的GlobalFilter实现。 需要确保回答中的引用标识正确添加,如[^2]、[^3]等,并且引用内容自然融入回答中,例如在提到JWT集成,说明参考了某个博主的文章。 可能还需要考虑其他框架,比如Apache Shiro,但根据提供的引用,主要围绕Spring生态,因此可能集中在Spring SecurityGateway的实现上。 检查是否有遗漏的方法,比如AOP(面向切面编程)实现权限校验,虽然可能如过滤器或框架直接,但也是一个可能性。过根据提供的引用内容,可能需要涉及,除非用户提到。 最后,确保回答符合用户要求的格式,分点清晰,代码示例正确,引用标识正确放置,并生成相关问题。</think>在Java中实现接口调用前的权限校验,主要有以下几种常用方法框架: --- ### 一、使用安全框架(Spring Security + JWT) **实现步骤**: 1. **集成Spring Security**:通过依赖注入配置安全规则。 2. **JWT生成与验证**:用户登录后生成Token,并在后续请求中验证Token有效性。 3. **权限注解**:使用`@PreAuthorize`等注解标注接口的访问权限。 **代码示例**: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/public/**").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())); } } ``` **注解控制权限**: ```java @GetMapping("/admin/data") @PreAuthorize("hasAuthority('ROLE_ADMIN')") public ResponseEntity<?> getAdminData() { // 业务逻辑 } ``` > 参考实现可结合JWT的详细配置。 --- ### 二、自定义拦截器/过滤器 **实现步骤**: 1. 实现`HandlerInterceptor`或`Filter`接口。 2. 在请求处理前校验Token或权限信息。 3. 通过`@RequestHeader`或参数解析获取权限标识。 **示例代码(过滤器)**: ```java @Component public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest req = (HttpServletRequest) request; String token = req.getHeader("Authorization"); if (!validateToken(token)) { throw new UnauthorizedException("权限足"); } chain.doFilter(request, response); } } ``` > 类似引用[3]中通过Header参数校验权限的实现。 --- ### 三、API网关统一鉴权 **场景**:微服务架构中,通过网关层统一拦截外部请求。 **实现方法**: 1. 在网关(如Spring Cloud Gateway)中配置`GlobalFilter`。 2. 清洗非法请求头或添加内部标识(如`X-Internal-Call`)。 **代码示例**: ```java @Component public class GatewayAuthFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String source = exchange.getRequest().getHeaders().getFirst("X-Source"); if ("external".equals(source)) { // 校验外部请求的权限 } return chain.filter(exchange); } } ``` > 类似引用[3]中网关过滤器的实现逻辑。 --- ### 四、注解驱动权限控制 **框架支持**: - **Spring Security**:通过`@Secured`、`@PreAuthorize`等注解。 - **Apache Shiro**:通过`@RequiresRoles`、`@RequiresPermissions`注解。 **Shiro示例**: ```java @RequiresRoles("user") @GetMapping("/user/profile") public ResponseEntity<?> getUserProfile() { // 业务逻辑 } ``` --- ### § 相关问题 § 1. **如何选择Spring SecurityApache Shiro?** 2. **JWT Token如何防止被篡改?** 3. **微服务架构中如何实现分布式权限管理?** 4. **如何通过AOP实现接口权限校验?** 5. **如何在网关层实现动态路由与权限绑定?** > 更多Spring Security与JWT集成的细节可参考[^2],网关过滤器的具体实现详见。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值