Spring Boot - Google oAuth2 from external client on VueJS
Проблема: не получается настроить просто отправку get запроса для авторизации так, чтобы не нужно было делать такой костыль как у меня с редиректом. И так же не получается выполнить грамотный запрос на logout и перехватить его ответ на клиенте. Описание текущей реализации смотрите ниже.
upd. Авторизацию сделал. Redirect в google уходит теперь правильно. Нужно на сервере понять как сделать грамотный redirect не через кастомный loginSuccess на мой localhost:8000/
Небольшое описание содержания кода: Конфиг oAuth2 (2), конфиг vue-cli (4), конфиг axios (5). Использую spring-boot:2.3.0.
Описание текущей реализации: авторизация с клиента (localhost:8000), который написан на VueJS, проходит только через redirect на серверный url (1). После чего я ловлю redirect (2) с google сервера и делаю redirect на клиентский url в rest контроллере (3). Пользователя после авторизации я могу создать и получить (1). После авторизации, если я захочу сделать logout, то отправляю post на сервер, а после получаю ошибку, но при этом выход произошел и пользователя без авторизации я больше получить не могу. Но после logout я получаю ошибку (0), которой хотел бы избежать.
0) Ошибка редиректа с google сервера:
1) Работа с авторизацией на клиенте:
import axios from './axiosApi'
export default {
getUser() {
return axios.get('/api/auth/create-or-get-user');
},
logout() {
const CSRF_TOKEN_OBJECT = document.cookie.match(
new RegExp(`XSRF-TOKEN=([^;]+)`));
const headers = {
'X-XSRF-TOKEN': CSRF_TOKEN_OBJECT
? CSRF_TOKEN_OBJECT[1] : '',
'Access-Control-Allow-Origin': '*'
}
return axios.postWithCustomHeaders('/api/logout', headers);
}
}
2) Конфигурация Java сервера:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.authorizeRequests(a -> a
.antMatchers("/**", "/auth/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(e -> e
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
)
.csrf(c -> c
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.logout(l -> l
.deleteCookies("remove")
.invalidateHttpSession(true)
.permitAll()
)
.oauth2Login(o -> o
.authorizationEndpoint()
.baseUri("/login").and()
.defaultSuccessUrl("/auth/loginSuccess")
);
}
}
}
3) RestContoller для обработки авторизации пользователя, получения или его создания:
@RestController
@RequestMapping("auth")
public class AuthController {
private final UserDetailsRepo userDetailsRepo;
public AuthController(UserDetailsRepo userDetailsRepo) {
this.userDetailsRepo = userDetailsRepo;
}
@GetMapping("loginSuccess")
public RedirectView redirectIntoClient() {
String CLIENT_URL = "http://localhost:8000/";
return configureRedirectView(CLIENT_URL);
}
@GetMapping("create-or-get-user")
@JsonView(Views.IdName.class)
public User user(@AuthenticationPrincipal OAuth2User principal) {
if (Objects.isNull(principal)) {
return null;
}
String id = principal.getAttribute("sub");
if (StringUtil.isBlank(id)) {
return null;
}
User user = userDetailsRepo.findById(id).orElseGet(() -> {
User newUser = new User();
newUser.setId(id);
newUser.setName(principal.getAttribute("name"));
newUser.setEmail(principal.getAttribute("email"));
newUser.setGender(principal.getAttribute("gender"));
newUser.setLocale(principal.getAttribute("locale"));
newUser.setUserpic(principal.getAttribute("picture"));
return newUser;
});
user.setLastVisit(LocalDateTime.now());
return userDetailsRepo.save(user);
}
private RedirectView configureRedirectView(String redirectUrl) {
RedirectView redirectView = new RedirectView();
redirectView.setStatusCode(HttpStatus.FOUND);
redirectView.setUrl(redirectUrl);
return redirectView;
}
}
4) vue.config.js:
module.exports = {
"outputDir": "target/dist",
"assetsDir": "static",
"publicPath": "./",
"devServer": {
"contentBase": "./dist",
"compress": true,
"port": 8000,
"proxy": {
"/api" : {
target: 'http://localhost:9000',
ws: true,
changeOrigin: true,
pathRewrite: {
'^/api': '',
},
}
},
"stats": "errors-only",
"clientLogLevel": "error"
},
"transpileDependencies": [
"vuetify"
],
}
5) axiosApi.js:
import axios from 'axios'
import store from './../store/index'
function request(method, url, params, data, headers) {
let axiosConfig = {
method: method,
url: url,
}
if (params) {
axiosConfig.params = params;
}
if (data) {
axiosConfig.data = data;
}
if (headers) {
axiosConfig.headers = headers;
}
store.commit('settings/setLoading', true);
axios.interceptors.response.use(function (response) {
store.commit('settings/setLoading', false);
return response;
}, function (error) {
store.commit('settings/setLoading', false);
return Promise.reject(error);
});
return axios(axiosConfig);
}
function get(url) {
return request('get', url, null, null, null)
}
function post(url, data) {
data = data || {}
return request('post', url, null, data, null)
}
function postWithCustomHeaders(url, headers) {
return request('post', url, null, null, headers)
}
function put(url, data) {
data = data || {}
return request('put', url, null, data, null)
}
function remove(url) {
return request('delete', url, null, null, null)
}
export default {get, post, postWithCustomHeaders, put, remove, request}
