Skip to content

Commit

Permalink
feat: better login and user system
Browse files Browse the repository at this point in the history
  • Loading branch information
ballade0d committed Oct 20, 2024
1 parent 9d64368 commit 1ffc46c
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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());
}
}
24 changes: 24 additions & 0 deletions src/main/java/fun/sast/evento/lark/api/common/UserController.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
9 changes: 7 additions & 2 deletions src/main/java/fun/sast/evento/lark/api/value/V2.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,13 @@ record Slide(
}

record Login(
String token,
User user
String accessToken,
String refreshToken
){
}

record RefreshToken(
String refreshToken
){
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -1,55 +1,89 @@
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
@Slf4j
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) {
user = new User();
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
Expand All @@ -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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,8 +22,6 @@ public class JWTInterceptor implements HandlerInterceptor {
public static ThreadLocal<User> userHolder = new ThreadLocal<>();
@Resource
private JWTService jwtService;
@Resource
private CacheManager cacheManager;

@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
Expand All @@ -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)) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,52 @@
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
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;
}

public record Payload<T>(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> T verify(String token, TypeReference<Payload<T>> typeReference) throws JWTVerificationException {
DecodedJWT decodedJWT = verifier.verify(token);
public <T> T verify(String token, TypeReference<Payload<T>> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
5 changes: 3 additions & 2 deletions src/main/resources/sql/SQL.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
);

0 comments on commit 1ffc46c

Please sign in to comment.