Основной демонстрацией является использование Spring Security и JWT для создания серверных API.
Основные возможности включают вход (как добавить проверку CAPTCHA при входе в Spring Security), поиск, создание, удаление и разделение прав доступа между пользователями.
Примечание: Поскольку это просто пример, база данных не используется, все операции CRUD выполняются в HashMap.
Демонстрация использования Vue для создания клиентской части и её взаимодействия с серверной частью, включая настройку CORS, проверку входа на стороне клиента и реализацию запросов POST, GET, DELETE. Также показано, как использовать XSRF-TOKEN от сервера для защиты от атак типа CSRF.
Компонент | Технология |
---|---|
Клиент | Vue.js 2 |
Сервер (REST API) | Spring Boot (Java) |
Безопасность | Базирующаяся на токенах (Spring Security, JJWT, CSRF) |
Клиентская сборка | vue-cli3, Webpack, npm |
Сборка сервера | Maven## Быстрый запуск |
Java 11, Node 12
Инструменты сборки Maven 3, vue-cli3
Клонировать проект локально
git clone https://github.com/PuZhiweizuishuai/SpringSecurity-JWT-Vue-Demo.git
cd spring-security-jwt
mvn clean package
Затем запустите программу, которая по умолчанию работает на порту bk 8088
java -jar target/security-0.0.1-SNAPSHOT.jar
cd vue
npm install
Затем запустите программу, которая по умолчанию работает на порту 8080
npm run serve
Наконец, откройте браузер и введите
http://127.0.0.1:8080
Создайте проект Spring Boot, добавьте зависимости JJWT и Spring Security, что очень просто, есть множество руководств, которые содержат эту информацию. Единственное, что следует отметить, если вы используете версию Java 11, вам также потребуется добавить следующую зависимость, если вы используете Java 8, то нет необходимости.
Исправлено:
SpringSecurity-JWT-Vue-Deom
на правильное название SpringSecurity-JWT-Vue-Demo
.```xml
javax.xml.bind
jaxb-api
2.3.0
Чтобы использовать Spring Security для управления правами доступа пользователя, сначала необходимо реализовать простой объект `User`, который будет реализовывать интерфейс `UserDetails`. Интерфейс `UserDetails` отвечает за предоставление основной информации о пользователе. Если вам требуется только имя пользователя и пароль, а также нет необходимости в других данных, таких как код подтверждения, вы можете использовать встроенный класс `User` из Spring Security.```java
public class User implements UserDetails {
private String username;
private String password;
private Boolean rememberMe;
private String verifyCode;
private String power;
private Long expirationTime;
private List<GrantedAuthority> authorities;
/**
* Остальные методы get/set опущены
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Это наш объект User
, содержащий информацию о том, чтобы помнить меня, код подтверждения и другие данные входа. Поскольку Spring Security использует JWT, это фактически замена стандартного механизма проверки подлинности Spring Security своим собственным, что делает невозможным использование стандартного механизма "помним меня". Поэтому мы добавляем эту информацию в объект User
.
Создадим новый класс TokenAuthenticationHelper
, который будет использоваться для обработки процесса аутентификации и запросов.
public class TokenAuthenticationHelper {
/**
* Время жизни токена при отсутствии отметки "помним меня"
*/
private static final long EXPIRATION_TIME = 7200000;
/**
* Время жизни токена в cookies при наличии отметки "помним меня"
*/
private static final int COOKIE_EXPIRATION_TIME = 1296000;
}
``` private static final String SECRET_KEY = "ЭтоДоСпрингСикьюритиДемо";
public static final String COOKIE_TOKEN = "COOKIE-TOKEN";
public static final String XSRF = "XSRF-TOKEN";
}
/**
* Устанавливает возврат токена после успешной авторизации
* */
public static void addAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException {
// Получение ролей пользователя при входе
Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
// Обход ролей пользователя
StringBuffer stringBuffer = new StringBuffer();
authorities.forEach(authority -> {
stringBuffer.append(authority.getAuthority()).append(",");
});
long expirationTime = EXPIRATION_TIME;
int cookExpirationTime = -1;
// Обработка дополнительной информации при входе
LoginDetails loginDetails = (LoginDetails) authResult.getDetails();
if (loginDetails.getRememberMe() != null && loginDetails.getRememberMe()) {
expirationTime = COOKIE_EXPIRATION_TIME * 1000;
cookExpirationTime = COOKIE_EXPIRATION_TIME;
}
String jwt = Jwts.builder()
// Подлежащий проверке субъект (subject)
.setSubject(authResult.getName())
// Установка прав доступа пользователя
.claim("authorities", stringBuffer)
// Время истечения
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
// Алгоритм подписи
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
Cookie cookie = new Cookie(COOKIE_TOKEN, jwt);
cookie.setHttpOnly(true);
cookie.setPath("/");
cookie.setMaxAge(cookExpirationTime);
response.addCookie(cookie);
} // Отправка данных на фронтенд LoginResultDetails loginResultDetails = new LoginResultDetails(); ResultDetails resultDetails = new ResultDetails(); resultDetails.setStatus(HttpStatus.OK.value()); resultDetails.setMessage("Аутентификация прошла успешно!"); resultDetails.setSuccess(true); resultDetails.setTimestamp(LocalDateTime.now()); User user = new User(); user.setUsername(authResult.getName()); user.setPower(stringBuffer.toString()); user.setExpirationTime(System.currentTimeMillis() + expirationTime); }```markdown loginResultDetails.setResultDetails(resultDetails); loginResultDetails.setUser(user); loginResultDetails.setStatus(200); response.setContentType("application/json; charset=UTF-8"); PrintWriter out = response.getWriter(); out.write(new ObjectMapper().writeValueAsString(loginResultDetails)); out.flush(); out.close();
```markdown
## Валидация запроса
``````java
/**
* Валидация запроса
*/
public static Authentication getAuthentication(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, COOKIE_TOKEN);
String token = cookie != null ? cookie.getValue() : null;
if (token != null) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
// Получение прав пользователя
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("authorities").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
String userName = claims.getSubject();
if (userName != null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, null, authorities);
usernamePasswordAuthenticationToken.setDetails(claims);
return usernamePasswordAuthenticationToken;
}
return null;
}
return null;
}
Метод addAuthentication
отвечает за возврат информации о успешной аутентификации, используя HTTP-only Cookie, что эффективно предотвращает атаки типа XSS.
После успешной аутентификации возвращаются права пользователя, имя пользователя, время истечения сессии, что помогает фронтенду создать подходящий пользовательский интерфейс.
Метод getAuthentication
отвечает за валидацию других запросов пользователя. Если JWT корректно распарсен, метод возвращает объект UsernamePasswordAuthenticationToken
, который передается Spring Security вместе с правами доступа, чтобы они были помещены в текущий контекст и запрос мог продолжиться через цепочку фильтров.Таким образом, основные методы для аутентификации и валидации готовы.
Примечание: Классы LoginResultDetails
и ResultDetails
можно найти в исходном коде проекта, подробное описание которых приведено ниже из-за ограничения объема текста.
Как известно, Spring Security использует серию фильтров Servlet для обеспечения различных видов безопасности. Поэтому нам потребуется реализовать два фильтра, связанных с JWT:
Рассмотрим эти два фильтра подробнее, начнем с первого:
Создайте пакет с названием filter
в проекте, затем создайте внутри него класс с именем JwtLoginFilter
, который будет расширять класс AbstractAuthenticationProcessingFilter
. Этот класс является абстрактным процессором для HTTP-запросов аутентификации, основанных на браузере.
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
private final VerifyCodeService verifyCodeService;
private final LoginCountService loginCountService;
}
``````java
/**
* @param defaultFilterProcessesUrl Конфигурирует адрес для фильтрации, то есть адрес входа
* @param authenticationManager Менеджер аутентификации, используется для проверки аутентичности
* @param loginCountService
*/
public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager,
VerifyCodeService verifyCodeService, LoginCountService loginCountService) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
this.loginCountService = loginCountService;
// Устанавливаем значения свойств для AbstractAuthenticationProcessingFilter
setAuthenticationManager(authenticationManager);
this.verifyCodeService = verifyCodeService;
}
/**
* Извлечение учетной записи пользователя и проверка пароля
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
throws AuthenticationException, IOException, ServletException {
// Определение необходимости выброса исключения "Слишком частые попытки входа"
loginCountService.judgeLoginCount(httpServletRequest);
// Получение объекта User
// readValue принимает в качестве первого аргумента входящий поток, а вторым — тип объекта для преобразования
User user = new ObjectMapper().readValue(httpServletRequest.getInputStream(), User.class);
// Проверка капчи
verifyCodeService.verify(httpServletRequest.getSession().getId(), user.getVerifyCode());
// Экранирование HTML-тегов для защиты от атак типа XSS
String username = user.getUsername();
username = HtmlUtils.htmlEscape(username);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
username,
user.getPassword(),
user.getAuthorities()
);
// Добавление дополнительной информации для проверки
}
``````markdown
/**
* Обратный вызов успешной авторизации
* */
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
loginCountService.cleanLoginCount(request);
// Успешная авторизация
TokenAuthenticationHelper.addAuthentication(request, response, authResult);
}
``````markdown
## Описание класса
Этот класс выполняет следующие основные задачи:
1. Создание пользовательского `JwtLoginFilter`, который наследуется от `AbstractAuthenticationProcessingFilter` и реализует три метода по умолчанию. Переменная `defaultFilterProcessesUrl` представляет собой путь авторизации.
2. В методе `attemptAuthentication` извлекаются имя пользователя и пароль из входных данных, после чего они передаются в метод `authenticate()` класса `AuthenticationManager` для проверки.
3. При успешной проверке выполнение переходит в метод `successfulAuthentication`. Здесь используется ранее созданная функция `addAuthentication` для генерации токена, который затем записывается в HTTP-only cookie и отправляется клиенту.
4. При неудачной проверке выполнение переходит в метод `unsuccessfulAuthentication`. Этот метод возвращает сообщение об ошибке клиенту.
### Метод `unsuccessfulAuthentication`
```java
/**
* Обратный вызов неудачной авторизации
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
// Количество ошибочных запросов увеличивается на 1
loginCountService.addLoginCount(request, 1);
// Отправка данных в клиентскую часть
ErrorDetails errorDetails = new ErrorDetails();
errorDetails.setStatus(HttpStatus.UNAUTHORIZED.value());
errorDetails.setMessage("Авторизация не удалась!");
errorDetails.setError(failed.getLocalizedMessage());
errorDetails.setTimestamp(LocalDateTime.now());
errorDetails.setPath(request.getServletPath());
response.setContentType("application/json; charset=UTF-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(errorDetails));
out.flush();
out.close();
}
```Примечание: Методы verifyCodeService
и `loginCountService` непосредственно не относятся к данному контексту. Для получения информации о реализации этих методов обратитесь к исходному коду.
Основное, что следует учесть:
При возникновении аномалии с проверочным кодом необходимо расширять класс AuthenticationException
.
Как видно, это родительский класс для различных исключений в Spring Security. Нужно создать отдельный класс для исключения с проверочным кодом, который будет расширять AuthenticationException
, и выбрасывать его при необходимости.
Ниже приведён полный код, расположенный в классе com.bugaugaoshu.security.service.impl.DigitsVerifyCodeServiceImpl
:
@Override
public void verify(String key, String code) {
String lastVerifyCodeWithTimestamp = verifyCodeRepository.find(key);
// Если нет проверочного кода, генерируем случайный
if (lastVerifyCodeWithTimestamp == null) {
lastVerifyCodeWithTimestamp = appendTimestamp(randomDigitString(verifyCodeUtil.getLen()));
}
String[] lastVerifyCodeAndTimestamp = lastVerifyCodeWithTimestamp.split("#");
String lastVerifyCode = lastVerifyCodeAndTimestamp[0];
long timestamp = Long.parseLong(lastVerifyCodeAndTimestamp[1]);
if (timestamp + VERIFY_CODE_EXPIRE_TIMEOUT < System.currentTimeMillis()) {
throw new VerifyFailedException("Проверочный код истёк!");
} else if (!Objects.equals(code, lastVerifyCode)) {
throw new VerifyFailedException("Неверный проверочный код!");
}
}
Второй фильтр пользователя
public class JwtAuthenticationFilter extends OncePerRequestFilter {
``` @Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
try {
Authentication authentication = TokenAuthenticationHelper.getAuthentication(httpServletRequest);
// Проводим проверку пользователя, полученного по токену
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(httpServletRequest, httpServletResponse);
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException |
SignatureException | IllegalArgumentException e) {
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Токен истёк, вход недействителен.");
}
}
}
```Это довольно просто — анализируем полученный пользовательский токен, если он правильный, то добавляем текущего пользователя в контекст безопасности `SecurityContext`, предоставляя ему права доступа. В противном случае возвращаем ошибку истечения срока действия токена.
#### Конфигурация Spring Security
Далее мы настроим Spring Security, используя следующий код:
```java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static final String ADMIN = "ROLE_ADMIN";
public static final String USER = "ROLE_USER";
private final VerifyCodeService verifyCodeService;
private final LoginCountService loginCountService;
/**
* Открытые для доступа маршруты
*/
private static final String[] PERMIT_ALL_MAPPING = {
"/api/hello",
"/api/login",
"/api/home",
"/api/verifyImage",
"/api/image/verify",
"/images/**"
};
public WebSecurityConfig(VerifyCodeService verifyCodeService, LoginCountService loginCountService) {
this.verifyCodeService = verifyCodeService;
this.loginCountService = loginCountService;
}
``` ```java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
``````java
/**
* Настройка CORS
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
// Разрешенные URL для CORS
List<String> allowedOriginsUrl = new ArrayList<>();
allowedOriginsUrl.add("http://localhost:8080");
allowedOriginsUrl.add("http://127.0.0.1:8080");
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// Установка разрешенных URL для CORS
config.setAllowedOrigins(allowedOriginsUrl);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
@override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(PERMIT_ALL_MAPPING)
.permitAll()
.antMatchers("/api/user/**", "/api/data", "/api/logout")
// USER и ADMIN могут получить доступ
.hasAnyAuthority(USER, ADMIN)
.antMatchers("/api/admin/**")
// Только ADMIN может получить доступ
.hasAnyAuthority(ADMIN)
.anyRequest()
.authenticated()
.and()
// Добавляем цепочку фильтров, первый параметр - это фильтр, второй параметр - место добавления фильтра
// Логин-фильтр
.addFilterBefore(new JwtLoginFilter("/api/login", authenticationManager(), verifyCodeService, loginCountService), UsernamePasswordAuthenticationFilter.class)
// Аутентификационный фильтр
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// Включаем поддержку CORS
.cors()
.and()
// Включаем защиту от CSRF атак
.csrf()
// .disable();
.ignoringAntMatchers(PERMIT_ALL_MAPPING)
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
/**
* Настройка CORS
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
// Разрешенные URL для CORS
List<String> allowedOriginsUrl = new ArrayList<>();
allowedOriginsUrl.add("http://localhost:8080");
allowedOriginsUrl.add("http://127.0.0.1:8080");
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// Установка разрешенных URL для CORS
config.setAllowedOrigins(allowedOriginsUrl);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
@override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(PERMIT_ALL_MAPPING)
.permitAll()
.antMatchers("/api/user/**", "/api/data", "/api/logout")
// USER и ADMIN могут получить доступ
.hasAnyAuthority(USER, ADMIN)
.antMatchers("/api/admin/**")
// Только ADMIN может получить доступ
.hasAnyAuthority(ADMIN)
.anyRequest()
.authenticated()
.and()
// Добавляем цепочку фильтров, первый параметр - это фильтр, второй параметр - место добавления фильтра
// Логин-фильтр
.addFilterBefore(new JwtLoginFilter("/api/login", authenticationManager(), verifyCodeService, loginCountService), UsernamePasswordAuthenticationFilter.class)
// Аутентификационный фильтр
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// Включаем поддержку CORS
.cors()
.and()
// Включаем защиту от CSRF атак
.csrf()
// .disable();
.ignoringAntMatchers(PERMIT_ALL_MAPPING)
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
``````markdown
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Сохранение данных пользователей в памяти
auth.
authenticationProvider(daoAuthenticationProvider());
//.inMemoryAuthentication();
// .withUser("user")
// .password(passwordEncoder().encode("123456"))
// .authorities("ROLE_USER")
// .and()
// .withUser("admin")
// .password(passwordEncoder().encode("123456"))
// .authorities("ROLE_ADMIN")
// .and()
// .withUser("block")
// .password(passwordEncoder().encode("123456"))
// .authorities("ROLE_USER")
// .accountLocked(true);
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(new CustomUserDetailsService());
return provider;
}
Примечание: в данном контексте "CORS" и "CSRF" являются общими аббreviатурами и обычно остаются без изменения. Однако, если требуется полный перевод, можно использовать "поддержка CORS" и "защита от атак CSRF".
```Сначала рассмотрим проблему с CSRF: просмотрев множество руководств по Spring Security, можно заметить, что большинство из них отключает .csrf()
с помощью `.disable()`. Такое решение удобно, но не безопасно и противоречит целям использования безопасности. Поэтому, чтобы обеспечить безопасность, я предпочитаю включать эту функцию и учиться использовать XSRF-TOKEN.Этот проект является демонстрационным, поэтому он не использует базу данных. Вместо этого данные пользователей записываются непосредственно в память. Этот подход, как показано в закомментированном коде выше, хотя и прост, имеет некоторые недостатки. Установив точку останова, мы можем видеть, что этот метод вызывает `ProviderManager` из Spring Security, который не позволяет выбрасывать специфичные ошибки, такие как "пользователь не найден". Вместо этого всегда выбрасывается общее сообщение об ошибке "неправильные учетные данные".


Для обеспечения безопасности Spring Security объединяет все ошибки аутентификации в одну общую ошибку "Неправильные учетные данные". Чтобы выбрасывать более конкретные ошибки, такие как "Пользователь не существует", можно использовать методы, аналогичные тем, что используются в [проекте GitHub VHR](https://github.com/lenve/vhr/blob/41dcea34d3a220988e19c34ab88b2822d02c1be9/hrserver/src/main/java/org/sang/config/WebSecurityConfig.java#L58). Однако, поскольку этот проект использует JWT вместо стандартной схемы аутентификации, реализация подробных ошибок становится сложнее.
```Мое решение заключается в использовании класса `DaoAuthenticationProvider` из Spring Security в качестве провайдера аутентификации. Этот класс реализует интерфейс `AbstractUserDetailsAuthenticationProvider`, который позволяет подклассам переопределять поведение и работать с объектами типа `UserDetails`. Класс предназначен для обработки запросов аутентификации типа `UsernamePasswordAuthenticationToken`.С помощью конфигурации пользовательского класса для выполнения запросов к базе данных, мы можем выбросить ошибку "Пользователь не найден" непосредственно в `CustomUserDetailsService`. Установка параметра `hideUserNotFoundExceptions` в значение `false` позволит различать ошибки между неверным паролем и отсутствием пользователя.
Однако такой подход имеет недостаток — он не позволяет выбрасывать ошибки, связанные с заблокированным аккаунтом. Теоретически это можно было бы сделать путём наследования от абстрактного класса `AbstractUserDetailsAuthenticationProvider` и переопределением метода аутентификации, но это выглядит достаточно сложным, поэтому я решил не продолжать эту работу.
Кроме того, согласно правилам безопасности, лучше всего скрывать информацию об ошибках. Поэтому пока оставлю всё так, как есть.
#### Сервис поиска пользователей
```java
public class CustomUserDetailsService implements UserDetailsService {
private List<UserDetails> userList = new ArrayList<>();
public CustomUserDetailsService() {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserDetails user = User.withUsername("user")
.password(passwordEncoder.encode("123456"))
.authorities(WebSecurityConfig.USER)
.build();
UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.encode("123456"))
.authorities(WebSecurityConfig.ADMIN)
.build();
userList.add(user);
userList.add(admin);
}
}
``` @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
for (UserDetails userDetails : userList) {
if (userDetails.getUsername().equals(username)) {
// Here I tried to return the user directly
// However, in this case, only the first login after starting the service will be successful
// Upon subsequent authorization, an error "Empty encrypted password" occurs
// This solution allows avoiding this problem
// After the first password check, its value is cleared, which leads to an error during the next authorization
// Therefore, a new object or copy of the existing one is required
// This approach was found here: https://stackoverflow.com/questions/43007763/spring-security-encoded-password-gives-me-bad-credentials/43046195#43046195
return new User(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
}
}
throw new UsernameNotFoundException("Пользователь не найден, проверьте имя пользователя или зарегистрируйтесь!");
}
}
```Эта часть довольно проста, основные моменты я отметил в комментариях. Конечно, если вы используете подключение к базе данных, эта проблема отсутствует.
Интерфейс `UserDetailsService` предоставляет поддержку для других стратегий доступа к данным в Spring Security.
На этом завершена реализация базового Spring Security + JWT аутентификации сервера. Вы можете создать несколько контроллеров и протестировать функциональность с помощью Postman.
Другие части кода достаточно просты, поэтому вы можете самостоятельно реализовать необходимые вам функции, ориентируясь на исходный код.
### Настройка фронтенда
Способов создания проекта Vue в интернете множество, поэтому здесь мы не будем подробно останавливаться на этом вопросе. В прошлом после завершения установки Vue проекта в корневой директории проекта создавалась папка config для хранения конфигураций Vue, но теперь при создании нового проекта эта папка больше не создаётся автоматически. Вам потребуется самостоятельно создать файл vue.config.js в корневой директории проекта.
Дополнительно можно ознакомиться с официальной документацией Vue CLI:
* [Конфигурация Vue CLI](https://cli.vuejs.org/ru/config/)
* [Создание проекта с помощью Vue CLI](https://cli.vuejs.org/ru/guide/creating-a-project.html)
#### ЗависимостиДля передачи данных между клиентской и серверной частью используется более простой API `fetch`, хотя вы также можете выбрать более совместимый вариант `axios`.
Интерфейс пользователя реализован с использованием [ElementUI](https://element.eleme.io/).
Для получения значения `XSRF-TOKEN` используется библиотека `VueCookies`.
Для отображения информации на главной странице проекта был внедрен плагин [mavonEditor](https://github.com/hinesboy/mavonEditor), основанный на Vue и поддерживающий язык Markdown.
После установки указанных выше зависимостей вам следует модифицировать файл `main.js` в директории `src` следующим образом:
```javascript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import mavonEditor from 'mavon-editor';
import 'mavon-editor/dist/css/index.css';
import VueCookies from 'vue-cookies'
import axios from 'axios'
// Добавление cookies в запросах AJAX
axios.defaults.withCredentials = true;
// Регистрация axios как глобального объекта
Vue.prototype.$axios = axios
// Использование VueCookies
Vue.use(VueCookies)
Vue.config.productionTip = false
// Использование компонентов ElementUI
Vue.use(ElementUI)
// Использование плагина для редактирования markdown
Vue.use(mavonEditor)
// Адрес сервиса backend
Vue.prototype.SERVER_API_URL = "http://127.0.0.1:8088/api";
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
После создания файла vue.config.js
вам потребуется добавить следующий код для настройки CORS в вашем проекте Vue:```javascript
module.exports = {
// Options...
devServer: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8088',
changeOrigin: true,
ws: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
### Several Important Points
When performing operations such as POST, DELETE, PUT and others with the server-backend, it's important to include the request header `"X-XSRF-TOKEN": this.$cookies.get('XSRF-TOKEN')`. Without this header, even after authorization, the system will return a 403 error.
Also, don't forget to specify `credentials: "include"`, which is a mandatory condition for transmitting cookies. Without this line, cookie transmission will not be possible, leading to a lack of authentication.
```javascript
deleteItem(data) {
fetch(this.SERVER_API_URL + "/admin/data/" + data.id, {
headers: {
"Content-Type": "application/json; charset=UTF-8",
"X-XSRF-TOKEN": this.$cookies.get('XSRF-TOKEN')
},
method: "DELETE",
credentials: "include"
}).then(response => response.json())
.then(json => {
if (json.status === 200) {
this.systemDataList.splice(data.id, 1);
this.$message({
message: 'Successful deletion',
type: 'success'
});
} else {
window.console.log(json);
this.$message.error(json.message);
}
});
},
Spring Security Documentation Vue.js
Лицензия MIT.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )