Skip to content
章节导航

微服务鉴权中心客户端

鉴权中心客户端: fly-auth-client

pom 文件依赖

xml
<dependencies>
    <dependency>
        <groupId>com.github.itdachen.framework</groupId>
        <artifactId>fly-core</artifactId>
    </dependency>

    <dependency>
        <groupId>com.github.itdachen.framework</groupId>
        <artifactId>fly-body-advice</artifactId>
    </dependency>

    <!-- openfeign 远程服务调用模块, openfeign 封装, RestTemplate 封装 -->
    <dependency>
        <groupId>com.github.itdachen.framework.cloud</groupId>
        <artifactId>fly-cloud-openfeign</artifactId>
    </dependency>

    <!-- token 解析模块 -->
    <dependency>
        <groupId>com.github.itdachen.framework.cloud</groupId>
        <artifactId>fly-cloud-jwt-parse</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-openfeign-core</artifactId>
    </dependency>

    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
    </dependency>

</dependencies>

获取 token 公钥

openfeign 获取公钥

新建 IAuthorizedClientTokenSecretFeign 接口, 获取 token 公钥

@FeignClient(value = "${fly.cloud.auth.app.service-id}", configuration = {})
public interface IAuthorizedClientTokenSecretFeign {

    @RequestMapping(value = "/auth/client/user/secret/key", method = RequestMethod.GET)
    ServerResponse<String> getSecretPublicKey(@RequestParam("appId") String appId,
                                              @RequestParam("appSecret") String appSecret) throws Exception;
    
}
  • fly.cloud.auth.app.service-id: 配置文件配置的认证中心服务名称
  • 需要对 feign 远程调用有一定的了解

同步 token 公钥

新建 AuthorizedClientTokenSecretRunner 类, 项目启动时, 初始化 Token 公钥

public class AuthorizedClientTokenSecretRunner implements CommandLineRunner {
    private static final Logger logger = LoggerFactory.getLogger(AuthorizedClientTokenSecretRunner.class);

    @Autowired
    private AuthClientTokenSecretKey authClientTokenSecretKey;

    private final IAuthorizedClientTokenSecretFeign clientTokenSecretFeign;
    private final FlyCloudAppClientProperties appClientProperties;

    public AuthorizedClientTokenSecretRunner(IAuthorizedClientTokenSecretFeign clientTokenSecretFeign,
                                             FlyCloudAppClientProperties appClientProperties) {
        this.clientTokenSecretFeign = clientTokenSecretFeign;
        this.appClientProperties = appClientProperties;
    }

    @Override
    public void run(String... args) throws Exception {
        logger.info("正在初始化用户 pubKey");
        try {
            refreshUserSecretKey();
            logger.info("初始化用户 pubKey 完成");
        } catch (Exception e) {
            logger.error("初始化用户 pubKey 失败, 1 分钟后自动重试!", e);
        }
    }

    /***
     * 每隔一分钟同步一次
     *
     * @author 王大宸
     * @date 2023/5/5 15:09
     * @return void
     */
    @Scheduled(cron = "0 0/1 * * * ?")
    public void refreshUserSecretKey() throws Exception {
        ServerResponse<String> res = clientTokenSecretFeign.getSecretPublicKey(
                appClientProperties.getAppId(),
                appClientProperties.getAppSecret()
        );
        if (res.getSuccess()) {
            this.authClientTokenSecretKey.setTokenPublicKey(res.getData());
        }
    }
    
}

配置 token 解析拦截器

新建 DefaultBootstrapWebMvcConfigurer 类, 配置 token 解析拦截器

public class DefaultBootstrapWebMvcConfigurer implements WebMvcConfigurer {
    private static final Logger logger = LoggerFactory.getLogger(FlyCloudBootstrapWebMvcConfig.class);
    private final IVerifyTicketTokenHelper verifyTicketTokenService;
    private final IRequestPassMatchers requestPassMatchers;

    public FlyCloudBootstrapWebMvcConfig(IVerifyTicketTokenHelper verifyTicketTokenService,
                                         IRequestPassMatchers requestPassMatchers) {
        this.verifyTicketTokenService = verifyTicketTokenService;
        this.requestPassMatchers = requestPassMatchers;
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }

    /***
     * 拦截器配置
     *
     * @author 王大宸
     * @date 2022/9/25 16:28
     * @param registry registry
     * @return void
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns(requestPassMatchers.passMatchers());
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userAuthRestMethodArgumentResolver());
    }

    @Bean
    public UserAuthRestMethodArgumentResolver userAuthRestMethodArgumentResolver() {
        return new UserAuthRestMethodArgumentResolver();
    }

    /***
     * token 解析拦截器
     *
     * @author 王大宸
     * @date 2023/9/3 17:30
     * @return com.github.itdachen.framework.cloud.jwt.parse.interceptor.UserAuthRestInterceptor
     */
    @Bean
    public UserAuthRestInterceptor authInterceptor() {
        return new UserAuthRestInterceptor(verifyTicketTokenService);
    }

}

配置默认的拦截器 bean

@Configuration
public class DefaultWebMvcBeanConfigurer {

    /**
     * 解析 token 接口
     */
    private final IVerifyTicketTokenHelper verifyTicketTokenService;

    /**
     * 获取不拦截的路径
     */
    private final IRequestPassMatchers requestPassMatchers;

    public DefaultWebMvcBeanConfigurer(IVerifyTicketTokenHelper verifyTicketTokenService,
                                       IRequestPassMatchers requestPassMatchers) {
        this.verifyTicketTokenService = verifyTicketTokenService;
        this.requestPassMatchers = requestPassMatchers;
    }

    /***
     * 配置默认的 WebMvcConfigurer 配置
     * 如果有需要, 可以重新写一个类, 实现 WebMvcConfigurer 中的接口即可
     * @author 王大宸
     * @date 2023/9/3 17:56
     * @return org.springframework.web.servlet.config.annotation.WebMvcConfigurer
     */
    @Bean
    @ConditionalOnMissingBean(WebMvcConfigurer.class)
    public WebMvcConfigurer smsCodeSender() {
        return new DefaultBootstrapWebMvcConfigurer(verifyTicketTokenService,requestPassMatchers );
    }

}

添加认证客户端注解

新建 EnableAuthClient 注解, 客户端启动时, 需要在 Spring Boot 启动类上添加 @EnableAuthClient 注解

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

}

注解启动时, 加载 AuthorizedClientTokenSecretRunner

  • AuthorizedClientTokenSecretRunner: 从认证中心获取 token 公钥
  • DefaultBootstrapWebMvcConfigurer: 添加系统默认拦截器, 在拦截器中解析 token 信息
  • 如果需要自定义加载 token 公钥, 启动类上不需要添加 @EnableAuthClient 注解
  • token 解析公钥, 在项目初始化时, 需要存放在 AuthClientTokenSecretKey 类中

服务端接口

微服务鉴权中心服务端 fly-auth-server, 编写获取公钥接口

@RestController
@IgnoreUserToken  // 忽略 token 注解
@RequestMapping(value = "/client")
public class AuthAppClientUserKeyController {
    private static final Logger logger = LoggerFactory.getLogger(AuthAppClientUserKeyController.class);

    private final AuthClientTokenSecretKey authClientTokenSecretKey;

    public AuthAppClientUserKeyController(AuthClientTokenSecretKey authClientTokenSecretKey) {
        this.authClientTokenSecretKey = authClientTokenSecretKey;
    }


    /***
     * 客户端同步用户公钥
     *
     * @author 王大宸
     * @date 2023/5/5 15:01
     * @param appId       客户端ID
     * @param appSecret   客户端秘钥
     * @return com.github.itdachen.framework.core.response.ServerResponse<java.lang.String>
     */
    @GetMapping("/user/secret/key")
    public ServerResponse<String> userSecretKey(@RequestParam(value = "appId") String appId,
                                                @RequestParam(value = "appSecret") String appSecret) throws Exception {

        logger.info("获取客户端 token 公钥, appId: {}, appSecret: {}", appId, appSecret);

        // TODO 自行根据 appId 和 appSecret 校验是否能获取 token 公钥

        return ServerResponse.okData(authClientTokenSecretKey.getTokenPublicKey());
    }

}

测试

新建 fly-auth-client-dome 模块

pom 依赖

xml
<dependencies>
    <dependency>
        <groupId>com.github.itdachen.framework</groupId>
        <artifactId>fly-runner</artifactId>
    </dependency>

    <!-- 认证中心客户端 -->
    <dependency>
        <groupId>com.github.itdachen</groupId>
        <artifactId>fly-auth-client</artifactId>
    </dependency>

    <!-- 項目启动容器 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>

    <!-- nacos discovery -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <!-- 新版本的微服务默认不加载 bootstrap.yml 文件, 需要添加 bootstrap 依赖 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>

    <!-- 健康检查 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

</dependencies>

启动类

@EnableAuthClient  // 启动认证客户端注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients({"com.github.itdachen"})
@ComponentScan(basePackages = {"com.github.itdachen"})
public class AuthClientDemoBootstrap {

    public static void main(String[] args) {
        SpringBootBootstrap.run(AuthClientDemoBootstrap.class);
    }

}

配置文件 bootstrap.yml

yaml
# 端口配置
server:
  port: 8002
  servlet:
    context-path: /${spring.application.name}

# 认证中心配置
fly:
  cloud:
    auth:
      token:
        type: rsa # 指定 token 类型
      app:
        service-id: auth # 认证中心服务名称
        app-id: ${spring.application.name}
        app-secret: 123456


spring:
  application:
    name: auth-client-demo
  main:
    allow-circular-references: true
    allow-bean-definition-overriding: true

  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

  cloud:
    loadbalancer:
      # 重试策略
      retry:
        maxRetriesOnSameServiceInstance: 0
        maxRetriesOnNextServiceInstance: 0
      clients:
        service-product:
          retry:
            maxRetriesOnSameServiceInstance: 0
            maxRetriesOnNextServiceInstance: 0
    ## 向 nacos 发起注册
    nacos:
      discovery:
        username: nacos
        password: nacos
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        server-addr: 127.0.0.1:8848
        namespace: 07656b3c-bb69-470a-a942-6ae27b5287cb
        group: FLY_GROUP
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: ALWAYS

启动项目, 控制台打印如下信息

项目启动后, 服务端控制台打印日志

获取客户端 token 公钥, appId: auth-client-demo, appSecret: 123456

编写 Controller

编写 Controller 测试获取用户信息

@RestController
@RequestMapping("/current/user")
public class CurrentUserController {

    @GetMapping("/details")
    public ServerResponse<CurrentUserDetails> userDetails(@CurrentUser CurrentUserDetails userDetails) {
        return ServerResponse.okData(userDetails);
    }

}

编写 http 请求文件

### 测试登录用户
GET http://localhost:8002/auth-client-demo/current/user/details
Accept: application/json
Verified-Ticket: verifiedTicket
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzUxMiJ9.eyJuaWNrTmFtZSI6IueOi-Wkp-WuuCIsInVzZXJUeXBlIjoiNCIsInVzZXJJZCI6IjEiLCJhY2NvdW50IjoiYWRtaW4iLCJpc3MiOiJjb20uZ2l0aHViLml0ZGFjaGVuIiwiaWF0IjoxNjkzNzM2ODE5LCJleHAiOjE2OTM3NTQ4MTl9.ljf1uPVIfSdhU3QBplZh1X1sJEMKM0mYXQ-eJPkmwpLA8zo8my8zPoHJCCRlf_9ZjGhZC4OAQceAYAQXoTRVUbV8MC8ZuFg1zuz0OJW_rw0u9P5yGlc5W5sMhI70QJ42zVBkb4NiOwCZ8UPqMqHoE5eNtbPTQcUy1RX98iGT7t7d0In8bxBnaa38Euq7CiBul4lOE7hJqkhZDFuC-ZTzWAeZbAmReCirzom0Jtvyjb53u3TW_A4Hcd5FbXyo6p2zfrokj44yqI-JB76BMlm64_Rp01BkCaNfWNyd6a-0UEL1fEoKdLmEU5kZKBBm8xcNyylsSQUVTq_fI8bnzW-17_d6-KMa8YLDnEbbpxAthEw3vVv5Cbg7PYwTIt3vY-bx3NVmsUlvx2MKOsU8MdRWda4Q-hfr5RR_t-EAKdS2Q2EuNh-3jf64ljE1sNaL1RN_qj3g1psUVB5u9SDbDfe2lfVFGwN79hjYIWq2XMu_FApUM28SKg-VM1ZiMxIq4a8t3AQR7NX7WLTVEfutYbc6uwkpiqGt2lfH_vCeyZwnPDB-d25QA5aor1HH0PnkwcUcc6lAbTZ5GCIZYNNroPHNkWMvxP3eRi4KD5mcTC1ydbljiJCDdgxauKH1GAob8KuiJjqXQXUiCf5RvR8uRdMlcQShWQ_yeVpRFJBIahpN3rI

先进行登录, 替换 token 信息, 运行 测试登录用户, 返回下面 JSON 信息

json
{
  "success": true,
  "status": 200,
  "msg": "操作成功!",
  "data": {
    "id": "1",
    "tenantId": null,
    "clientId": null,
    "signMethod": null,
    "nickName": "王大宸",
    "avatar": null,
    "anId": null,
    "anTitle": null,
    "telephone": null,
    "email": null,
    "account": "admin",
    "accountSecret": null,
    "status": null,
    "appId": null,
    "openId": null,
    "userType": "4",
    "sex": null,
    "deptId": null,
    "deptTitle": null,
    "postId": null,
    "postTitle": null,
    "grade": null,
    "isSuperAdmin": null,
    "delFlag": null,
    "canDel": null,
    "expireTime": null,
    "other": null
  }
}