mirror of
https://github.com/vran-dev/databasir.git
synced 2025-08-08 18:25:17 +08:00
Feature/oauth2 github (#27)
* feat: jooq generate * feat: support github oauth * feat: support oauth2 login * feat: update content-type * feat: add custom authentication exception * feat: add oauth2 app api * fix: checkstyle
This commit is contained in:
@@ -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'
|
||||
|
||||
|
@@ -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) {
|
||||
@@ -53,4 +52,14 @@ public class LoginController {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,87 @@
|
||||
package com.databasir.api;
|
||||
|
||||
import com.databasir.common.JsonData;
|
||||
import com.databasir.core.domain.app.OpenAuthAppService;
|
||||
import com.databasir.core.domain.app.data.*;
|
||||
import com.databasir.core.domain.app.handler.OpenAuthHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.data.domain.Sort.Direction.DESC;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class OpenAuth2AppController {
|
||||
|
||||
private final OpenAuthHandler openAuthHandler;
|
||||
|
||||
private final OpenAuthAppService openAuthAppService;
|
||||
|
||||
/**
|
||||
* 无需授权
|
||||
*/
|
||||
@GetMapping("/oauth2/authorization/{registrationId}")
|
||||
@ResponseBody
|
||||
public JsonData<String> authorization(@PathVariable String registrationId) {
|
||||
String authorization = openAuthHandler.authorization(registrationId);
|
||||
return JsonData.ok(authorization);
|
||||
}
|
||||
|
||||
/**
|
||||
* 无需授权
|
||||
*/
|
||||
@GetMapping("/oauth2/apps")
|
||||
@ResponseBody
|
||||
public JsonData<List<OAuthAppResponse>> listApps() {
|
||||
return JsonData.ok(openAuthAppService.listAll());
|
||||
}
|
||||
|
||||
@GetMapping(Routes.OAuth2App.LIST_PAGE)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
|
||||
@ResponseBody
|
||||
public JsonData<Page<OAuthAppPageResponse>> listPage(@PageableDefault(sort = "id", direction = DESC)
|
||||
Pageable page,
|
||||
OAuthAppPageCondition condition) {
|
||||
return JsonData.ok(openAuthAppService.listPage(page, condition));
|
||||
}
|
||||
|
||||
@GetMapping(Routes.OAuth2App.GET_ONE)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
|
||||
@ResponseBody
|
||||
public JsonData<OAuthAppDetailResponse> getOne(@PathVariable Integer id) {
|
||||
return JsonData.ok(openAuthAppService.getOne(id));
|
||||
|
||||
}
|
||||
|
||||
@PostMapping(Routes.OAuth2App.CREATE)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
|
||||
@ResponseBody
|
||||
public JsonData<Integer> create(@RequestBody @Valid OAuthAppCreateRequest request) {
|
||||
Integer id = openAuthAppService.create(request);
|
||||
return JsonData.ok(id);
|
||||
}
|
||||
|
||||
@PatchMapping(Routes.OAuth2App.UPDATE)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
|
||||
@ResponseBody
|
||||
public JsonData<Void> updateById(@RequestBody @Valid OAuthAppUpdateRequest request) {
|
||||
openAuthAppService.updateById(request);
|
||||
return JsonData.ok();
|
||||
}
|
||||
|
||||
@DeleteMapping(Routes.OAuth2App.DELETE)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
|
||||
@ResponseBody
|
||||
public JsonData<Void> deleteById(@PathVariable Integer id) {
|
||||
openAuthAppService.deleteById(id);
|
||||
return JsonData.ok();
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -21,6 +22,7 @@ public class OperationLogController {
|
||||
private final OperationLogService operationLogService;
|
||||
|
||||
@GetMapping(Routes.OperationLog.LIST)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
|
||||
public JsonData<Page<OperationLogPageResponse>> list(@PageableDefault(sort = "id", direction = Sort.Direction.DESC)
|
||||
Pageable page,
|
||||
OperationLogPageCondition condition) {
|
||||
|
@@ -102,9 +102,26 @@ public interface Routes {
|
||||
String LOGOUT = "/logout";
|
||||
|
||||
String REFRESH_ACCESS_TOKEN = "/access_tokens";
|
||||
|
||||
String LOGIN_INFO = "/login_info";
|
||||
|
||||
}
|
||||
|
||||
interface OperationLog {
|
||||
String LIST = BASE + "/operation_logs";
|
||||
}
|
||||
|
||||
interface OAuth2App {
|
||||
|
||||
String LIST_PAGE = BASE + "/oauth2_apps";
|
||||
|
||||
String CREATE = BASE + "/oauth2_apps";
|
||||
|
||||
String UPDATE = BASE + "/oauth2_apps";
|
||||
|
||||
String DELETE = BASE + "/oauth2_apps/{id}";
|
||||
|
||||
String GET_ONE = BASE + "/oauth2_apps/{id}";
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -45,6 +45,8 @@ 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()
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
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.domain.app.OpenAuthAppService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
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.security.web.authentication.AuthenticationFailureHandler;
|
||||
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 OpenAuthAppService openAuthAppService;
|
||||
|
||||
@Autowired
|
||||
private DatabasirUserDetailService databasirUserDetailService;
|
||||
|
||||
public DatabasirOauth2LoginFilter(AuthenticationManager authenticationManager,
|
||||
OAuth2AuthenticationSuccessHandler auth2AuthenticationSuccessHandler,
|
||||
AuthenticationFailureHandler authenticationFailureHandler) {
|
||||
super(OAUTH_LOGIN_URI, authenticationManager);
|
||||
this.setAuthenticationSuccessHandler(auth2AuthenticationSuccessHandler);
|
||||
this.setAuthenticationFailureHandler(authenticationFailureHandler);
|
||||
}
|
||||
|
||||
@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 = openAuthAppService.oauthCallback(registrationId, params);
|
||||
UserDetails details = databasirUserDetailService.loadUserByUsername(userDetailResponse.getUsername());
|
||||
DatabasirOAuth2Authentication authentication = new DatabasirOAuth2Authentication(details);
|
||||
if (!userDetailResponse.getEnabled()) {
|
||||
throw new DisabledException("账号已禁用");
|
||||
}
|
||||
authentication.setAuthenticated(true);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("login {} success", registrationId);
|
||||
}
|
||||
return authentication;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
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.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;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@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("请重新登陆"));
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
objectMapper.writeValue(response.getWriter(), JsonData.ok(data));
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
package com.databasir.api.config.security;
|
||||
|
||||
import com.databasir.core.domain.app.exception.DatabasirAuthenticationException;
|
||||
import com.databasir.common.JsonData;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -39,6 +40,12 @@ public class DatabasirAuthenticationFailureHandler implements AuthenticationFail
|
||||
String jsonString = objectMapper.writeValueAsString(data);
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
response.getOutputStream().write(jsonString.getBytes(StandardCharsets.UTF_8));
|
||||
} else if (exception instanceof DatabasirAuthenticationException) {
|
||||
DatabasirAuthenticationException bizException = (DatabasirAuthenticationException) exception;
|
||||
JsonData<Void> data = JsonData.error("-1", bizException.getMessage());
|
||||
String jsonString = objectMapper.writeValueAsString(data);
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
response.getOutputStream().write(jsonString.getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
JsonData<Void> data = JsonData.error("-1", "未登录或未授权用户");
|
||||
String jsonString = objectMapper.writeValueAsString(data);
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user