Basi c авторизация:
(я предполагаю, что вы знаете, как создавать конечные точки, и у вас есть базовые знания о создании как простого приложения Spring Boot, так и приложения реагирования, поэтому я буду придерживайтесь только авторизации topi c.)
При авторизации basi c ваше веб-приложение должно отправлять учетные данные пользователя при каждом вызове API. И мы должны принять во внимание, что ваш бэкэнд, вероятно, открыт на localhost:8080
и во внешнем интерфейсе localhost:3000
, поэтому нам приходится иметь дело с CORS. (подробнее о CORS Обмен ресурсами между источниками (CORS) и CORS в Spring Security Spring Security CORS )
Начнем с настройки безопасности, в которой мы видим конечные точки.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// by default uses a Bean by the name of corsConfigurationSource
.cors(withDefaults())
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").authenticated()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers(HttpMethod.GET, "/cars").authenticated()
.anyRequest().authenticated()
.and()
.httpBasic();
}
//and cors configuration
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowedMethods(Arrays.asList("GET","POST", "OPTIONS"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
У нас есть конечные точки /login
и /cars
, которые требуют аутентификации. Если вы запустите бэкэнд-приложение и откроете браузер на localhost:8080/login
(или /cars
не имеет значения), то в середине экрана появится окно с базовой авторизацией c. Имя пользователя по умолчанию в Spring Security - user
, и пароль генерируется в вашей консоли. Скопируйте и вставьте пароль.
Теперь go в приложение переднего плана. Предположим, что у нас есть простое приложение с двумя полями: имя пользователя и пароль и кнопка: логин. Теперь нам нужно реализовать logi c.
...
basicAuthorize = () => {
let username = this.state.username;
let password = this.state.password;
fetch("http://localhost:8080/login", {
headers: {
"Authorization": 'Basic ' + window.btoa(username + ":" + password)
}
}).then(resp => {
console.log(resp);
if (resp.ok) {
this.setState({
isLoginSucces: true});
} else {
this.setState({isLoginSucces: false});
}
return resp.text();
});
}
...
Идя сверху, мы имеем:
- Учетные данные пользователя
- Заголовок для авторизации в соответствии с basi c authorization spe c on MDN web docks Авторизационный заголовок
- Если ответ
ok
, мы можем где-то хранить учетные данные пользователя, и при следующих вызовах API мы должны снова включить заголовок авторизации. (но мы не должны хранить конфиденциальные данные пользователя, например, LocalStorage
или SessionStorage
для производства, но для разработки все в порядке Хранение учетных данных в локальном хранилище )
JWT:
Что такое JWT, вы можете прочитать на этом сайте Jwt.io . Вы также можете отлаживать токены, что полезно при попрошайничестве.
Создание конечной точки аутентификации и логики c.
JWT довольно сложно реализовать, поэтому полезно создать некоторые классы, которые помогут реализовать это.
Как и здесь, самое важное:
- JwtTokenRequest tokenRequest - это POJO с
username
и password
, просто чтобы получить его из логина с внешнего интерфейса и отправить его дальше. - JwtTokenResponse, также POJO, это просто единственная строка токена, которая отправляется в cook ie
- Я также получаю TimeZone для установки срока действия токена.
@PostMapping("/authenticate")
public ResponseEntity<String> createJwtAuthenticationToken(@RequestBody JwtTokenRequest tokenRequest, HttpServletRequest request, HttpServletResponse response, TimeZone timeZone)
{
try
{
JwtTokenResponse accessToken = authenticationService.authenticate(tokenRequest, String.valueOf(request.getRequestURL()), timeZone);
HttpCookie accessTokenCookie = createCookieWithToken("accessToken", accessToken.getToken(), 10 * 60);
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()).body("Authenticated");
}
catch (AuthenticationException e)
{
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
}
//creating cookie
private HttpCookie createCookieWithToken(String name, String token, int maxAge)
{
return ResponseCookie.from(name, token)
.httpOnly(true)
.maxAge(maxAge)
.path("/")
.build();
}
Служба, отвечающая за аутентификацию и создание токена
@Service
public class JwtAuthenticationService
{
private AuthenticationManager authenticationManager;
private final String SECRET_KEY = "SecretKey";
public JwtAuthenticationService(AuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
}
public JwtTokenResponse authenticate(JwtTokenRequest tokenRequest, String url, TimeZone timeZone) throws AuthenticationException
{
UserDetails userDetails = managerAuthentication(tokenRequest.getUsername(), tokenRequest.getPassword());
String token = generateToken(userDetails.getUsername(), url, timeZone);
return new JwtTokenResponse(token);
}
Управление аутентификацией. Вам не нужно проверять, относится ли пароль к имени пользователя вручную, потому что если у вас реализован loadByUsername
, Spring будет использовать этот метод для загрузки пользователя и проверки пароля. Проверка подлинности пользователя вручную с помощью Spring Security
private UserDetails managerAuthentication(String username, String password) throws AuthenticationException
{
Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
return (UserDetails) authenticate.getPrincipal();
}
Если не выдано исключение, это означает, что учетные данные пользователя верны, тогда мы можем сгенерировать токен JWT.
В этом примере I Я использую библиотеку Java JWT , которую можно добавить в файл pom.xml
.
Этот метод генерирует токен в соответствии с часовым поясом из запроса, а также сохраняет URL-адрес запроса информации.
private String generateToken(String username, String url, TimeZone timeZone)
{
try
{
Instant now = Instant.now();
ZonedDateTime zonedDateTimeNow = ZonedDateTime.ofInstant(now, ZoneId.of(timeZone.getID()));
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
String token = JWT.create()
.withIssuer(url)
.withSubject(username)
.withIssuedAt(Date.from(zonedDateTimeNow.toInstant()))
.withExpiresAt(Date.from(zonedDateTimeNow.plusMinutes(10).toInstant()))
.sign(algorithm);
return token;
}
catch (JWTCreationException e)
{
e.printStackTrace();
throw new JWTCreationException("Exception creating token", e);
}
}
Если все было в порядке, то токен хранится в http-cook ie.
Если у нас есть токен, то, если запрос сделан к аутентифицированной конечной точке, мы должны отфильтровать этот запрос раньше. Нам нужно добавить наш пользовательский фильтр:
public class JwtFilter extends OncePerRequestFilter
{
private final String SECRET_KEY = "SecretKey";
}
//or load from other source
public class JwtFilter extends OncePerRequestFilter
{
private final String SECRET_KEY = ApplicationConstants.SECRET_KEY;
}
- Реализация метода из родительского класса
- Зависит от того, откуда вы получаете токены, нам просто нужно его загрузить. В этом примере я использую HttpOnly cook ie
- Если присутствует повар ie, тогда выполните авторизацию
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException
{
Cookie tokenCookie = null;
if (request.getCookies() != null)
{
for (Cookie cookie : request.getCookies())
{
if (cookie.getName().equals("accessToken"))
{
tokenCookie = cookie;
break;
}
}
}
if (tokenCookie != null)
{
cookieAuthentication(tokenCookie);
}
chain.doFilter(request, response);
}
- Если вся проверка прошла, то установите в SecurityContextHolder, что этот пользователь аутентифицирован. Что такое SecurityContextHolder, вы можете прочитать здесь 10.1. SecurityContextHolder
private void cookieAuthentication(Cookie cookie)
{
UsernamePasswordAuthenticationToken auth = getTokenAuthentication(cookie.getValue());
SecurityContextHolder.getContext().setAuthentication(auth);
}
private UsernamePasswordAuthenticationToken getTokenAuthentication(String token)
{
DecodedJWT decodedJWT = decodeAndVerifyJwt(token);
String subject = decodedJWT.getSubject();
Set<SimpleGrantedAuthority> simpleGrantedAuthority = Collections.singleton(new SimpleGrantedAuthority("USER"));
return new UsernamePasswordAuthenticationToken(subject, null, simpleGrantedAuthority);
}
private DecodedJWT decodeAndVerifyJwt(String token)
{
DecodedJWT decodedJWT = null;
try
{
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET_KEY))
.build();
decodedJWT = verifier.verify(token);
} catch (JWTVerificationException e)
{
//Invalid signature/token expired
}
return decodedJWT;
}
А теперь запрос фильтруется токеном в cook ie. Мы должны добавить пользовательский фильтр в Spring Security:
@Override
protected void configure(HttpSecurity http) throws Exception
{
...
//now 'session' is managed by JWT http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class);
}
Во внешнем интерфейсе у вас мало работы.
В вашем запросе вам просто нужно добавить withCredentials: 'include'
, тогда куки будут отправлены с просьбой. Вы должны использовать 'include'
, потому что это перекрестный запрос. Request.credentials
Пример запроса:
fetch('http://localhost:8080/only-already-authenticated-users', {
method: "GET",
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
})