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:
vran
2022-03-02 20:13:25 +08:00
committed by GitHub
parent e799940c2d
commit cd20dfd7cf
52 changed files with 2120 additions and 63 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) {
@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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}";
}
}

View File

@@ -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()

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,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;
}
}

View File

@@ -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));
}
}

View File

@@ -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);

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)

View File

@@ -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