Лекция 13-14
Тема: Понятие сессии. Cookies. JWT-токены. Генерация токенов. Security Filter Chain. Разработка приложения с использованием jwt-токенов.
Материал по теме находится в стадии оформления
Добавляем в pom-файл поддержку jwt и jaxb
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
Создадим класс JwtUtils, добавим приватный ключ и методы для создания токена
@Service
public class JwiUtil {
private String PRIVATE_KEY = "670d4918c206e1bfec75bcaf637dc5b8";
private String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 8))
.signWith(SignatureAlgorithm.HS512, PRIVATE_KEY).compact();
}
}
Добавим метод для валидациия токена и сопутствующие ему методы
@Service
public class JwiUtil {
private String PRIVATE_KEY = "670d4918c206e1bfec75bcaf637dc5b8";
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 8))
.signWith(SignatureAlgorithm.HS512, PRIVATE_KEY).compact();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(PRIVATE_KEY).parseClaimsJws(token).getBody();
}
}
Создадим классы-обертки для входящего логина и пароля и для исходящего jwt
@Data
public class AuthRequest {
private String username;
private String password;
}
@Data
public class AuthResponse {
private final String jwt;
}
Создадим класс REST-контроллера, добавим метод для создания токена. Логика работы метода следующая:
Производится аутентификация по пришедшему логину и паролю;
Если аутентификация прошла успешно, то получаем UserDetails из БД по username;
Генерируем токен с помощью данных UserDetails;
Возвращаем токен как объект AuthResponse.
@org.springframework.web.bind.annotation.RestController
public class RestController {
@Autowired
private AuthenticationManager authManager;
@Autowired
private JwtUtil jwiUtil;
@Autowired
MyUserDetailsService userDetailsService;
@PostMapping(value = "/auth")
public ResponseEntity<?> createAuthToken(@RequestBody AuthRequest request) throws BadCredentialsException {
try {
authManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
} catch (BadCredentialsException ex) {
ex.printStackTrace();
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
final String jwt = jwiUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthResponse(jwt));
}
}
Настраиваем конфигурационный класс SecurityConfig. Добавляем bean для AuthenticationManager, а также доступ к URL. Для того, чтобы сервер не генерировал новую сессию, устанавливаем sessionCreationPolicy.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailsService userDetailsService;
@Autowired
JwtRequestFilter filter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// Авторизация
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/", "static/css", "static/js").permitAll()
.antMatchers("/auth").permitAll()
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/admin/**").hasRole("ADMIN")
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
В качестве клиента используем Postman.
Для начала попробуем сделать запрос с некорректным логином и паролем

На стороне сервера было выброшено исключение

Теперь попробуем ввести логин и пароль существующего пользователя. В ответ мы получим jwt, который будем использовать для последующих запросов.

Аутентификация с помощью jwt
Добавим в класс-контроллер endpoint, для доступа к которому требуется аутентификация. Входной аргумент типа Principal хранит информацию об аутентифицированном пользователе. Получаем username пользователя и извлекаем из БД информацию о нем.
@GetMapping(value = "/helloworld")
public String helloWorldRequest(Principal principal) {
final UserDetails userDetails = userDetailsService.loadUserByUsername(principal.getName());
return "<h1>Hello " + userDetails.getAuthorities().toString() + " </h1>";
}
Редактируем конфигурационный класс SecurityConfig, добавляем требование аутентификации для endpoint /helloworld.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/", "static/css", "static/js").permitAll()
.antMatchers("/auth").permitAll()
.antMatchers("/helloworld").authenticated()
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/admin/**").hasRole("ADMIN")
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Для того чтобы провести аутентификацию с помощью jwt, создадим отдельный фильтр, который потом встроим в filter chain, которая используется в Spring Security.
Логика работы нашего фильтра следующая:
Считываем заголовок GET-запроса с ключом "Authorization";
Проверяем, есть ли в начале заголовка слово "Bearer ";
Извлекаем из jwt значение username;
Извлекаем из БД пользователя по полученному username и осуществляем валидацию токена;
Если jwt провел валидацию, то аутентифицируем пользователя;
Передаем управление дальше по цепочке фильтров.
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
MyUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
final String authHeader = httpServletRequest.getHeader("Authorization");
String jwt = null;
String username = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
jwt = authHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(token);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
Отредактируем конфигурационный класс SecurityConfig, добавим использование фильтра в цепочке фильтров (мы указываем, что наш фильтр должен быть встроен в цепочку ДО фильтра UsernamePasswordAuthenticationFilter, который является стандартным фильтром для аутентификации).
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Авторизация
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/", "static/css", "static/js").permitAll()
.antMatchers("/auth").permitAll()
.antMatchers("/helloworld","/me").authenticated()
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/admin/**").hasRole("ADMIN")
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
Last updated
Was this helpful?