Spring Boot 微服务需要对用户进行身份认证,其中一种方式是使用 JSON Web Token (JWT)。JWT 是一种开放标准(RFC 7519),它定义了一种紧凑的机制,用于在各方之间安全地传输信息。
本文将会带你了解如何在 Spring Boot 微服务项目中使用 JWT 进行身份认证。
JWT Token 概览 {#jwt-token-概览}
JWT 的体积相对较小。因此,它可以通过 URL 发送:
- POST 参数
- HTTP 请求头
通过 HTTP Header 发送 Token 是最常见的方式。
JWT Token 包含一个实体(可以是用户或服务)的所有必要信息。
下图显示了使用 JWT 进行身份认证的典型用例。
示例应用 {#示例应用}
创建两个服务:
- AuthenticatorService:负责验证用户名和密码。验证成功后,该服务会生成并返回一个 JWT Token。
- BlogService:受保护的服务。该服务包含一个 Filter,用于验证客户端发送的 JWT Token。验证成功后,该服务会返回业务数据。
下图显示了客户端与上述服务之间的交互。
AuthenticatorService {#authenticatorservice}
Maven 依赖 {#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}
然后是 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 {#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}
本模块的 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}
Controller 有两个端点:/register
和 /login
。第一个端点负责保存新用户。后一个端点负责对用户进行身份认证。认证成功后,后一个端点会返回一个 JWT Token。
Controller 如下。
UserController {#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-中验证-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-1}
Controller 有两个端点。第一个是不受限制的端点,只需返回一条信息。第二个是受 JWT 限制的端点。
BlogControlleer {#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}
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 负责验证 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 测试应用 {#用-jwt-token-测试应用}
测试应用:
-
启动 POSTMAN 或 REST 客户端访问
AuthenticarService
-
向
login
端点发起 POST 请求,在请求体中使用Alice
和pass123
作为凭证。注意Authenticator
服务返回的 JSON Web Token。 -
接下来,启动
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/