微服务认证中心
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 中 - 将初始化的私钥/公钥存入
AuthTokenSecretKeyAuthClientTokenSecretKey两个配置文件中(存放在内存中) - 初始化 私钥/公钥 依赖
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, 默认使用 RSAfly.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
}
}
剑鸣秋朔