在 Spring Boot 微服务中使用 JWT Token 进行认证

Spring Boot 微服务需要对用户进行身份认证,其中一种方式是使用 JSON Web Token (JWT)。JWT 是一种开放标准(RFC 7519),它定义了一种紧凑的机制,用于在各方之间安全地传输信息。

本文将会带你了解如何在 Spring Boot 微服务项目中使用 JWT 进行身份认证。

JWT Token 概览

JWT 的体积相对较小。因此,它可以通过 URL 发送:

  • POST 参数
  • HTTP 请求头

通过 HTTP Header 发送 Token 是最常见的方式。

JWT Token 包含一个实体(可以是用户或服务)的所有必要信息。

下图显示了使用 JWT 进行身份认证的典型用例。

使用 JWT 进行身份认证的典型用例

示例应用

创建两个服务:

  • AuthenticatorService:负责验证用户名和密码。验证成功后,该服务会生成并返回一个 JWT Token。
  • BlogService:受保护的服务。该服务包含一个 Filter,用于验证客户端发送的 JWT Token。验证成功后,该服务会返回业务数据。

下图显示了客户端与上述服务之间的交互。

认证服务和博客服务之间的交互

AuthenticatorService

Maven 依赖

添加 jjwt 依赖,用于生成 JWT Token。

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<!-- 其他依赖忽略 -->

实体

AuthenticatorService 包含一个 User 实体,用于表示用户凭证。如下:

package com.stackroute.AuthenticatorService.model;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="users")
public class User {

@Id
private String userName;
private String password;

public User() {
}

public User(String userName, String password) {
    this.userName = userName;
    this.password = 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;
}
}

JwtGeneratorInterface

然后是 JwtGeneratorInterface。该接口包含一个生 generateToken() 方法,该方法接受一个 User 对象。

package com.stackroute.AuthenticatorService.config;

import com.stackroute.AuthenticatorService.model.User;
import java.util.Map;

public interface JwtGeneratorInterface {

Map<String, String> generateToken(User user);
}

JwtGeneratorInterface 的实现 JwtGeneratorImpl 如下:

package com.stackroute.AuthenticatorService.config;

import com.stackroute.AuthenticatorService.model.User;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import io.jsonwebtoken.Jwts;

@Service
public class JwtGeneratorImpl implements JwtGeneratorInterface{

  @Value("${jwt.secret}")
  private String secret;

  @Value("${app.jwttoken.message}")
  private String message;

  @Override
  public Map<String, String> generateToken(User user) {
    String jwtToken="";
    jwtToken = Jwts.builder().setSubject(user.getUserName()).setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, "secret").compact();
    Map<String, String> jwtTokenGen = new HashMap<>();
    jwtTokenGen.put("token", jwtToken);
    jwtTokenGen.put("message", message);
    return jwtTokenGen;
  }
}

Repository

使用 Spring Data JPA 的 Repository 检索数据。

创建 UserRepository,实现 JpaRepository,如下:

package com.stackroute.AuthenticatorService.repository;

import com.stackroute.AuthenticatorService.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, String> {

public User findByUserNameAndPassword(String userName, String password);
}

Service

本模块的 Service 接口是 UserService。该接口声明了两个方法:saveUser() 用于在数据库中存储 User 对象。第二个方法是 getUserByNameAndPassword(),用于通过指定的用户名和密码检索 User

UserService 接口如下:

package com.stackroute.AuthenticatorService.service;

import com.stackroute.AuthenticatorService.exception.UserNotFoundException;
import com.stackroute.AuthenticatorService.model.User;
import org.springframework.stereotype.Service;

@Service
public interface UserService {
    public void saveUser(User user);
    public User getUserByNameAndPassword(String name, String password) throws UserNotFoundException;
}

UserService 的实现类是 UserServiceImpl。如下:

package com.stackroute.AuthenticatorService.service;

import com.stackroute.AuthenticatorService.exception.UserNotFoundException;
import com.stackroute.AuthenticatorService.model.User;
import com.stackroute.AuthenticatorService.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

  private UserRepository userRepository;

  @Autowired
  public UserServiceImpl(UserRepository userRepository){
    this.userRepository=userRepository;
  }
  @Override
  public void saveUser(User user) {
    userRepository.save(user);
  }

  @Override
  public User getUserByNameAndPassword(String name, String password) throws UserNotFoundException {
    User user = userRepository.findByUserNameAndPassword(name, password);
    if(user == null){
       throw new UserNotFoundException("Invalid id and password");
    }
    return user;
  }
}

Controller

Controller 有两个端点:/register/login。第一个端点负责保存新用户。后一个端点负责对用户进行身份认证。认证成功后,后一个端点会返回一个 JWT Token。

Controller 如下。

UserController

package com.stackroute.AuthenticatorService.controller;

import com.stackroute.AuthenticatorService.config.JwtGeneratorImpl;
import com.stackroute.AuthenticatorService.config.JwtGeneratorInterface;
import com.stackroute.AuthenticatorService.exception.UserNotFoundException;
import com.stackroute.AuthenticatorService.model.User;
import com.stackroute.AuthenticatorService.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api/v1/user")
public class UserController {
private UserService userService;
private JwtGeneratorInterface jwtGenerator;

  @Autowired
  public UserController(UserService userService, JwtGeneratorInterface jwtGenerator){
    this.userService=userService;
    this.jwtGenerator=jwtGenerator;
  }

  @PostMapping("/register")
  public ResponseEntity<?> postUser(@RequestBody User user){
  try{
     userService.saveUser(user);
     return new ResponseEntity<>(user, HttpStatus.CREATED);
   } catch (Exception e){
     return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT);
   }
  }

  @PostMapping("/login")
  public ResponseEntity<?> loginUser(@RequestBody User user) {
    try {
      if(user.getUserName() == null || user.getPassword() == null) {
      throw new UserNotFoundException("UserName or Password is Empty");
    }
    User userData = userService.getUserByNameAndPassword(user.getUserName(), user.getPassword());
    if(userData == null){
       throw new UserNotFoundException("UserName or Password is Invalid");
    }
       return new ResponseEntity<>(jwtGenerator.generateToken(user), HttpStatus.OK);
    } catch (UserNotFoundException e) {
       return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT);
    }
  }
}

在 BlogService 中验证 Token

BlogService 服务通过 JWT 进行保护。这是一个简单的服务,包含以下组件:

  • Controller:暴露端点
  • Configuration:注册 Filter
  • Filter 是用于认证 Token 的组件

同样,需要在 pom.xml 中添加 jjwt 依赖:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

Controller

Controller 有两个端点。第一个是不受限制的端点,只需返回一条信息。第二个是受 JWT 限制的端点。

BlogControlleer

package guru.springframework.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("api/v1/blog")
public class BlogController {

    @GetMapping("/unrestricted")
        public ResponseEntity<?> getMessage() {
        return new ResponseEntity<>("Hai this is a normal message..", HttpStatus.OK);
    }

    @GetMapping("/restricted")
    public ResponseEntity<?> getRestrictedMessage() {
        return new ResponseEntity<>("This is a restricted message", HttpStatus.OK);
    }
}

Configuration

Configuration 类负责注册 Authentication Filter,用于认证。

配置类 FilterConfig 如下:

package guru.springframework.config;

import guru.springframework.filter.JwtFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean jwtFilter() {
        FilterRegistrationBean filter= new FilterRegistrationBean();
        filter.setFilter(new JwtFilter());

        // 提供需要限制的端点。
        // 如果未指定,所有端点都将受到限制。
       filter.addUrlPatterns("/api/v1/blog/restricted");
    return filter;
    }
}

在 Filter 中测试 JWT Token

Filter 负责验证 JWT Token。Filter 类继承了 GenericFilter 类,并覆写了 doFiter() 方法。

该方法的输入参数如下:

  • ServletRequest
  • ServletResponse
  • FilterChain

JwtFilter 代码如下:

package guru.springframework.filter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
           final HttpServletRequest request = (HttpServletRequest) servletRequest;
           final HttpServletResponse response = (HttpServletResponse) servletResponse;
           final String authHeader = request.getHeader("authorization");
           if ("OPTIONS".equals(request.getMethod())) {
               response.setStatus(HttpServletResponse.SC_OK);
               filterChain.doFilter(request, response);
           } else {
               if(authHeader == null || !authHeader.startsWith("Bearer ")){
                   throw new ServletException("An exception occurred");
               }  
           }
           final String token = authHeader.substring(7);
           Claims claims = Jwts.parser().setSigningKey("secret").parseClaimsJws(token).getBody();
           request.setAttribute("claims", claims);
           request.setAttribute("blog", servletRequest.getParameter("id"));
           filterChain.doFilter(request, response);
    }
}

用 JWT Token 测试应用

测试应用:

  1. 启动 POSTMAN 或 REST 客户端访问 AuthenticarService

  2. login 端点发起 POST 请求,在请求体中使用 Alicepass123 作为凭证。注意 Authenticator 服务返回的 JSON Web Token。

    使用 POST 发起请求,获取 Token

  3. 接下来,启动 BlogService 并向受保护的端点发起 GET 请求。并在 Authorization Header 中添加 Token。

Authorization Header 的值必须是 Bearer,中间用空格隔开,后面是 Token。发送请求后,你就能获取到业务数据了。

总结

我看到很多开发人员在他们的服务中验证 JWT 标记。微服务有一种称为网关卸载(Gateway Offloading)的模式。这种模式能让每个微服务将共享服务功能(如通过 SSL 证书、Token 进行验证)卸载到 API 网关。

此外,微服务网关往往会成为单点故障。然而,随着技术的快速发展和向云计算的迁移。有硬件负载均衡器、软件负载均衡器和云负载均衡器。所有这些都有冗余和各种故障转移方案,以防止单点故障。


Ref:https://springframework.guru/jwt-authentication-in-spring-microservices-jwt-token/