Полное руководство по аутентификации с помощью токена Jwt на Java
В данной статье мы поговорим о том, как реализовать аутентификацию с помощью токена Jwt на Java.
Предварительные условия:
Базовые знания о том, как взаимодействует клиент-сервер, цикл запрос-ответ.
В этой статье я собираюсь предоставить руководство по работе с Authentication Manager на Java.
Итак, первый вопрос, который может возникнуть в процессе, заключается в том,
что такое Authentication Manager?
Authentication Manager является фундаментальной основой для процесса аутентификации Spring security. AuthenticationManager
– это API, который определяет, как должны работать фильтры Spring Security.
Относительные зависимости, которые нам нужны для интеграции аутентификации с Spring boot:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
Структура папок для будет выглядеть следующим образом :
controller – Предоставляет обработчики для любого входящего запроса.
RequestFormat – Спецификаторы формата поля данных для входа в систему, запроса на регистрацию.
ResponseFormat – Ответы на запросы аутентификации на стороне сервера.
Security
Models
Repository
SecurityServices
Давайте создадим файл User.java в Models:
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "auser")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(unique = true)
private String username;
@Column(unique = true)
private String email;
private String password;
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = 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 getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public User() {
super();
}
}
Давайте сопоставим созданный выше класс модели с репозиторием для работы с JpaRepository (JPA= Java Persistence API ) :
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.artisticlubfab.AuthMS.models.User;
public interface UserRepository
extends JpaRepository<User, String> {
Optional<User> findByUsername(String username);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
}
Теперь мы создадим 2 файла UserDetailsServiceImpl.java и UserDetailsService.java, которые будут выполнять все операции, связанные с пользователем.
/**
Important points : UserDetailsService comes from spring security core library
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(()
-> new UsernameNotFoundException
("user Not Found with username: " + username));
return UserDetailsImpl.build(user);
}
}
//UserDetailsImpl.java
/**
The above class while returning the built UserDetails Object uses
the below referred code
*/
public class UserDetailsImpl implements UserDetails {
/**
this class uses UserDetails Interface which is available in spring
security core library
*/
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
@JsonIgnore
private String password;
public UserDetailsImpl(Long id, String username,
String email, String password) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
}
public static UserDetailsImpl build(User user) {
return new UserDetailsImpl(user.getId(),
user.getUsername(), user.getEmail(),
user.getPassword());
}
public Long getId() {
return id;
}
public String getEmail() {
return email;
}
@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;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
UserDetailsImpl user = (UserDetailsImpl) o;
return Objects.equals(id, user.id);
}
@Override
public Collection<? extends GrantedAuthority>
getAuthorities() {
return null;
}
}
//Jwt Specific Security Operations :
/**
AuthTokenFilter.java, AuthEntryPointJwt.java and JwtUtils.java
These all 3 files will manage token management and request filters
for the application cycle .
Create jwt folder inside SecurityServices and add above files with the code
below.
*/
// AuthTokenFilter.java
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}
return null;
}
}
--------------------------------------------------------------
//AuthEntryPointJwt.java
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
---------------------------------------------------
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${acf.app.jwtExpirationMs}")
private int jwtExpirationMs;
@Value("${acf.app.jwtSecret}")
private String jwtSecret;
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
public String generateJwtToken(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return Jwts.builder().setSubject((userPrincipal.getUsername())).setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret).compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
}
-------------------------------------------------------------------------
//SecurityConfig.java inside SecurityServices package.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Autowired
UserDetailsServiceImpl userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {
http.cors().and().csrf().disable().exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy
(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/api/auth/**")
.permitAll()
.antMatchers("/api/test/**")
.permitAll().anyRequest().authenticated();
http.addFilterBefore(authenticationJwtTokenFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
@Bean
public AuthenticationManager authenticationManager
(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration
.getAuthenticationManager();
}
}
Теперь, когда мы уже готовы с основной функциональной частью, мы можем перейти к работе с Controller и частями Request, Response Format.
Для целей обучения мы создадим 2 отдельных Controller-файла:
1. AuthController.java
2. UserController.java
Конечные точки, которые мы будем предоставлять в AuthController
, не должны быть отключены. Но для того, чтобы UserController
выполнял любой запрос и извлекал данные, он должен запрашивать учётные данные для аутентификации.
Итак, если вы внимательно следили, мы определили свойства маршрутизации в файле SecurityConfig
. В нём указано, что конечная точка всегда должна быть разрешена, поскольку для входа в приложение пользователь должен пройти процесс аутентификации, верно? Таким образом, шаблон (/api/auth/**) должен быть разрешён для любого суффиксного дерева. Но для api / test, который специфичен для UserController, он всегда должен быть аутентифицирован, поскольку это тот файл, в котором мы будем пытаться извлечь или обновить любые данные, относящиеся к пользователю.
.authorizeRequests()
.antMatchers("/api/auth/**")
.permitAll()
.antMatchers("/api/test/**")
.permitAll().anyRequest().authenticated();
Теперь давайте подготовим Request ,Response formats.
Создайте 2 файла внутри пакета RequestFormat:
1. LoginRequest.java
2. SignUpRequest.java
В этих файлах будет указано, что мы на самом деле ожидаем от пользователя в качестве входных данных, которые будут работать для их аутентификации и позволят им быстро войти в приложение.
//LoginRequest.java
public class LoginRequest {
private String username;
private String password;
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;
}
}
---------------------------------------------------------------------------
//SignUpRequest.java
public class SignupRequest {
private String username;
private String email;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Таким же образом у нас будет DTO и для Response Formats. Создайте эти файлы внутри ResponseFormats:
//JwtReponse.java
/**
this file will return us the operated user object and the token associated
with it after successful login attempt.
*/
public class JwtResponse {
private String token;
private String type = "Bearer";
private Long id;
private String username;
private String email;
public JwtResponse(String accessToken,
Long id, String username, String email) {
this.token = accessToken;
this.id = id;
this.username = username;
this.email = email;
}
// ADD GETTERS AND SETTERS HERE
}
-------------------------------------------------------------------------------
//MessageReponse.java
public class MessageResponse {
private String message;
public MessageResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
AuthController.java
Этот файл будет обрабатывать все конечные точки api тестирования, которые выполняют операции, специфичные для пользователя. Он использует JwtUtils и PasswordEncoder. PasswordEncoder помогает нам хранить пароль, закодированный с помощью стандартизированного механизма. @ CrossOrigin указывает совместное использование ресурсов из разных источников и в нём указано, что любой входящий запрос из любого источника не должен блокироваться.
UserController.Java
Здесь мы не выполняем никаких специальных операций. Мы просто хотим ограничить маршрут, чтобы он был доступен только в том случае, если пользователь успешно прошёл аутентификацию
//UserController.java
@CrossOrigin(origins = "*", maxAge = 4800)
@RestController
@RequestMapping("/api/test")
public class UserController {
@GetMapping("/all")
public MessageResponse allAccess() {
return new MessageResponse("Server is up.....");
}
@GetMapping("/greeting")
@PreAuthorize("isAuthenticated()")
public MessageResponse userAccess() {
return new MessageResponse
("Congratulations! You are an authenticated user.");
}
}
----------------------------------------------------------------------------
//AuthController.java
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@PostMapping("/signin")
public ResponseEntity<?> authenticateuser
(@RequestBody LoginRequest loginRequest) {
org.springframework.security.core.Authentication authentication = authenticationManager.authenticate
(new UsernamePasswordAuthenticationToken
(loginRequest.getUsername(),
loginRequest.getPassword()));
SecurityContextHolder.getContext()
.setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl)
authentication.getPrincipal();
return ResponseEntity
.ok(new JwtResponse(jwt, userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail()));
}
@PostMapping("/signup")
public ResponseEntity<?> registerUser
(@RequestBody SignupRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest
.getUsername())) {
return ResponseEntity.badRequest()
.body(new MessageResponse
("Error: username is already taken!"));
}
if (userRepository.existsByEmail
(signUpRequest.getEmail())) {
return ResponseEntity.badRequest()
.body(new MessageResponse
("Error: Email is already in use!"));
}
// Create new user account
User user = new User(signUpRequest.getUsername(),
signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));
userRepository.save(user);
return ResponseEntity
.ok(new MessageResponse("user registered successfully!"));
}
}
Итак, всё!
Теперь вы готовы протестировать данную технологию! Если вы всё будете делать шаг за шагом, у вас должен получиться ожидаемый результат!
Java обучение в телеграме