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/