说一说java中的自定义注解之设计及实现

一、需求背景

比如我们需要对系统的部分接口进行token验证,防止对外的接口裸奔。所以,在调用这类接口前,先校验token的合法性,进而得到登录用户的userId/role/authority/tenantId等信息;再进一步对比当前用户是否有权限调用该接口。

但是,不是所有的接口都需要token校验,我们应该按需配置,能够支持排除掉无需token校验的接口。

本文的重点是讲述,如果让业务方开启token校验,不会涉及到如何去做权限及接口配置等方面。

因为,接口配置,我们是建议放在api网关层(不应该放在具体的微服务里),而实际生产中,不同的业务会有不同的api网关。

二、目标

  • 接口支持token校验与否的开关控制
  • 编写一个自定义注解
  • 对业务方透明,简单易用

三、总体设计

  • 建议的方案
    说一说java中的自定义注解之设计及实现_第1张图片
  • 本文所说的方案
    说一说java中的自定义注解之设计及实现_第2张图片

四、注解的定义

  • EnableJwtAuth.java
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableJwtAuthImportSelector.class})
public @interface EnableJwtAuth {

}
  • 增加开关
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableJwtAuthImportSelector.class})
@ConditionalOnProperty(name = "spring.jwt.enabled", havingValue = "true", matchIfMissing = true)
public @interface EnableJwtAuth {

}
  • EnableJwtAuthImportSelector.java

import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

// 实现接口EnvironmentAware和ImportSelector
public final class EnableJwtAuthImportSelector implements ImportSelector, EnvironmentAware {

    private Environment environment;

    protected Environment getEnvironment() {
        return this.environment;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

// 读取配置项spring.jwt.enabled的值,默认是开启jwt校验
    protected boolean isEnabled() {
        return getEnvironment().getProperty("spring.jwt.enabled", Boolean.class, Boolean.TRUE);
    }

    @Override
    public final String[] selectImports(AnnotationMetadata importingClassMetadata) {
        if (!isEnabled()) {
            return new String[0];
        }

        Class<EnableJwtAuth> annoType = EnableJwtAuth.class;
        Map<String, Object> annotationAttributes = importingClassMetadata
                .getAnnotationAttributes(annoType.getName(), false);
        AnnotationAttributes attributes = AnnotationAttributes
                .fromMap(annotationAttributes);
        
        Assert.notNull(attributes, String.format(
                "@%s is not present on importing class '%s' as expected",
                annoType.getSimpleName(), importingClassMetadata.getClassName()));

// 实例化两个类
        List<String> classNames = new ArrayList<>(2);
        classNames.add(GsonAutoConfiguration.class.getName());
        // JwtAuthConfiguration是我们自定义的类
        classNames.add(JwtAuthConfiguration.class.getName());
     
        return classNames.toArray(new String[0]);
    }

}
  • JwtAuthConfiguration.java
    此类中定义你所需要的java类。
    另外一点,如果你需要对接口uri进行过滤,也需要在这里进行校验。
    
    @Bean
    public JwtAuthenticationProvider jwtAuthenticationManager() {
        return new JwtAuthenticationProvider();
    }
  • JwtAuthenticationProvider.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.util.StringUtils;

/**
 * 依赖spring security 权限框架
 *
 * @author xxx
 */
public class JwtAuthenticationProvider implements AuthenticationProvider {

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationProvider.class);

   
    @Override
    public boolean supports(Class<?> authentication) {
        return JwtAuthenticationToken.class.isAssignableFrom(authentication);
    }

    @Override
    public Authentication authenticate(Authentication authenticationToken)  {
        final String authToken = (String) authenticationToken.getCredentials();
        if (StringUtils.isEmpty(authToken)) {
            throw new AuthenticationCredentialsNotFoundException(MessageDefs.TOKEN_MISSING);
        }

        // 调用认证服务进行token校验,示意图见上面的总体设计
        // 下面是伪代码,自定义类JwtUser用于包装用户信息
        JwtUser userDetails = tokenVerifyClient.verify(authToken);
    
        // 取出需要的字段,传递给TransmittedUserInfo透传对象
        TransmittedUserInfo userInfo = new TransmittedUserInfo(userDetails);

        XxTransmittableThreadLocal<TransmittedUserInfo> xxTransmittableThreadLocal = (XxTransmittableThreadLocal<TransmittedUserInfo>) ApplicationContextProvider
                .getApplicationContext()
                .getBean("xxTransmittableThreadLocal");
                
        xhTransmittableThreadLocal.set(userInfo);

        // 校验成功
        return new JwtAuthenticationToken(userDetails.getId(), null, userDetails.getAuthorities());
    }

}

五、自定义注解的使用

说一说java中的自定义注解之设计及实现_第3张图片

六、说在最后的话

自定义注解,本身比较简单,这里使用了@Import注解,故不需要再在META-INF/spring.factories增加org.springframework.boot.autoconfigure.EnableAutoConfiguration配置类。

我这里想要补充说明的是,文章里的权限校验,只是一种实现方案。
更建议你在api网关中实现token的校验。

那么,java服务中,需要做哪些工作呢?

把api网关传过来的字段,很好地传承并透传至下游服务。

还有一个重要的工作就是,取出当前服务上下文中的数据,做以下工作:

  • 用户ID是否和token一致,防止token被冒用
  • 接口的权限(判断当前用户是否能够访问该接口)
  • 取出当前用户的角色、租户ID、用户ID、学校ID等关键字段,保存到数据库,并输出打印日志。

你可能感兴趣的:(java,开发语言,数据库)