From 77769558ac44da52ea17b9de7b8d6d92097ea735 Mon Sep 17 00:00:00 2001 From: xiang Date: Thu, 25 Sep 2025 23:30:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:token=20=E9=AA=8C=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 3 +- .../src/main/resources/keys/rsa-private.pem | 28 ++++++++ .../src/main/resources/keys/rsa-public.pem | 9 +++ .../config/AuthorizationServerConfig.java | 24 ------- .../auth/service/config/JwtConfig.java | 71 +++++++++++++++++++ .../auth/service/constants/RedisConstant.java | 1 + .../xservice/auth/service/entity/XUser.java | 9 +++ .../service/impl/XUserServiceImpl.java | 38 ++++++++++ .../resources/mapper/user/XUserMapper.xml | 26 +++++-- 9 files changed, 180 insertions(+), 29 deletions(-) create mode 100644 xs-server/src/main/resources/keys/rsa-private.pem create mode 100644 xs-server/src/main/resources/keys/rsa-public.pem create mode 100644 xs-service/src/main/java/com/xiang/xservice/auth/service/config/JwtConfig.java diff --git a/pom.xml b/pom.xml index 4047a8b..77b81b0 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> com.xiang - xservice-basic + xservice-parent-starter 2.0 pom @@ -86,6 +86,7 @@ com.github.pagehelper pagehelper-spring-boot-starter + 1.4.7 diff --git a/xs-server/src/main/resources/keys/rsa-private.pem b/xs-server/src/main/resources/keys/rsa-private.pem new file mode 100644 index 0000000..d132742 --- /dev/null +++ b/xs-server/src/main/resources/keys/rsa-private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD7JdqQe8W8byYT +KuPK7STLYO5TTe5uWhHj1SLq7Xoj1y0agO+x1X3/dUA1X/k5Dd2rmkmIU2VoB/9c +aoE5naXUhVfE9wDsl5AKdWRCn1PKvuofd/0kuGFS44tNQcLGtDD7e8CQQJZJPZVS +Z8F2nlHjYSTUPVvi77hKG21qoZLEa0F+TYfqgOohabQtxeBAnyRW50/g281Bqd3w +ntfM0aKfW9fsFhJnk+AlXWaqxQKkhtzMmKVeqLNfJnXvhgUA6QbnzHPtPxF+fLEx +BNnBEPF8CsQFVdt1TtsQlfCzVMTMibZU2CcwtqHUP1maEcH5zV5whPfgkahejiNm +L08Jw74VAgMBAAECggEACJHb0pGHh4IkF/XmM6Qs6xMFYmqmvRkf0JjoTYDl9+JW +Y9RMYKfqy+Yius+GSNRjPWS7p38MHkiysGL/F7uYyCwvhwU3x+kugM2+/+gWqyaT +WnwYgZ4YXIRu2ieFr4xq1symnzO14nDny4uqB9PEZFd7wS4I0ZShe5yLM6Yai88V +0Jm6Hi9RcC5efiE4tWismBKaP13WXamAVySROW30lEaMgyI66DNYgs+RHiZgAFP7 +u7raUD07xrk6eV6YnG/9EvS/oqV+IPEacY+bP3ZUqvPMI50tLTEVN1yJXrr4T3kS +W1TiApaL87rdDCYem7rtIury+JcSadaI6lwP5OhmLwKBgQD/4mSrFi30IzD6LFam +N7CYBxSleWgha2fHuycDGNGcXDCn3y4JuqTMRpV1c9F5emPPv1E3ELj4plz/NnVr +67SkCs3nqoSRjyXJKhN/kJqM/NB+Ic1UgXeI+wGMkfkUHrQ6T6SghYVxWW+hQKm8 +IeV7aVJiM01/Ze938cnJuWd17wKBgQD7QumZzRTMkmnmSFCpOhs2Y3B8JYrJsJXY +PeYkxea/7brDyuIdWKt0kl9EvpsrIzTe0t4LYV3Vmmfh15PNZp4PEr30NEBxeVOO +HoglNfyJgP/nvhOGYesNhqPlK96/ajEvu7FpFHwDje5RKRWxCK5qhZneDy0ppjWb +6seshN1wOwKBgQDqNPxxP/bFu6Qrh4Oz1cs0C18RakMuO5Gc1acKhZ/tntAGBxer +XgNS2dQY0e5MYwKSdwlN/mdfZ149Vko5gl8vupfmUEPQuxYZvwJjwyZCn2/x0tyO +WYXggeZUFJPHn6bUrGsBZdTS/8pV7Mqu4NOblrYKHez0C4gY390TXzjcTwKBgQDR +mD6fYrjf8Z7PTzGiCOucUhUKKpL8rgZBbVknAcL8BYZPP1Whn07fHh7EjK+Jq4O2 +AHbjTWRmA7h2Z0tPAzQEZOD57gB35/pwSj3NtJwl4+sU2LUW22WlUdQ0HoVgbWf8 +ZniWrFTK7kGHiFsk45YDG9F/sG8/F/wORSotWmQR8wKBgQDqrlyvMCwiJHW7vOPs +ih+utzIvtZ8D4fxzEFluTUqubAAl3N+81NuRJuEFIJLjIAeTNOHj1IdlPj5oe0aa +IYOzoB2+xJxnLPbvI1tTat/pqgXxPY24/9c9rBoTCsJboTPb0fMh1nHxTZvny4tB +jP7d5EBvIMWnCEuTo4y39ZFsMA== +-----END PRIVATE KEY----- diff --git a/xs-server/src/main/resources/keys/rsa-public.pem b/xs-server/src/main/resources/keys/rsa-public.pem new file mode 100644 index 0000000..bbf9565 --- /dev/null +++ b/xs-server/src/main/resources/keys/rsa-public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+yXakHvFvG8mEyrjyu0k +y2DuU03ubloR49Ui6u16I9ctGoDvsdV9/3VANV/5OQ3dq5pJiFNlaAf/XGqBOZ2l +1IVXxPcA7JeQCnVkQp9Tyr7qH3f9JLhhUuOLTUHCxrQw+3vAkECWST2VUmfBdp5R +42Ek1D1b4u+4ShttaqGSxGtBfk2H6oDqIWm0LcXgQJ8kVudP4NvNQand8J7XzNGi +n1vX7BYSZ5PgJV1mqsUCpIbczJilXqizXyZ174YFAOkG58xz7T8RfnyxMQTZwRDx +fArEBVXbdU7bEJXws1TEzIm2VNgnMLah1D9ZmhHB+c1ecIT34JGoXo4jZi9PCcO+ +FQIDAQAB +-----END PUBLIC KEY----- diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationServerConfig.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationServerConfig.java index 03ceed9..2e7ca1e 100644 --- a/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationServerConfig.java +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/AuthorizationServerConfig.java @@ -1,10 +1,5 @@ package com.xiang.xservice.auth.service.config; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; -import com.xiang.xservice.basic.utils.JwkUtils; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -22,9 +17,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtEncoder; -import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; @@ -138,18 +130,6 @@ public class AuthorizationServerConfig { return repository; } - @Bean - public JWKSource jwkSource() { - RSAKey rsaKey = JwkUtils.generateRsa(); - JWKSet jwkSet = new JWKSet(rsaKey); - return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); - } - - @Bean - public JwtEncoder jwtEncoder(JWKSource jwkSource) { - return new NimbusJwtEncoder(jwkSource); - } - @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder() @@ -157,9 +137,5 @@ public class AuthorizationServerConfig { .build(); } - @Bean - public JwtDecoder jwtDecoder(JWKSource jwkSource) { - return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); - } } diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/config/JwtConfig.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/JwtConfig.java new file mode 100644 index 0000000..c9776ae --- /dev/null +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/config/JwtConfig.java @@ -0,0 +1,71 @@ +package com.xiang.xservice.auth.service.config; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +@Configuration +public class JwtConfig { + + private RSAPrivateKey loadPrivateKey(String classpath) throws Exception { + InputStream is = new ClassPathResource(classpath).getInputStream(); + String key = new String(is.readAllBytes(), StandardCharsets.UTF_8) + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key)); + return (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(spec); + } + + private RSAPublicKey loadPublicKey(String classpath) throws Exception { + InputStream is = new ClassPathResource(classpath).getInputStream(); + String key = new String(is.readAllBytes(), StandardCharsets.UTF_8) + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s+", ""); + X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(key)); + return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(spec); + } + + @Bean + public JWKSource jwkSource() throws Exception { + // 使用RSA对称加密进行加密 + RSAPublicKey publicKey = loadPublicKey("keys/rsa-public.pem"); + RSAPrivateKey privateKey = loadPrivateKey("keys/rsa-private.pem"); + + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID("xservice") + .build(); + + JWKSet jwkSet = new JWKSet(rsaKey); + return (jwkSelector, ctx) -> jwkSelector.select(jwkSet); + } + + @Bean + public JwtEncoder jwtEncoder(JWKSource jwkSource) { + return new NimbusJwtEncoder(jwkSource); + } + + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } +} diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/constants/RedisConstant.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/constants/RedisConstant.java index c7ccfd0..3cfd2ab 100644 --- a/xs-service/src/main/java/com/xiang/xservice/auth/service/constants/RedisConstant.java +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/constants/RedisConstant.java @@ -2,6 +2,7 @@ package com.xiang.xservice.auth.service.constants; public class RedisConstant { + public static final String LOGIN_TOKEN = "login:token:"; public static final String XS_PERMISSION_ROLE = "auth:permission:role"; public static final String XS_SMS_CODE_KEY = "auth:sms:code:key:"; public static final String XS_CAPTCHA_CODE_KEY = "auth:captcha:code:key:"; diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XUser.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XUser.java index 9b70be0..6e7d4f8 100644 --- a/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XUser.java +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/entity/XUser.java @@ -95,4 +95,13 @@ public class XUser implements Serializable { * 修改时间 */ private LocalDateTime updateTime; + + /** + * token + */ + private String token; + /** + * 刷新token + */ + private String refreshToken; } diff --git a/xs-service/src/main/java/com/xiang/xservice/auth/service/service/impl/XUserServiceImpl.java b/xs-service/src/main/java/com/xiang/xservice/auth/service/service/impl/XUserServiceImpl.java index 990f7e7..8424ccd 100644 --- a/xs-service/src/main/java/com/xiang/xservice/auth/service/service/impl/XUserServiceImpl.java +++ b/xs-service/src/main/java/com/xiang/xservice/auth/service/service/impl/XUserServiceImpl.java @@ -17,6 +17,7 @@ import com.xiang.xservice.auth.api.dto.resp.LoginResp; import com.xiang.xservice.auth.api.dto.resp.RegisterResp; import com.xiang.xservice.auth.api.dto.resp.UserDTO; import com.xiang.xservice.auth.api.dto.resp.UserResp; +import com.xiang.xservice.auth.service.constants.RedisConstant; import com.xiang.xservice.auth.service.convert.XDeptConvert; import com.xiang.xservice.auth.service.convert.XPermissionConvert; import com.xiang.xservice.auth.service.convert.XRoleConvert; @@ -40,15 +41,19 @@ import com.xiang.xservice.auth.service.service.XUserService; import com.xiang.xservice.basic.enums.DelStatusEnum; import com.xiang.xservice.basic.exception.BusinessException; import com.xiang.xservice.basic.utils.PrimaryKeyUtils; +import com.xiang.xservice.cache.service.IRedisService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.stereotype.Service; @@ -59,6 +64,7 @@ import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Objects; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j @@ -80,6 +86,8 @@ public class XUserServiceImpl implements XUserService { private final XRolePermissionMapper rolePermissionMapper; private final XPermissionMapper permissionMapper; private final XPermissionConvert permissionConvert; + private final IRedisService redisService; + private final JwtDecoder jwtDecoder; @Override public LoginResp login(LoginRequest request) { @@ -106,6 +114,21 @@ public class XUserServiceImpl implements XUserService { } roleCodes.addAll(roles.stream().map(XRole::getCode).toList()); } + if (StringUtils.isNotBlank(user.getToken())) { + try { + Jwt jwt = jwtDecoder.decode(user.getToken()); + if (Objects.nonNull(jwt.getExpiresAt())) { + if (jwt.getExpiresAt().isAfter(Instant.now())) { + LoginResp loginResp = new LoginResp(); + loginResp.setToken(user.getToken()); + loginResp.setUsername(request.getUsername()); + return loginResp; + } + } + } catch (Exception e) { + log.info("jwt解析token失败", e); + } + } // 生成 token Instant now = Instant.now(); @@ -121,11 +144,26 @@ public class XUserServiceImpl implements XUserService { .claim("authorities", roleCodes) .build(); + JwtClaimsSet refreshClaims = JwtClaimsSet.builder() + // 对应 ProviderSettings.issuer + .issuedAt(now) + .expiresAt(now.plus(24, ChronoUnit.HOURS)) + // 自定义 scope + .claim("tenantId", user.getTenantId()) + .build(); + // 2. 编码生成 token String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); + String refreshToken = jwtEncoder.encode(JwtEncoderParameters.from(refreshClaims)).getTokenValue(); LoginResp loginResp = new LoginResp(); loginResp.setToken(token); loginResp.setUsername(request.getUsername()); + // 3. redis缓存token + redisService.set(RedisConstant.LOGIN_TOKEN + request.getUsername(), token, 3, TimeUnit.HOURS); + // 4. db 存储token + user.setToken(token); + user.setRefreshToken(refreshToken); + userMapper.update(user); return loginResp; } diff --git a/xs-service/src/main/resources/mapper/user/XUserMapper.xml b/xs-service/src/main/resources/mapper/user/XUserMapper.xml index 9f7feb2..a64f815 100644 --- a/xs-service/src/main/resources/mapper/user/XUserMapper.xml +++ b/xs-service/src/main/resources/mapper/user/XUserMapper.xml @@ -20,6 +20,8 @@ + + @@ -38,7 +40,9 @@ create_time, update_by, update_time, - tenant_id + tenant_id, + token, + refresh_token @@ -87,7 +91,13 @@ update_time, - tenant_id + tenant_id, + + + token, + + + refreshToken @@ -134,7 +144,13 @@ #{updateTime}, - #{tenantId} + #{tenantId}, + + + #{token}, + + + #{refreshToken} @@ -165,7 +181,9 @@ create_time = #{createTime}, update_by = #{updateBy}, update_time = #{updateTime}, - tenant_id = #{tenantId} + tenant_id = #{tenantId}, + token = #{token}, + refresh_token = #{refreshToken} WHERE id = #{id}