feat: support github oauth
This commit is contained in:
parent
ad8cbab226
commit
18edc58d2f
|
@ -53,4 +53,5 @@ public class LoginController {
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.databasir.api;
|
||||
|
||||
import com.databasir.common.JsonData;
|
||||
import com.databasir.core.domain.login.data.LoginKeyResponse;
|
||||
import com.databasir.core.infrastructure.oauth2.OAuthAppService;
|
||||
import com.databasir.core.infrastructure.oauth2.OAuthHandler;
|
||||
import com.databasir.core.infrastructure.oauth2.data.OAuthAppResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class OAuth2LoginController {
|
||||
|
||||
private final OAuthHandler oAuthHandler;
|
||||
|
||||
private final OAuthAppService oAuthAppService;
|
||||
|
||||
@GetMapping("/oauth2/authorization/{registrationId}")
|
||||
public RedirectView authorization(@PathVariable String registrationId) {
|
||||
String authorization = oAuthHandler.authorization(registrationId);
|
||||
return new RedirectView(authorization);
|
||||
}
|
||||
|
||||
@GetMapping("/oauth2/login/{registrationId}")
|
||||
public RedirectView callback(@PathVariable String registrationId,
|
||||
@RequestParam Map<String, String> params,
|
||||
HttpServletResponse response) {
|
||||
LoginKeyResponse loginKey = oAuthAppService.oauthCallback(registrationId, params);
|
||||
// set cookie
|
||||
Cookie accessTokenCookie = new Cookie("accessToken", loginKey.getAccessToken());
|
||||
accessTokenCookie.setPath("/");
|
||||
response.addCookie(accessTokenCookie);
|
||||
|
||||
long epochSecond = loginKey.getAccessTokenExpireAt()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.toEpochMilli();
|
||||
Cookie accessTokenExpireAtCookie = new Cookie("accessTokenExpireAt", epochSecond + "");
|
||||
accessTokenExpireAtCookie.setPath("/");
|
||||
response.addCookie(accessTokenExpireAtCookie);
|
||||
return new RedirectView("/");
|
||||
}
|
||||
|
||||
@GetMapping("/oauth2/apps")
|
||||
public JsonData<List<OAuthAppResponse>> listApps() {
|
||||
return JsonData.ok(oAuthAppService.listAll());
|
||||
}
|
||||
|
||||
}
|
|
@ -45,6 +45,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/login", Routes.Login.REFRESH_ACCESS_TOKEN).permitAll()
|
||||
.antMatchers("/oauth2/apps", "/oauth2/failure", "/oauth2/authorization/*", "/oauth2/login/*").permitAll()
|
||||
.antMatchers("/", "/*.html", "/js/**", "/css/**", "/img/**", "/*.ico").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
|
|
|
@ -8,4 +8,4 @@ spring.jooq.sql-dialect=mysql
|
|||
|
||||
spring.flyway.enabled=true
|
||||
spring.flyway.baseline-on-migrate=true
|
||||
spring.flyway.locations=classpath:db/migration
|
||||
spring.flyway.locations=classpath:db/migration
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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<>();
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.databasir.dao.enums;
|
||||
|
||||
public enum OAuthAppType {
|
||||
|
||||
GITHUB, GITLAB;
|
||||
|
||||
public boolean isSame(String type) {
|
||||
return this.name().equalsIgnoreCase(type);
|
||||
}
|
||||
}
|
|
@ -1,22 +1,37 @@
|
|||
package com.databasir.dao.impl;
|
||||
|
||||
import com.databasir.dao.tables.OauthApp;
|
||||
import com.databasir.dao.exception.DataNotExistsException;
|
||||
import com.databasir.dao.tables.pojos.OauthAppPojo;
|
||||
import lombok.Getter;
|
||||
import org.jooq.DSLContext;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.databasir.dao.Tables.OAUTH_APP;
|
||||
|
||||
@Repository
|
||||
public class OAuthAppDao extends BaseDao<OauthApp> {
|
||||
public class OAuthAppDao extends BaseDao<OauthAppPojo> {
|
||||
|
||||
@Autowired
|
||||
@Getter
|
||||
private DSLContext dslContext;
|
||||
|
||||
public OAuthAppDao() {
|
||||
super(OAUTH_APP, OauthApp.class);
|
||||
super(OAUTH_APP, OauthAppPojo.class);
|
||||
}
|
||||
|
||||
public Optional<OauthAppPojo> selectOptionByRegistrationId(String registrationId) {
|
||||
return this.getDslContext()
|
||||
.select(OAUTH_APP.fields()).from(OAUTH_APP).where(OAUTH_APP.REGISTRATION_ID.eq(registrationId))
|
||||
.fetchOptionalInto(OauthAppPojo.class);
|
||||
}
|
||||
|
||||
public OauthAppPojo selectByRegistrationId(String registrationId) {
|
||||
return this.getDslContext()
|
||||
.select(OAUTH_APP.fields()).from(OAUTH_APP).where(OAUTH_APP.REGISTRATION_ID.eq(registrationId))
|
||||
.fetchOptionalInto(OauthAppPojo.class)
|
||||
.orElseThrow(() -> new DataNotExistsException("can not found oauth app by " + registrationId));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
CREATE TABLE IF NOT EXISTS oauth_app
|
||||
(
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
registration_id VARCHAR(100) NOT NULL,
|
||||
app_name VARCHAR(128) NOT NULL,
|
||||
app_icon VARCHAR(256) NOT NULL DEFAULT '',
|
||||
app_type VARCHAR(64) NOT NULL COMMENT 'github, gitlab',
|
||||
client_id VARCHAR(256),
|
||||
client_secret VARCHAR(256),
|
||||
auth_url VARCHAR(256),
|
||||
resource_url VARCHAR(256),
|
||||
scope VARCHAR(256),
|
||||
update_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
create_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE uk_registration_id (registration_id)
|
||||
) CHARSET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci COMMENT 'oauth app info';
|
||||
|
Loading…
Reference in New Issue