feat: support oauth2 login

This commit is contained in:
vran
2022-03-01 23:27:10 +08:00
parent 18edc58d2f
commit c1ab379f31
23 changed files with 307 additions and 137 deletions

View File

@@ -26,6 +26,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-quartz'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.flywaydb:flyway-core'

View File

@@ -1,5 +1,6 @@
package com.databasir.api;
import com.databasir.api.config.security.DatabasirUserDetails;
import com.databasir.common.DatabasirException;
import com.databasir.common.JsonData;
import com.databasir.common.exception.InvalidTokenException;
@@ -7,10 +8,10 @@ import com.databasir.core.domain.DomainErrors;
import com.databasir.core.domain.log.annotation.Operation;
import com.databasir.core.domain.login.data.AccessTokenRefreshRequest;
import com.databasir.core.domain.login.data.AccessTokenRefreshResponse;
import com.databasir.core.domain.login.data.UserLoginResponse;
import com.databasir.core.domain.login.service.LoginService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@@ -27,8 +28,6 @@ import java.util.Objects;
@Slf4j
public class LoginController {
private final AuthenticationManager authenticationManager;
private final LoginService loginService;
@GetMapping(Routes.Login.LOGOUT)
@@ -39,8 +38,8 @@ public class LoginController {
}
@PostMapping(Routes.Login.REFRESH_ACCESS_TOKEN)
public JsonData<AccessTokenRefreshResponse> refreshAccessTokens(@RequestBody @Valid
AccessTokenRefreshRequest request) {
public JsonData<AccessTokenRefreshResponse> refreshAccessTokens(@RequestBody
@Valid AccessTokenRefreshRequest request) {
try {
return JsonData.ok(loginService.refreshAccessTokens(request));
} catch (DatabasirException e) {
@@ -54,4 +53,13 @@ public class LoginController {
}
}
@GetMapping(Routes.Login.LOGIN_INFO)
public JsonData<UserLoginResponse> getUserLoginData() {
DatabasirUserDetails user = (DatabasirUserDetails) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
Integer userId = user.getUserPojo().getId();
return JsonData.ok(loginService.getUserLoginData(userId));
}
}

View File

@@ -0,0 +1,36 @@
package com.databasir.api;
import com.databasir.common.JsonData;
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.ResponseBody;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class OAuth2AppController {
private final OAuthHandler oAuthHandler;
private final OAuthAppService oAuthAppService;
@GetMapping("/oauth2/authorization/{registrationId}")
@ResponseBody
public JsonData<String> authorization(@PathVariable String registrationId) {
String authorization = oAuthHandler.authorization(registrationId);
return JsonData.ok(authorization);
}
@GetMapping("/oauth2/apps")
@ResponseBody
public JsonData<List<OAuthAppResponse>> listApps() {
return JsonData.ok(oAuthAppService.listAll());
}
}

View File

@@ -1,60 +0,0 @@
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());
}
}

View File

@@ -102,6 +102,9 @@ public interface Routes {
String LOGOUT = "/logout";
String REFRESH_ACCESS_TOKEN = "/access_tokens";
String LOGIN_INFO = "/login_info";
}
interface OperationLog {

View File

@@ -1,6 +1,7 @@
package com.databasir.api.config;
import com.databasir.api.Routes;
import com.databasir.api.config.oauth2.DatabasirOauth2LoginFilter;
import com.databasir.api.config.security.*;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;

View File

@@ -0,0 +1,28 @@
package com.databasir.api.config.oauth2;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
public class DatabasirOAuth2Authentication extends AbstractAuthenticationToken {
private Object credentials;
private Object principal;
public DatabasirOAuth2Authentication(UserDetails principal) {
super(principal.getAuthorities());
this.credentials = null;
this.principal = principal;
setAuthenticated(false);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}

View File

@@ -0,0 +1,55 @@
package com.databasir.api.config.oauth2;
import com.databasir.api.config.security.DatabasirUserDetailService;
import com.databasir.core.domain.user.data.UserDetailResponse;
import com.databasir.core.infrastructure.oauth2.OAuthAppService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
@Component
@Slf4j
public class DatabasirOauth2LoginFilter extends AbstractAuthenticationProcessingFilter {
public static final String OAUTH_LOGIN_URI = "/oauth2/login/*";
@Autowired
private OAuthAppService oAuthAppService;
@Autowired
private DatabasirUserDetailService databasirUserDetailService;
public DatabasirOauth2LoginFilter(AuthenticationManager authenticationManager,
OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler) {
super(OAUTH_LOGIN_URI, authenticationManager);
this.setAuthenticationSuccessHandler(oAuth2AuthenticationSuccessHandler);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
Map<String, String[]> params = request.getParameterMap();
String registrationId = new AntPathMatcher().extractPathWithinPattern(OAUTH_LOGIN_URI, request.getRequestURI());
UserDetailResponse userDetailResponse = oAuthAppService.oauthCallback(registrationId, params);
UserDetails details = databasirUserDetailService.loadUserByUsername(userDetailResponse.getUsername());
DatabasirOAuth2Authentication authentication = new DatabasirOAuth2Authentication(details);
authentication.setAuthenticated(true);
if (log.isDebugEnabled()) {
log.debug("login {} success", registrationId);
}
return authentication;
}
}

View File

@@ -0,0 +1,38 @@
package com.databasir.api.config.oauth2;
import com.databasir.api.config.security.DatabasirUserDetails;
import com.databasir.common.JsonData;
import com.databasir.core.domain.login.data.LoginKeyResponse;
import com.databasir.core.domain.login.data.UserLoginResponse;
import com.databasir.core.domain.login.service.LoginService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final LoginService loginService;
private final ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
DatabasirUserDetails details = (DatabasirUserDetails) authentication.getPrincipal();
LoginKeyResponse loginKey = loginService.generate(details.getUserPojo().getId());
UserLoginResponse data = loginService.getUserLoginData(details.getUserPojo().getId())
.orElseThrow(() -> new CredentialsExpiredException("请重新登陆"));
objectMapper.writeValue(response.getWriter(), JsonData.ok(data));
}
}

View File

@@ -2,11 +2,12 @@ package com.databasir.api.config.security;
import com.databasir.common.JsonData;
import com.databasir.core.domain.login.data.LoginKeyResponse;
import com.databasir.core.domain.login.data.UserLoginResponse;
import com.databasir.core.domain.login.service.LoginService;
import com.databasir.core.domain.user.data.UserLoginResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
@@ -16,9 +17,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.util.List;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
@@ -35,28 +33,10 @@ public class DatabasirAuthenticationSuccessHandler implements AuthenticationSucc
DatabasirUserDetails user = (DatabasirUserDetails) authentication.getPrincipal();
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
UserLoginResponse data = new UserLoginResponse();
data.setId(user.getUserPojo().getId());
data.setNickname(user.getUserPojo().getNickname());
data.setEmail(user.getUserPojo().getEmail());
data.setUsername(user.getUserPojo().getUsername());
LoginKeyResponse loginKey = loginService.generate(user.getUserPojo().getId());
data.setAccessToken(loginKey.getAccessToken());
long expireAt = loginKey.getAccessTokenExpireAt().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
data.setAccessTokenExpireAt(expireAt);
data.setRefreshToken(loginKey.getRefreshToken());
List<UserLoginResponse.RoleResponse> roles = user.getRoles()
.stream()
.map(ur -> {
UserLoginResponse.RoleResponse roleResponse = new UserLoginResponse.RoleResponse();
roleResponse.setRole(ur.getRole());
roleResponse.setGroupId(ur.getGroupId());
return roleResponse;
})
.collect(Collectors.toList());
data.setRoles(roles);
UserLoginResponse data = loginService.getUserLoginData(user.getUserPojo().getId())
.orElseThrow(() -> new CredentialsExpiredException("请重新登陆"));
objectMapper.writeValue(response.getWriter(), JsonData.ok(data));
}
}

View File

@@ -1,6 +1,5 @@
package com.databasir.api.config.security;
import com.databasir.dao.impl.LoginDao;
import com.databasir.dao.impl.UserDao;
import com.databasir.dao.impl.UserRoleDao;
import com.databasir.dao.tables.pojos.UserPojo;
@@ -22,8 +21,6 @@ public class DatabasirUserDetailService implements UserDetailsService {
private final UserRoleDao userRoleDao;
private final LoginDao loginDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserPojo user = userDao.selectByEmailOrUsername(username)