feat: support github oauth

This commit is contained in:
vran
2022-02-28 23:12:44 +08:00
parent ad8cbab226
commit 18edc58d2f
20 changed files with 509 additions and 6 deletions

View File

@@ -31,6 +31,9 @@ dependencies {
implementation 'org.commonmark:commonmark:0.18.1'
implementation 'org.freemarker:freemarker:2.3.31'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-jackson:2.9.0'
// test
testImplementation "mysql:mysql-connector-java:${mysqlConnectorVersion}"
}

View File

@@ -65,14 +65,14 @@ public class UserService {
}
@Transactional
public void create(UserCreateRequest userCreateRequest) {
public Integer create(UserCreateRequest userCreateRequest) {
userDao.selectByEmailOrUsername(userCreateRequest.getUsername()).ifPresent(data -> {
throw DomainErrors.USERNAME_OR_EMAIL_DUPLICATE.exception();
});
String hashedPassword = bCryptPasswordEncoder.encode(userCreateRequest.getPassword());
UserPojo pojo = userPojoConverter.of(userCreateRequest, hashedPassword);
try {
userDao.insertAndReturnId(pojo);
return userDao.insertAndReturnId(pojo);
} catch (DuplicateKeyException e) {
throw DomainErrors.USERNAME_OR_EMAIL_DUPLICATE.exception();
}

View File

@@ -0,0 +1,71 @@
package com.databasir.core.infrastructure.oauth2;
import com.databasir.core.infrastructure.remote.github.GithubRemoteService;
import com.databasir.dao.enums.OAuthAppType;
import com.databasir.dao.impl.OAuthAppDao;
import com.databasir.dao.tables.pojos.OauthAppPojo;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class GithubOauthHandler implements OAuthHandler {
private final GithubRemoteService githubRemoteService;
private final OAuthAppDao oAuthAppDao;
@Override
public boolean support(String oauthAppType) {
return OAuthAppType.GITHUB.isSame(oauthAppType);
}
@Override
public String authorization(String registrationId) {
OauthAppPojo app = oAuthAppDao.selectByRegistrationId(registrationId);
String authUrl = app.getAuthUrl();
String clientId = app.getClientId();
String authorizeUrl = authUrl + "/login/oauth/authorize";
String url = UriComponentsBuilder.fromUriString(authorizeUrl)
.queryParam("client_id", clientId)
.queryParam("scope", "read:user user:email")
.encode()
.build()
.toUriString();
return url;
}
@Override
public OAuthProcessResult process(OAuthProcessContext context) {
OauthAppPojo authApp = oAuthAppDao.selectByRegistrationId(context.getRegistrationId());
String clientId = authApp.getClientId();
String clientSecret = authApp.getClientSecret();
String baseUrl = authApp.getResourceUrl();
Map<String, String> params = context.getCallbackParameters();
String code = params.get("code");
String accessToken = githubRemoteService.getToken(baseUrl, clientId, clientSecret, code)
.get("access_token")
.asText();
String email = null;
for (JsonNode node : githubRemoteService.getEmail(baseUrl, accessToken)) {
if (node.get("primary").asBoolean()) {
email = node.get("email").asText();
}
}
JsonNode profile = githubRemoteService.getProfile(baseUrl, accessToken);
String nickname = profile.get("name").asText();
String avatar = profile.get("avatar_url").asText();
OAuthProcessResult result = new OAuthProcessResult();
result.setEmail(email);
result.setNickname(nickname);
result.setUsername(email);
result.setAvatar(avatar);
return result;
}
}

View File

@@ -0,0 +1,61 @@
package com.databasir.core.infrastructure.oauth2;
import com.databasir.core.domain.login.data.LoginKeyResponse;
import com.databasir.core.domain.login.service.LoginService;
import com.databasir.core.domain.user.data.UserCreateRequest;
import com.databasir.core.domain.user.service.UserService;
import com.databasir.core.infrastructure.oauth2.data.OAuthAppResponse;
import com.databasir.dao.impl.OAuthAppDao;
import com.databasir.dao.tables.pojos.OauthAppPojo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class OAuthAppService {
private final List<OAuthHandler> oAuthHandlers;
private final OAuthAppDao oAuthAppDao;
private final UserService userService;
private final LoginService loginService;
public LoginKeyResponse oauthCallback(String registrationId, Map<String, String> params) {
// match handler
OauthAppPojo app = oAuthAppDao.selectByRegistrationId(registrationId);
OAuthHandler oAuthHandler = oAuthHandlers.stream()
.filter(handler -> handler.support(app.getAppType()))
.findFirst()
.orElseThrow();
// process by handler
OAuthProcessContext context = OAuthProcessContext.builder()
.callbackParameters(params)
.registrationId(registrationId)
.build();
OAuthProcessResult result = oAuthHandler.process(context);
// create new user
UserCreateRequest user = new UserCreateRequest();
user.setUsername(result.getUsername());
user.setNickname(result.getNickname());
user.setEmail(result.getEmail());
user.setAvatar(result.getAvatar());
user.setPassword(UUID.randomUUID().toString().substring(0, 6));
Integer userId = userService.create(user);
return loginService.generate(userId);
}
public List<OAuthAppResponse> listAll() {
return null;
}
}

View File

@@ -0,0 +1,10 @@
package com.databasir.core.infrastructure.oauth2;
public interface OAuthHandler {
boolean support(String oauthAppType);
String authorization(String registrationId);
OAuthProcessResult process(OAuthProcessContext context);
}

View File

@@ -0,0 +1,22 @@
package com.databasir.core.infrastructure.oauth2;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OAuthProcessContext {
private String registrationId;
@Builder.Default
private Map<String, String> callbackParameters = new HashMap<>();
}

View File

@@ -0,0 +1,16 @@
package com.databasir.core.infrastructure.oauth2;
import lombok.Data;
@Data
public class OAuthProcessResult {
private String email;
private String username;
private String nickname;
private String avatar;
}

View File

@@ -0,0 +1,32 @@
package com.databasir.core.infrastructure.oauth2.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OAuthAppResponse {
private Integer id;
private String name;
private String icon;
private String registrationId;
private String clientId;
private String clientSecret;
private LocalDateTime updateAt;
private LocalDateTime createAt;
}

View File

@@ -0,0 +1,34 @@
package com.databasir.core.infrastructure.remote;
import com.databasir.core.infrastructure.remote.github.GithubApiClient;
import com.databasir.core.infrastructure.remote.github.GithubOauthClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
@Configuration
@RequiredArgsConstructor
@Slf4j
public class ClientConfig {
@Bean
public GithubApiClient githubApiClient() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(JacksonConverterFactory.create())
.build();
return retrofit.create(GithubApiClient.class);
}
@Bean
public GithubOauthClient githubOauthClient() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://github.com")
.addConverterFactory(JacksonConverterFactory.create())
.build();
return retrofit.create(GithubOauthClient.class);
}
}

View File

@@ -0,0 +1,23 @@
package com.databasir.core.infrastructure.remote.github;
import com.fasterxml.jackson.databind.JsonNode;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.Url;
public interface GithubApiClient {
@GET
@Headers(value = {
"Accept: application/json"
})
Call<JsonNode> getEmail(@Url String url, @Header("Authorization") String token);
@GET
@Headers(value = {
"Accept: application/json"
})
Call<JsonNode> getProfile(@Url String url, @Header("Authorization") String token);
}

View File

@@ -0,0 +1,20 @@
package com.databasir.core.infrastructure.remote.github;
import com.fasterxml.jackson.databind.JsonNode;
import retrofit2.Call;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.QueryMap;
import retrofit2.http.Url;
import java.util.Map;
public interface GithubOauthClient {
@Headers(value = {
"Accept: application/json"
})
@POST
Call<JsonNode> getAccessToken(@Url String url, @QueryMap Map<String, String> request);
}

View File

@@ -0,0 +1,74 @@
package com.databasir.core.infrastructure.remote.github;
import com.databasir.common.SystemException;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import retrofit2.Call;
import retrofit2.Response;
import java.io.IOException;
@Service
@RequiredArgsConstructor
@Slf4j
public class GithubRemoteService {
private static final String TOKEN_PREFIX = "token ";
private final GithubApiClient githubApiClient;
private final GithubOauthClient githubOauthClient;
public JsonNode getToken(String baseUrl,
String clientId,
String clientSecret,
String code) {
TokenRequest request = TokenRequest.builder()
.client_id(clientId)
.client_secret(clientSecret)
.code(code)
.build();
String path = "/login/oauth/access_token";
String url = baseUrl + path;
return execute(githubOauthClient.getAccessToken(url, request.toMap()));
}
public JsonNode getEmail(String baseUrl, String token) {
String path;
if (baseUrl.contains("api.github.com")) {
path = "/user/emails";
} else {
path = "/api/v3/user/emails";
}
String url = baseUrl + path;
return execute(githubApiClient.getEmail(url, TOKEN_PREFIX + token));
}
public JsonNode getProfile(String baseUrl, String token) {
String path;
if (baseUrl.contains("api.github.com")) {
path = "/user";
} else {
path = "/api/v3/user";
}
String url = baseUrl + path;
return execute(githubApiClient.getProfile(url, TOKEN_PREFIX + token));
}
private <T> T execute(Call<T> call) {
try {
Response<T> response = call.execute();
if (!response.isSuccessful()) {
log.error("request error: " + call.request());
throw new SystemException("Call Remote Error");
} else {
T body = response.body();
return body;
}
} catch (IOException e) {
throw new SystemException("System Error", e);
}
}
}

View File

@@ -0,0 +1,32 @@
package com.databasir.core.infrastructure.remote.github;
import lombok.Builder;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
@SuppressWarnings("checkstyle:all")
@Builder
public class TokenRequest {
private String client_id;
private String client_secret;
private String code;
private String redirect_uri;
public Map<String, String> toMap() {
HashMap<String, String> map = new HashMap<>();
map.put("client_id", client_id);
map.put("client_secret", client_secret);
map.put("code", code);
if (redirect_uri != null) {
map.put("redirect_uri", redirect_uri);
}
return map;
}
}