Защита с помощта на JWT
JWT е компактен начин за прилагане на удостоверяване в модерни уеб приложения. За да го приложим, ще използваме библиотеката jjwt, която е JWT библиотека за Java и Android и се използва за създаване и анализиране на JWT. Трябва да добавим следните зависимости в pom.xml.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
Следващите стъпки демонстрират как да активирате JWT удостоверяване и упълномощаване в бекенда. За реализацията им са необходимо от предходното упражнение да сте предвидили:
· Клас със SecurityFilterChain метод за задаване на правила за упълномощаване;
· Сервизен клас с метод за вписване на потребителя;
· Клас, имплементиращ UserDetailsService и неговия метод loadUserByUsername();
· Контролер с точка за достъп за вписване на потребителя.
Създаване на JWT токен
1. Към application.properties добавете свойства, задаващи тайния ключ (secret) на токена и продължителността на неговата валидност в милисекунди. За криптиране на ключа можете да използвате инструмента: https://emn178.github.io/online-tools/sha256.html, а за изчисляване на продължителността на валидност на токена в милисекунди - https://www.convertworld.com/en/time/milliseconds.html.
jwt.secret=2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b
jwt.expiration-milliseconds=604800000
Създайте клас с метод за генериране на токен след успешно удостоверяване на потребителя.
@Component
public class JWTTokenProvider {
@Value("${jwt.secret}")
private String JWTSecret;
@Value("${jwt.expiration-milliseconds}")
private int JWTExpirationDate;
public String generateToken(Authentication authentication) {
String userName = authentication.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime()+JWTExpirationDate);
String token = Jwts.builder()
.setSubject(userName)
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(key())
.compact();
return token;
}
private Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(JWTSecret));
}
}
В персонализирана реализация на SecurityFilterChain допълнете правилата с изискване сесия да не бъде създавана или използвана от Spring Security.
http.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
В метода за вписване заменете добавянето на данните на вписания потребител към сесийния обект с логика по създаване на токен. Нека методът за връща създадения токен като текст.
public String login(LoginDto loginDto) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginDto.getUsername(), loginDto.getPassword()
));
SecurityContext sc = SecurityContextHolder.getContext();
sc.setAuthentication(authentication);
//Добавяне
String token = jwtTokenProvider.generateToken(authentication);
return token;
//Премахване
/* HttpSession session = req.getSession(true);
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, sc);
return "User logged-in successfully!";*/
}
Добавете клас, с помощта на който създаденият токен да бъде върнат като отговор на клиента.
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class JWTAuthResponse {
private String accessToken;
private String tokenType = "Bearer";
}
По отношение на точката за достъп за вписване на потребителя задайте отговорът да включва генерирания токен.
@PostMapping("/login")
public ResponseEntity<JWTAuthResponse> login(@RequestBody LoginDto loginDto) {
String token = authService.login(loginDto);
JWTAuthResponse jwtAuthResponse = new JWTAuthResponse();
jwtAuthResponse.setAccessToken(token);
return new ResponseEntity<>(jwtAuthResponse, HttpStatus.OK);
}
Изпробвайте създадената функционалност за създаване на токен c Postman.
Оторизация с JWT токен
1. Създайте клас, който имплементира интерфейса AuthenticationEntryPoint и неговия метод commence().
AuthenticationEntryPoint се използва за изпращане на HTTP отговор, който да изисква вписване от страна на клиент. Понякога клиентът проактивно включва идентификационни си данни (като потребителско име и парола), за да поиска достъп до ресурс. Тогава Spring Security не трябва да предоставя HTTP отговор, който изисква идентификационни данни от клиента, тъй като те вече са включени.
В други случаи обаче клиентът прави неупълномощена заявка към ресурс, до който не е оторизиран за достъп. В този случай се използва реализация на AuthenticationEntryPoint за изискване на идентификационни данни от клиента. Тя може да извърши пренасочване към страница за влизане, да отговори с хедър WWW-Authenticate или да предприеме друго действие. В нашия случай, тъй като нямаме налична страница за вписване, ще бъде хвърлено authException изключение.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
2. Допълнете класа JWTTokenProvider с методи за:
a. връщане на потребителско име от подаден токен;
b. валидиране на подаден токен;
@Component
public class JWTTokenProvider {
//Предходно съдържание
public String getUsername(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
return username;
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parse(token);
return true;
}
catch (MalformedJwtException ex) {
throw new ApiException(HttpStatus.BAD_REQUEST, "Invalid JWT token");
}
catch (ExpiredJwtException ex) {
throw new ApiException(HttpStatus.BAD_REQUEST, "Expired JWT token");
}
catch (UnsupportedJwtException ex) {
throw new ApiException(HttpStatus.BAD_REQUEST, "Unsupported JWT token");
}
catch (IllegalArgumentException ex) {
throw new ApiException(HttpStatus.BAD_REQUEST, "JWT string claims is empty");
}
}
}
Създайте потребителски филтър, който да използва създадения токен за удостоверяване на потребителя.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private JWTTokenProvider jwtTokenProvider;
private UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JWTTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) {
this.jwtTokenProvider = jwtTokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request,response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}j
Включете филтъра в SecurityFilterChain метода. Нека той да се изпълнява непосредствено преди UsernamePasswordAuthenticationFilter. Добавете и JwtAuthenticationEntryPoint като ресурс, отговарящ за обработка на изключения, свързани с оторизацията.
private JwtAuthenticationEntryPoint authenticationEntryPoint;
private JwtAuthenticationFilter authenticationFilter;
***
http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling(exception -> exception.authenticationEntryPoint (authenticationEntryPoint))
Изпробвайте функционалността с Postman. Полученият при вписването токен се добавя към заявката към последващ ресурс посредством опцията Bearer token.

Last updated
Was this helpful?