В этом посте я покажу, как защитить ваш REST API на основе spring boot. В последнее время стало более распространенной тенденцией защищать API REST, чтобы избежать ненужных вызовов общедоступных API. Мы будем использовать некоторые функции загрузки Spring для обеспечения безопасности Spring вместе с JSONWebTokens для авторизации.
Поток пользователей в этом случае составляет
- Пользователь входит в систему
- Мы проверяем учетные данные пользователя
- Маркер отправляется обратно агенту пользователя.
- Пользователь пытается получить доступ к защищенному ресурсу.
- Пользователь отправляет JWT при доступе к защищенному ресурсу. Мы подтверждаем JWT.
- Если JWT действителен, мы разрешаем пользователю получить доступ к ресурсу.
JSONWebTokens, известные как JWTS, используются для формирования авторизации пользователей. Это помогает нам создавать безопасные API, а также легко масштабируется. Во время аутентификации возвращается веб-токен JSON. Всякий раз, когда пользователь хочет получить доступ к защищенному ресурсу, браузер должен отправлять JWTS в заголовке авторизации вместе с запросом. Здесь следует понять одну вещь: защита REST API является хорошей практикой безопасности.
В принципе, мы покажем
- Проверка веб-токена JSON
- Подтвердите подпись
- Проверьте права доступа клиента
Что вам понадобится?
- Java 8,
- База данных MySQL
- Редактор IntelliJ
- Градация
Примечание – Это будет не полноценное приложение, а API-интерфейсы REST, основанные на Spring boot, Spring security.
API REST на основе весенней загрузки
Я буду защищать REST API для компании, которую я создал в этом сообщении в блоге REST API . Этот API также включает кэширование. Пользователь попытается получить доступ к /cachedemo/v1/компаниям/
и поскольку API защищены, он получит ответ, подобный приведенному ниже:
Теперь мы расскажем, как защитить этот API и как получить к нему доступ, когда он защищен.
Добавление пользователя и регистрация пользователя
Поскольку мы хотим добавить авторизацию для API, нам понадобится место, где пользователь сможет войти в систему и отправить учетные данные. Эти учетные данные будут проверены, и будет сгенерирован токен. Затем этот токен будет передан в запросе на вызов API. Маркер будет проверен в фильтре авторизации Spring security, который мы добавим. При наличии действительного токена пользователь сможет получить доступ к API.
Создайте пользовательскую модель
package com.betterjavacode.models; import javax.persistence.*; import java.io.Serializable; @Entity(name = "User") @Table(name = "user") public class User implements Serializable { public User() { } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "username") private String username; @Column(name = "password") private String password; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
Мы добавим контроллер, где пользователь может зарегистрироваться со своими данными для имени пользователя
и пароль
.
package com.betterjavacode.resources; import com.betterjavacode.models.User; import com.betterjavacode.repositories.UserRepository; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value = "/cachedemo/v1/users") public class UserController { private UserRepository userRepository; private BCryptPasswordEncoder bCryptPasswordEncoder; public UserController(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) { this.userRepository = userRepository; this.bCryptPasswordEncoder = bCryptPasswordEncoder; } @PostMapping("/signup") public void signUp(@RequestBody User user) { user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); userRepository.save(user); } }
Теперь, когда мы ОТПРАВЛЯЕМ запрос в /cachedemo/v1/пользователи/регистрация
, пользователь будет сохранен в базе данных. Пароль
для пользователя будет сохранен в зашифрованном формате, также используется BCryptPasswordEncoder
. Мы покажем, как пользователь может войти в систему, чтобы создать токен.
Вход пользователя
Для обработки входа пользователя в систему мы добавим фильтр аутентификации , который будет добавлен в цепочку фильтров, и Spring boot соответствующим образом обработает его выполнение. Этот фильтр будет выглядеть так, как показано ниже:
package com.betterjavacode.SpringAppCache; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Date; public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; public AuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; setFilterProcessesUrl("/login"); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { com.betterjavacode.models.User creds = new ObjectMapper().readValue(request.getInputStream(), com.betterjavacode .models.User.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(), creds.getPassword(),new ArrayList<>())); } catch(IOException e) { throw new RuntimeException("Could not read request" + e); } } protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, Authentication authentication) { String token = Jwts.builder() .setSubject(((User) authentication.getPrincipal()).getUsername()) .setExpiration(new Date(System.currentTimeMillis() + 864_000_000)) .signWith(SignatureAlgorithm.HS512, "SecretKeyToGenJWTs".getBytes()) .compact(); response.addHeader("Authorization","Bearer " + token); } }
По сути, пользователь отправит учетные данные в запросе на URL, заканчивающийся на/login . Этот фильтр поможет аутентифицировать пользователя, при успешной аутентификации в заголовок ответа будет добавлен токен с ключом авторизации.
Проверка и авторизация токенов
Мы добавляем еще один фильтр авторизации фильтра для проверки токена, который мы ранее пропустили через фильтр аутентификации. Этот фильтр будет выглядеть так, как показано ниже:
package com.betterjavacode.SpringAppCache; import io.jsonwebtoken.Jwts; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; public class AuthorizationFilter extends BasicAuthenticationFilter { public AuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { String header = request.getHeader("Authorization"); if(header == null || !header.startsWith("Bearer")) { filterChain.doFilter(request,response); return; } UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request,response); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String token = request.getHeader("Authorization"); if(token != null) { String user = Jwts.parser().setSigningKey("SecretKeyToGenJWTs".getBytes()) .parseClaimsJws(token.replace("Bearer","")) .getBody() .getSubject(); if(user != null) { return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>()); } return null; } return null; } }
Если проверка токена прошла успешно, пользователь возвращается и назначается контексту безопасности.
Чтобы включить Spring security, мы добавим новый класс WebSecurityConfiguration с аннотацией @EnableWebSecurity
. Этот класс расширит стандарт WebSecurityConfigurerAdapter
. В этом классе мы ограничим наши API, а также добавим некоторые URL-адреса из белого списка, к которым нам потребуется доступ без какого-либо маркера авторизации. Это будет выглядеть так, как показано ниже:
package com.betterjavacode.SpringAppCache; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @EnableWebSecurity public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { private BCryptPasswordEncoder bCryptPasswordEncoder; private UserDetailsService userDetailsService; private static final String[] AUTH_WHITELIST = { "/v2/api-docs", "/swagger-resources", "/swagger-resources/**", "/configuration/ui", "/configuration/security", "/swagger-ui.html", "/webjars/**" }; public WebSecurityConfiguration(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) { this.bCryptPasswordEncoder = bCryptPasswordEncoder; this.userDetailsService = userDetailsService; } protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.cors().and().csrf().disable().authorizeRequests() .antMatchers(AUTH_WHITELIST).permitAll() .antMatchers(HttpMethod.POST, "/cachedemo/v1/users/signup").permitAll() .anyRequest().authenticated() .and().addFilter(new AuthenticationFilter(authenticationManager())) .addFilter(new AuthorizationFilter(authenticationManager())) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder); } @Bean CorsConfigurationSource corsConfigurationSource() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues()); return source; } }
В настройке метода мы ограничили большинство API, разрешив только URL-адреса Swagger и URL-адрес регистрации. Мы также добавляем фильтры для защиты Http. Мы добавим наш собственный класс UserDetailsServiceImpl
для проверки учетных данных пользователя.
package com.betterjavacode.services; import com.betterjavacode.models.User; import com.betterjavacode.repositories.UserRepository; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import java.util.Collections; @Component public class UserDetailsServiceImpl implements UserDetailsService { private UserRepository userRepository; public UserDetailsServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user == null) { throw new UsernameNotFoundException(username); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), Collections.emptyList()); } }
Демонстрация
Со всеми изменениями кода теперь мы готовы создать пользователя, войти в систему и получить доступ к защищенным API REST. На изображении выше пользователь получает ошибку “Отказано в доступе” для доступа к защищенным API. Чтобы продемонстрировать это, я уже зарегистрировал пользователя с именем пользователя test1
и паролем test@123.
Запрос POST для входа в систему выдаст нам в ответ токен авторизации. Теперь мы используем этот токен в нашем запросе GET для получения данных о компаниях. Этот запрос на получение будет выглядеть следующим образом:
Таким образом, мы показали, как защитить REST API с помощью веб-токена JSON.
Оригинальное сообщение в блоге было опубликовано в моем блоге лучший java-код
Оригинал: “https://dev.to/betterjavacode/json-web-token-how-to-secure-spring-boot-rest-api-3cg2”