diff --git a/src/main/java/fun/sast/evento/lark/api/common/LoginController.java b/src/main/java/fun/sast/evento/lark/api/common/LoginController.java index 89e7a79..0e787a0 100644 --- a/src/main/java/fun/sast/evento/lark/api/common/LoginController.java +++ b/src/main/java/fun/sast/evento/lark/api/common/LoginController.java @@ -3,10 +3,7 @@ import fun.sast.evento.lark.api.value.V2; import fun.sast.evento.lark.domain.event.service.UserService; import jakarta.annotation.Resource; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v2/login") @@ -19,4 +16,9 @@ class LoginController { V2.Login link(@RequestParam String code, @RequestParam Integer type, @RequestParam String codeVerifier) { return userService.login(code, type, codeVerifier); } + + @PostMapping("/refresh-token") + V2.Login link(@RequestBody V2.RefreshToken refreshToken) { + return userService.refreshToken(refreshToken.refreshToken()); + } } diff --git a/src/main/java/fun/sast/evento/lark/api/common/UserController.java b/src/main/java/fun/sast/evento/lark/api/common/UserController.java new file mode 100644 index 0000000..b06e01e --- /dev/null +++ b/src/main/java/fun/sast/evento/lark/api/common/UserController.java @@ -0,0 +1,24 @@ +package fun.sast.evento.lark.api.common; + +import fun.sast.evento.lark.api.security.Permission; +import fun.sast.evento.lark.api.security.RequirePermission; +import fun.sast.evento.lark.api.value.V2; +import fun.sast.evento.lark.domain.event.service.UserService; +import jakarta.annotation.Resource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v2/user") +public class UserController { + + @Resource + private UserService userService; + + @GetMapping("/profile") + @RequirePermission(Permission.LOGIN) + public V2.User getProfile() { + return userService.getProfile(); + } +} diff --git a/src/main/java/fun/sast/evento/lark/api/value/V2.java b/src/main/java/fun/sast/evento/lark/api/value/V2.java index b9315a8..56c8d34 100644 --- a/src/main/java/fun/sast/evento/lark/api/value/V2.java +++ b/src/main/java/fun/sast/evento/lark/api/value/V2.java @@ -73,8 +73,13 @@ record Slide( } record Login( - String token, - User user + String accessToken, + String refreshToken + ){ + } + + record RefreshToken( + String refreshToken ){ } diff --git a/src/main/java/fun/sast/evento/lark/domain/event/entity/User.java b/src/main/java/fun/sast/evento/lark/domain/event/entity/User.java index d5d2078..db8d25d 100644 --- a/src/main/java/fun/sast/evento/lark/domain/event/entity/User.java +++ b/src/main/java/fun/sast/evento/lark/domain/event/entity/User.java @@ -1,10 +1,9 @@ package fun.sast.evento.lark.domain.event.entity; import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import fun.feellmoose.model.UserInfo; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; @Data @@ -13,6 +12,6 @@ public class User { @TableId(value = "user_id", type = IdType.INPUT) private String userId; private Integer permission; - @TableField(exist = false) - private UserInfo userInfo; + @JsonIgnore // ignore in jwt + private String refreshToken; // sast-link refresh token } diff --git a/src/main/java/fun/sast/evento/lark/domain/event/service/UserService.java b/src/main/java/fun/sast/evento/lark/domain/event/service/UserService.java index 7bf6e2a..a12f42b 100644 --- a/src/main/java/fun/sast/evento/lark/domain/event/service/UserService.java +++ b/src/main/java/fun/sast/evento/lark/domain/event/service/UserService.java @@ -7,7 +7,9 @@ public interface UserService { V2.Login login(String code, Integer type, String codeVerifier); - Boolean assignManagerRole(String userId); + V2.Login refreshToken(String refreshToken); + + V2.User getProfile(); - V2.User mapToV2User(User user); + Boolean assignManagerRole(String userId); } diff --git a/src/main/java/fun/sast/evento/lark/domain/event/service/impl/UserServiceImpl.java b/src/main/java/fun/sast/evento/lark/domain/event/service/impl/UserServiceImpl.java index ac5830e..37e0b10 100644 --- a/src/main/java/fun/sast/evento/lark/domain/event/service/impl/UserServiceImpl.java +++ b/src/main/java/fun/sast/evento/lark/domain/event/service/impl/UserServiceImpl.java @@ -1,18 +1,22 @@ package fun.sast.evento.lark.domain.event.service.impl; +import com.fasterxml.jackson.core.type.TypeReference; import fun.feellmoose.model.UserInfo; import fun.feellmoose.model.response.data.AccessToken; +import fun.feellmoose.model.response.data.RefreshToken; import fun.feellmoose.service.SastLinkService; import fun.sast.evento.lark.api.security.Permission; import fun.sast.evento.lark.api.value.V2; import fun.sast.evento.lark.domain.event.entity.User; import fun.sast.evento.lark.domain.event.service.UserService; +import fun.sast.evento.lark.infrastructure.auth.JWTInterceptor; import fun.sast.evento.lark.infrastructure.auth.JWTService; +import fun.sast.evento.lark.infrastructure.error.BusinessException; +import fun.sast.evento.lark.infrastructure.error.ErrorEnum; import fun.sast.evento.lark.infrastructure.repository.UserMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @@ -20,20 +24,19 @@ public class UserServiceImpl implements UserService { @Resource - private SastLinkService sastLinkServiceWeb; - @Resource - private SastLinkService sastLinkServiceApp; + private SastLinkService sastLinkService; @Resource private JWTService jwtService; @Resource private UserMapper userMapper; - @Resource - private CacheManager cacheManager; + @Value("${app.sast-link.app-redirect-uri}") + private String appRedirectUri; + @Value("${app.sast-link.web-redirect-uri}") + private String webRedirectUri; @Override public V2.Login login(String code, Integer type, String codeVerifier) { - SastLinkService sastLinkService = type == 1 ? sastLinkServiceWeb : sastLinkServiceApp; - AccessToken accessToken = sastLinkService.accessToken(code, codeVerifier); + AccessToken accessToken = sastLinkService.accessToken(code, type == 1 ? webRedirectUri : appRedirectUri, codeVerifier); UserInfo userInfo = sastLinkService.user(accessToken.getAccessToken()); User user = userMapper.selectById(userInfo.getUserId()); if (user == null) { @@ -41,15 +44,46 @@ public V2.Login login(String code, Integer type, String codeVerifier) { user.setUserId(userInfo.getUserId()); user.setPermission(Permission.LOGIN.getNum()); } - user.setUserInfo(userInfo); - String token = jwtService.generate(new JWTService.Payload<>(user)); - Cache cache = cacheManager.getCache("user"); - if (cache == null) { - log.error("user cache not found while logging in"); - throw new RuntimeException("user cache not found"); + user.setRefreshToken(accessToken.getRefreshToken()); + userMapper.insertOrUpdate(user); + String token = jwtService.generate(new JWTService.Payload<>(user), 15); + String refreshToken = jwtService.generate(new JWTService.Payload<>(user.getUserId()), 10080); + return new V2.Login(token, refreshToken); + } + + @Override + public V2.Login refreshToken(String refreshToken) { + String userId = jwtService.verify(refreshToken, new TypeReference<>() { + }); + // User shouldn't be null + User user = userMapper.selectById(userId); + String token = jwtService.generate(new JWTService.Payload<>(user), 30); + return new V2.Login(token, refreshToken); + } + + @Override + public V2.User getProfile() { + try { + User user = userMapper.selectById(JWTInterceptor.userHolder.get().getUserId()); + RefreshToken accessToken = sastLinkService.refreshToken(user.getRefreshToken()); + UserInfo userInfo = sastLinkService.user(accessToken.getAccessToken()); + user.setRefreshToken(accessToken.getRefreshToken()); + userMapper.insertOrUpdate(user); + return new V2.User( + userInfo.getUserId(), + userInfo.getEmail(), + userInfo.getAvatar(), + userInfo.getBadge(), + userInfo.getBio(), + userInfo.getDep(), + userInfo.getHide(), + userInfo.getLink(), + userInfo.getNickname(), + userInfo.getOrg() + ); + } catch (Exception e) { + throw new BusinessException(ErrorEnum.TOKEN_EXPIRED); } - cache.put(user.getUserId(), token); - return new V2.Login(token, mapToV2User(user)); } @Override @@ -58,28 +92,6 @@ public Boolean assignManagerRole(String userId) { user.setUserId(userId); user.setPermission(Permission.MANAGER.getNum()); userMapper.insertOrUpdate(user); - Cache cache = cacheManager.getCache("user"); - if (cache == null) { - log.error("user cache not found while assigning role"); - throw new RuntimeException("user cache not found"); - } - cache.evictIfPresent(userId); return true; } - - @Override - public V2.User mapToV2User(User user) { - return new V2.User( - user.getUserId(), - user.getUserInfo().getEmail(), - user.getUserInfo().getAvatar(), - user.getUserInfo().getBadge(), - user.getUserInfo().getBio(), - user.getUserInfo().getDep(), - user.getUserInfo().getHide(), - user.getUserInfo().getLink(), - user.getUserInfo().getNickname(), - user.getUserInfo().getOrg() - ); - } } diff --git a/src/main/java/fun/sast/evento/lark/infrastructure/auth/JWTInterceptor.java b/src/main/java/fun/sast/evento/lark/infrastructure/auth/JWTInterceptor.java index e5232cb..fa02119 100644 --- a/src/main/java/fun/sast/evento/lark/infrastructure/auth/JWTInterceptor.java +++ b/src/main/java/fun/sast/evento/lark/infrastructure/auth/JWTInterceptor.java @@ -9,8 +9,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; @@ -24,8 +22,6 @@ public class JWTInterceptor implements HandlerInterceptor { public static ThreadLocal userHolder = new ThreadLocal<>(); @Resource private JWTService jwtService; - @Resource - private CacheManager cacheManager; @Override public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { @@ -36,9 +32,6 @@ public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServl String token = header.substring(7); User user = jwtService.verify(token, new TypeReference<>() { }); - if (requireLogin(user.getUserId())) { - throw new BusinessException(ErrorEnum.AUTH_ERROR, "token expired"); - } userHolder.set(user); if (handler instanceof HandlerMethod method) { if (!method.hasMethodAnnotation(RequirePermission.class)) { @@ -55,20 +48,6 @@ public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServl return true; } - private boolean requireLogin(String userId) { - try { - Cache cache = cacheManager.getCache("user"); - if (cache == null) { - log.error("user cache not found"); - return false; - } - return cache.get(userId) == null; - } catch (RuntimeException exception) { - log.error("failed to check user token cache", exception); - return false; - } - } - @Override public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, @Nullable Exception ex) { diff --git a/src/main/java/fun/sast/evento/lark/infrastructure/auth/JWTService.java b/src/main/java/fun/sast/evento/lark/infrastructure/auth/JWTService.java index 36ac30c..1bc7b2b 100644 --- a/src/main/java/fun/sast/evento/lark/infrastructure/auth/JWTService.java +++ b/src/main/java/fun/sast/evento/lark/infrastructure/auth/JWTService.java @@ -3,15 +3,18 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; -import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import fun.sast.evento.lark.infrastructure.error.BusinessException; +import fun.sast.evento.lark.infrastructure.error.ErrorEnum; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Base64; @Component @@ -19,15 +22,12 @@ public class JWTService { private final Algorithm algorithm; private final JWTVerifier verifier; - private final Long expire; private final ObjectMapper objectMapper; public JWTService(@Value("${app.auth.jwt.secret}") String secret, - @Value("${app.auth.jwt.expire}") Long expire, ObjectMapper objectMapper) { this.algorithm = Algorithm.HMAC256(secret); this.verifier = JWT.require(algorithm).build(); - this.expire = expire; this.objectMapper = objectMapper; } @@ -35,15 +35,20 @@ public record Payload(T value) { } @SneakyThrows - public String generate(Payload payload) { + public String generate(Payload payload, long expire) { return JWT.create().withPayload(objectMapper.writeValueAsString(payload)) - .withExpiresAt(Instant.now().plusSeconds(expire)) + .withExpiresAt(Instant.now().plus(expire, ChronoUnit.MINUTES)) .sign(algorithm); } @SneakyThrows - public T verify(String token, TypeReference> typeReference) throws JWTVerificationException { - DecodedJWT decodedJWT = verifier.verify(token); + public T verify(String token, TypeReference> typeReference) { + DecodedJWT decodedJWT; + try { + decodedJWT = verifier.verify(token); + } catch (TokenExpiredException e) { + throw new BusinessException(ErrorEnum.TOKEN_EXPIRED); + } String payload = new String(Base64.getDecoder().decode(decodedJWT.getPayload())); return objectMapper.readValue(payload, typeReference).value; } diff --git a/src/main/java/fun/sast/evento/lark/infrastructure/auth/SastLinkConfig.java b/src/main/java/fun/sast/evento/lark/infrastructure/auth/SastLinkConfig.java index 40ca408..0bfa4bd 100644 --- a/src/main/java/fun/sast/evento/lark/infrastructure/auth/SastLinkConfig.java +++ b/src/main/java/fun/sast/evento/lark/infrastructure/auth/SastLinkConfig.java @@ -10,27 +10,12 @@ class SastLinkConfig { @Bean - SastLinkService sastLinkServiceWeb(@Value("${app.sast-link.link-path}") String path, - @Value("${app.sast-link.web-redirect-uri}") String uri, - @Value("${app.sast-link.web-client-id}") String id, - @Value("${app.sast-link.web-client-secret}") String secret) { + SastLinkService sastLinkService(@Value("${app.sast-link.link-path}") String path, + @Value("${app.sast-link.client-id}") String id, + @Value("${app.sast-link.client-secret}") String secret) { return new HttpClientSastLinkService.Builder() .setClientId(id) .setClientSecret(secret) - .setRedirectUri(uri) - .setHostName(path) - .build(); - } - - @Bean - SastLinkService sastLinkServiceApp(@Value("${app.sast-link.link-path}") String path, - @Value("${app.sast-link.app-redirect-uri}") String uri, - @Value("${app.sast-link.app-client-id}") String id, - @Value("${app.sast-link.app-client-secret}") String secret) { - return new HttpClientSastLinkService.Builder() - .setClientId(id) - .setClientSecret(secret) - .setRedirectUri(uri) .setHostName(path) .build(); } diff --git a/src/main/java/fun/sast/evento/lark/infrastructure/error/ErrorEnum.java b/src/main/java/fun/sast/evento/lark/infrastructure/error/ErrorEnum.java index 322668f..f9d3120 100644 --- a/src/main/java/fun/sast/evento/lark/infrastructure/error/ErrorEnum.java +++ b/src/main/java/fun/sast/evento/lark/infrastructure/error/ErrorEnum.java @@ -7,9 +7,10 @@ @Getter public enum ErrorEnum { AUTH_ERROR(1000, "Authentication error"), - PARAM_ERROR(1001, "Parameter error"), - PERMISSION_DENIED(1002, "Permission denied"), - CHECKIN_CODE_NOT_EXISTS(1003, "Checkin code not exists or has expired"), + TOKEN_EXPIRED(1001, "Token expired"), + PARAM_ERROR(1002, "Parameter error"), + PERMISSION_DENIED(1003, "Permission denied"), + CHECKIN_CODE_NOT_EXISTS(1004, "Checkin code not exists or has expired"), LARK_ERROR_CREATE_CALENDAR_EVENT(2001, "Failed to create event"), LARK_ERROR_SET_ROOM(2002, "Failed to set room"), LARK_ERROR_GET_EVENT(2003, "Failed to get event"), diff --git a/src/main/resources/sql/SQL.sql b/src/main/resources/sql/SQL.sql index b6c5349..21a3664 100644 --- a/src/main/resources/sql/SQL.sql +++ b/src/main/resources/sql/SQL.sql @@ -63,7 +63,8 @@ CREATE TABLE `message` ); CREATE TABLE `user` ( - `user_id` VARCHAR(16) NOT NULL, - `permission` INT, + `user_id` VARCHAR(16) NOT NULL, + `permission` INT NOT NULL, + `refresh_token` VARCHAR(255) NOT NULL, PRIMARY KEY (`user_id`) );