Полное руководство по аутентификации с помощью токена 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 обучение в телеграме

+1
0
+1
3
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *