Skip to content
章节导航

微服务认证中心

fly-auth-server

TIP

以 fly 开头的依赖来自于 fly-framework: https://gitee.com/itdachen/fly-framework

项目依赖

pom.xml

xml
<dependencies>
    <dependency>
        <groupId>com.github.itdachen.framework</groupId>
        <artifactId>fly-runner</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>

    <!-- token 加解密 -->
    <dependency>
        <groupId>com.github.itdachen.framework.cloud</groupId>
        <artifactId>fly-cloud-jwt-crypto</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.itdachen.framework.cloud</groupId>
        <artifactId>fly-cloud-jwt-parse</artifactId>
    </dependency>

    <dependency>
        <groupId>com.github.itdachen.framework</groupId>
        <artifactId>fly-core</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.itdachen.framework</groupId>
        <artifactId>fly-webmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.itdachen.framework</groupId>
        <artifactId>fly-tools</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.itdachen.framework</groupId>
        <artifactId>fly-body-advice</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.itdachen.framework</groupId>
        <artifactId>fly-rbac</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</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>

    <!-- mysql -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <!-- 数据库 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>tk.mybatis</groupId>
        <artifactId>mapper-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>

    <!-- 配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>


</dependencies>

添加启动类

新建项目启动类 AuthBootstrap

@RefreshScope // 动态刷新配置
@EnableScheduling // 定时任务
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(basePackages = {"com.github.itdachen"})
@MapperScan(basePackages = "com.github.itdachen.auth.**.mapper")
public class AuthBootstrap {

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

}

初始化 Token 私钥/公钥

配置 Redis 序列化

@Configuration
@EnableAutoConfiguration
public class RedisSerializerConfiguration {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

}

新建 AuthorizedServerSecretKeyRunner 类, 项目启动时, 初始化 Token 私钥/公钥

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

    @Autowired
    private AuthTokenSecretKey authTokenSecretKey;
    @Autowired
    private AuthClientTokenSecretKey authClientTokenSecretKey;

    private final RedisTemplate<String, String> redisTemplate;
    private final SecretKeyHelper secretKeyHelper;

    public AuthorizedServerSecretKeyRunner(RedisTemplate<String, String> redisTemplate,
                                           SecretKeyHelper secretKeyHelper) {
        this.redisTemplate = redisTemplate;
        this.secretKeyHelper = secretKeyHelper;
    }

    @Override
    public void run(String... args) throws Exception {
        logger.info("初始化加载用户 secret key ...");
        String publicKey = "";
        if (Boolean.TRUE.equals(redisTemplate.hasKey(JwtRedisKeyConstants.USER_PRI_KEY))
                && Boolean.TRUE.equals(redisTemplate.hasKey(JwtRedisKeyConstants.USER_PUB_KEY))) {

            String privateKey = redisTemplate.opsForValue().get(JwtRedisKeyConstants.USER_PRI_KEY);
            publicKey = redisTemplate.opsForValue().get(JwtRedisKeyConstants.USER_PUB_KEY);

            authTokenSecretKey.setUserPriKey(privateKey);
            authTokenSecretKey.setUserPubKey(publicKey);
        } else {
            JwtSecretKey jwtSecretKey = secretKeyHelper.secretKey();
            publicKey = jwtSecretKey.getPublicKey();

            redisTemplate.opsForValue().set(JwtRedisKeyConstants.USER_PRI_KEY, jwtSecretKey.getPrivateKey());
            redisTemplate.opsForValue().set(JwtRedisKeyConstants.USER_PUB_KEY, jwtSecretKey.getPublicKey());

            authTokenSecretKey.setUserPriKey(jwtSecretKey.getPrivateKey());
            authTokenSecretKey.setUserPubKey(jwtSecretKey.getPublicKey());
        }

        /* 客户 token 解析秘钥 */
        authClientTokenSecretKey.setTokenPublicKey(publicKey);
        logger.info("用户 secret key 初始化完成 ...");
    }

}
  • 初始化时, 先查询 Redis 中是否存在私钥/公钥, 如果不存在, 通过 SecretKeyHelper 类生成公钥和私钥, 再存放在 Redis 中
  • 将初始化的私钥/公钥存入 AuthTokenSecretKey AuthClientTokenSecretKey 两个配置文件中(存放在内存中)
  • 初始化 私钥/公钥 依赖
xml
<dependency>
    <groupId>com.github.itdachen.framework.cloud</groupId>
    <artifactId>fly-cloud-jwt-crypto</artifactId>
</dependency>

添加配置文件

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


# 认证中心配置
fly:
  cloud:
    auth:
      token:
        type: rsa
      app:
        service-id: auth
        app-id: ${spring.application.name}
        app-secret: 123456
        matchers:
          - /oauth/jwt/**

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

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

  cloud:
    ## 向 nacos 发起注册
    nacos:
      discovery:
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        username: nacos
        password: nacos
        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

  data:
    redis:
      database: 5
      host: 127.0.0.1

认证中心配置说明:

  • fly.cloud.auth.token.type: 指定 token 生成方式, fly-cloud-jwt-crypto 中提供了三种 token 生成方式, 分别是: RSA, HMAC, ECDSA, 默认使用 RSA
  • fly.cloud.auth.app.matchers: 请求不拦截的路径

启动项目

启动项目, 控制台将打印 初始化加载用户 secret key ..., 如下图

再 redis 中, 查看初始化 Token 私钥/公钥

用户登录

Controller

AuthorizeOAuthToken 用户接收登录时传入的参数

public class AuthorizeOAuthToken {

    /**
     * 登录账号
     */
    private String username;

    /**
     * 登录密码
     */
    private String password;

    /**
     * 验证码
     */
    private String code;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

登录方法映射

@RestController
@RequestMapping("/oauth/jwt")
public class AuthorizeOAuthTokenController {
    private static final Logger logger = LoggerFactory.getLogger(AuthorizeOAuthTokenController.class);
    private final IAuthorizeOAuthTokenService oAuthTokenService;

    public AuthorizeOAuthTokenController(IAuthorizeOAuthTokenService oAuthTokenService) {
        this.oAuthTokenService = oAuthTokenService;
    }
    
    /***
     * 账号密码登录
     *
     * @author 王大宸
     * @date 2023/5/5 14:52
     * @param authToken authToken
     * @return com.github.itdachen.framework.core.response.ServerResponse<com.github.itdachen.auth.jwts.core.AccessTokenInfo>
     */
    @PostMapping(value = "/token")
    public ServerResponse<AccessTokenInfo> oauthJwtToken(@RequestBody AuthorizeOAuthToken authToken) throws Exception {
        return ServerResponse.okMsgData("登录成功!", oAuthTokenService.oauthJwtToken(authToken));
    }

}

service

IAuthorizeOAuthTokenService登录处理接口

public interface IAuthorizeOAuthTokenService {

    /***
     * 账号密码登录
     *
     * @author 王大宸
     * @date 2023/5/5 15:05
     * @param authToken authToken
     * @return com.github.itdachen.auth.jwts.core.AccessTokenInfo
     */
    AccessTokenInfo oauthJwtToken(AuthorizeOAuthToken authToken) throws Exception;


}

登录方法实现

@Service
public class AuthorizeOAuthTokenServiceImpl implements IAuthorizeOAuthTokenService {
    private static final Logger logger = LoggerFactory.getLogger(AuthorizeOAuthTokenServiceImpl.class);
    private final JwtsTokenHelper jwtsTokenHelper;

    public AuthorizeOAuthTokenServiceImpl(JwtsTokenHelper jwtsTokenHelper) {
        this.jwtsTokenHelper = jwtsTokenHelper;
    }

    /***
     * 账号密码登录
     *
     * @author 王大宸
     * @date 2023/5/5 14:54
     * @param authToken authToken
     * @return com.github.itdachen.auth.jwts.core.AccessTokenInfo
     */
    @Override
    public AccessTokenInfo oauthJwtToken(AuthorizeOAuthToken authToken) throws Exception {
        if (null == authToken
                || StringUtils.isEmpty(authToken.getUsername())
                || StringUtils.isEmpty(authToken.getPassword())) {
            throw new BizException("登录认证信息不全!!!");
        }

        /* 登录用户信息写死, 后面从数据库查询 */
        CurrentUserDetails currentUserDetails = new CurrentUserDetails();
        currentUserDetails.setAccount("admin");
        currentUserDetails.setNickName("王大宸");
        currentUserDetails.setId("1");
        currentUserDetails.setUserType("1");

        /* 存放在 token 中的信息 */
        Map<String, String> otherInfo = new HashMap<>(16);
        otherInfo.put(UserInfoConstant.AVATAR, currentUserDetails.getAvatar());
        otherInfo.put(UserInfoConstant.TELEPHONE, currentUserDetails.getTelephone());
        otherInfo.put(UserInfoConstant.USER_TYPE, UserTypeConstant.MEMBER);
        otherInfo.put(UserInfoConstant.TENANT_ID, currentUserDetails.getTenantId());
        otherInfo.put(UserInfoConstant.GRADE, currentUserDetails.getGrade());
        
        /* 生成 token */
        String access_token = jwtsTokenHelper.createToken(new JwtTokenInfo.Builder()
                .username(currentUserDetails.getAccount())
                .nickName(currentUserDetails.getNickName())
                .userId(currentUserDetails.getId())
                .otherInfo(otherInfo)
                .build());


        Map<String, Object> infoMap = new HashMap<>(8);
        infoMap.put(UserInfoConstant.USER_TYPE, UserTypeConstant.MEMBER);
        infoMap.put(UserInfoConstant.TENANT_ID, currentUserDetails.getTenantId());
        infoMap.put(UserInfoConstant.AVATAR, currentUserDetails.getAvatar());
        infoMap.put(UserInfoConstant.NICK_NAME, currentUserDetails.getNickName());
        infoMap.put(UserInfoConstant.TELEPHONE, currentUserDetails.getTelephone());
        infoMap.put(UserInfoConstant.GRADE, currentUserDetails.getGrade());

        return new AccessTokenInfo.Builder()
                .access_token(access_token)
             //   .expires_in(Integer.parseInt(String.valueOf(jwtProperties.getExpires())))
                .info(infoMap)
                .build();
    }

}
  • JwtsTokenHelper 类来自于 fly-cloud-jwt-crypto 依赖

拦截器, 解析用户信息

拦截器配置: AuthBootstrapWebMvcConfig

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

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

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

    /***
     * 拦截器配置
     *
     * @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();
    }

    @Bean
    public UserAuthRestInterceptor authInterceptor() {
        return new UserAuthRestInterceptor(verifyTicketTokenService);
    }

}
  • UserAuthRestInterceptor: 拦截器, 用于解析 token 信息, 来自于 fly-cloud-jwt-parse 依赖
  • UserAuthRestMethodArgumentResolver: 获取当前登录账号 CurrentUserDetails 信息, 只能在 Controller 使用, 使用时, 需要添加 @CurrentUser CurrentUserDetails userDetails
@RestController
@RequestMapping("/current/user")
public class CurrentUserController {

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

}

测试

编写测试类

编写 CurrentUserController 测试类, 获取当前登录用户信息

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

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

}

创建请求方法

创建 login.http 文件, 内容如下:

### 测试登录
POST http://localhost:8001/auth/oauth/jwt/token
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}


### 测试登录用户(auth)
GET http://localhost:8001/auth/current/user/details
Accept: application/json
Verified-Ticket: verifiedTicket
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzUxMiJ9.eyJuaWNrTmFtZSI6IueOi-Wkp-WuuCIsInVzZXJUeXBlIjoiNCIsInVzZXJJZCI6IjEiLCJhY2NvdW50IjoiYWRtaW4iLCJpc3MiOiJjb20uZ2l0aHViLml0ZGFjaGVuIiwiaWF0IjoxNjg5NDM3MTc1LCJleHAiOjE2ODk0NTUxNzV9.vaYjFMVVK95J_tm_JFhJT-sQ2F2gComiuJQzM90_6M5M0xGFvkFIxvOfXhA_isy4DGg-U8N7wh60yXPYNplk6Q3K_mrEO1ukG-xybI9Oax1zhRxZARiVkHw2wmCypkW9x2SrpvkZhBWjqwtDRHehHqCjYkN8WjBOc-K8tLcEdWkrHNnDnchAH_H3wJaj3qcS1n7Lpc_GUIHXrWuw8gmxG-azb55Olx2_mcMwgGmTAkz1coOooa4rl5DLlz8mwlOtlXHPY5I10RttVS-ZT-39kFt9BE6Uqh-tE7cyTkF5zcVKKGVMMZ8s-NvkslkgjOnHrZKMxl7cMgL-wgPukjQHMhRcvC7WYb7x35RS_grPnJomFU8HhszhQf_8z_fDWT3LJpK__pPbb96YKvltRSXejpjGtzRMwEgOpNzZAFoF8QlaLULK6EJk7L9MCnfCTPiu48g6P42EaAVxzQbG1YBZXpJ72YNEjBWY7BcHlJ_WxdBqhxfE2m6iJumUfeYx2wXEkdnDqr0MrZXwDRkEuJmuapzq5KHtroYjhF_cGfvj43pILfkJaL-dHyHqN7N_1jKt8C-DoBgCallWkRxmb3CnmIwLXkQTTh63eSpGvOUaO4N9hbzopVT_3jFgfllAMepDTUtJ1_qBg6szjmXG4hSHbhRj0T7FGhDZAM3szP1GUaw

登录

运行登录方法, 返回下面 JSON 数据

json
{
  "success": true,
  "status": 200,
  "msg": "登录成功!",
  "data": {
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJQUzUxMiJ9.eyJuaWNrTmFtZSI6IueOi-Wkp-WuuCIsInVzZXJUeXBlIjoiNCIsInVzZXJJZCI6IjEiLCJhY2NvdW50IjoiYWRtaW4iLCJpc3MiOiJjb20uZ2l0aHViLml0ZGFjaGVuIiwiaWF0IjoxNjkzMzg3NjUxLCJleHAiOjE2OTM0MDU2NTF9.Nh4UKe4TBRO2SZlfCE7O9vOSjbcAhz46RNlJhm0NDsCVe8dDdYc2o29QecUObC7uIq9a7mCamF1_wA7tbEqThRY95ENf16QnQcHbu3sjrJJeoDCNVJ5qtpiXJyq2dz-qR2l6eenXYikKlGHvNZWHUuQx8yWnZycS8Rw3xqjJMa7-bjA2P_1_ixXx_13nnQQb59rd6o_06jCZE89h5qt_b-kBuIv6XiMQsH-jz-l259nhf1J7an3mcklaT1er4IQI2qiwwf24t_wsujWP-zqLfe5j1FnFxdx9l1KCIpCC_mZFRVPbTOW12Y7oyLon62VvtHysAX8hvELR_eAD6oPkILbR_bUmwRVf_JSd1vs7WaI9-RRM_FDLgBXx_3MjEvqIauSJW1exlvNJeZM37l620GQIjZChECqlZ56l8Mp2lZYa5Yp7UwV5A0pnFZSqtbaMUNEMW2HQhwxM1ffSWPEBf8gcANBBp4Cu1FHbHImnFT-evMGFSnAH7t05-1sPomDJ_7wiy7hbnXNXBBjLqbbR5d6iGmHwlCXglQm7ycxRCqVSOWDvVLHEgUePK4ans7T2-XlM2oRw1NQCoFl_4rzM_0BmmMkcvwOZj7vf4BfkBllob2DiGJEHSgH3n_7DlZVTwjLUyKhmpA_R2MFZqCsMew0t2-AycN-N0A9P41qF6ig",
    "token_type": null,
    "refresh_token": null,
    "expires_in": null,
    "scope": null,
    "path": null,
    "info": {
      "tenantId": null,
      "telephone": null,
      "userType": "4",
      "avatar": null,
      "nickName": "王大宸",
      "grade": null
    }
  }
}

获取用户信息

替换 http 文件中的 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
  }
}