在本教程中,您将学习使用 Spring Boot 和 Spring Security 实现 Json Web Token (JWT) 身份验证。 首先,您将了解一些有关 JWT 的基本理论,然后您将切换到动手模式并在您的 Spring 应用程序中实现它。 我会详细解释每一步,所以坚持到最后。 Note 如果您不熟悉 JWT,请继续阅读。 但是,如果您已经使用过 JWT 或了解它们并希望开始实现,请单击此处。 JSON Web Token (JWT) 是一种开放的互联网标准,用于在两方之间共享安全信息。 令牌包含一个 JSON“有效负载”,该“有效负载”使用加密算法进行数字签名(使用私有密钥或公/私密钥)。 数字签名使令牌不会被篡改,因为被篡改的令牌变得无效。 JWT 看起来像这样: 相当令人生畏的一段文字,嗯? 如果仔细观察,您会注意到 JWT 中有两个句点 (.) 符号。 这些句点符号将 JWT 分为三个部分—— Header、Payload 和 Signature。 JWT 的一般形式是 → header.payload.signature。 Header 令牌的第一部分,header 是一个 JSON 对象,包含两个属性,typ(表示令牌的类型,即 JWT)和 alg(用于签名的算法)。 此 JSON 对象经过 Base64Url 编码以形成字符串的第一部分。 Payload 令牌的第二部分,有效负载包含您希望使用此 JWT 传输的数据或 “claims”。 有一些明确的声明,例如 Signature 签名是通过获取前两部分的编码字符串并将其与您的秘钥一起传递给签名算法来创建的。 输出是你之前看到的 JWT 下图显示了 JWT 身份验证的流程。 如下图所示,服务器端没有存储任何内容。 您将构建一个公开三个端点的 REST API — 是时候动手实践一下了,看看这一切的实际效果。 要设置您的 Spring Boot 项目,请访问 https://start.spring.io/ 。 确保您选择了 Maven 项目和最新版本的 Spring Boot(一个没有 SNAPSHOT)。 添加以下依赖项:- 您可以根据需要填写 artifact、name 和 description 字段。 最后,它应该看起来像这样。 单击 Generate,它将下载包含 starter 文件的存档。 提取文件并在您喜欢的 IDE 中打开它们。 这将是项目的文件结构 Entity 首先让我们创建我们的用户实体。 创建一个新包 entity 并创建一个类 User 。 此类使用 id、email 和 password 字段定义 User POJO。 @Entity 注解将这个类标记为实体,其他注解是 Lombok 注解,以减少样板代码(例如添加 getter、setter、构造函数等)。 Note 注意:@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 防止 password 字段包含在 JSON 响应中。 Repository 现在我们有了实体,让我们创建一种持久化它的方法。 创建一个新包 repository 并创建一个新接口 UserRepo 。 我们定义了一个自定义方法 findByEmail(String email),它根据用户的电子邮件检索用户实体。 (有点不言自明,嗯? _(.)_/ ) 现在让我们进入最重要的部分 —— 安全性 Security 在我们做任何与安全相关的事情之前,让我们首先创建一个类来处理 JWT 的创建和验证。 创建一个包 security 并在其中创建一个类 JWTUtil 。 要执行 JWT 相关操作,我建议您使用 java-jwt 包。 要将包包含在您的项目中,请将以下依赖项添加到您的 pom.xml 文件中,然后重新构建项目。 Note 注意:最好从 github 站点复制依赖项,因为在您阅读本文时,最新版本可能会有所不同。 您可以在此处找到该软件包的 github 站点 → https://github.com/auth0/java-jwt。 generateToken 方法创建一个带有 subject、issuer、发布时间和自定义声明 “email” 的令牌,第二个方法验证相同并提取 email。 现在为了让它工作,你需要提供一个秘钥。 秘钥是一个字符串(您的项目/团队/公司专用),用于签署您的令牌。 永远不要分享你的秘钥。 打开 resources/application.properties 文件并添加以下属性。 确保您选择一个随机且长的字符串作为您的秘钥,以确保您的令牌的安全性。 一种行之有效的方法是让你的猫在键盘上跑来跑去(开个玩笑;p) 现在让我们创建用户详细信息服务。 UserDetailsService 用于提供自定义实现以获取尝试在应用程序中进行身份验证的用户的用户详细信息。 这是在 loadUserByUsername 方法中完成的。 如果没有找到这样的用户,则会抛出 UsernameNotFoundException。 创建一个类 MyUserDetailsService 来实现 UserDetailsService 接口。 如果您想了解更多关于 UserDetailsService 的工作原理以及 Spring Security 中的一般身份验证如何工作,请查看 → https://medium.com/geekculture/spring-security-authentication-process-authentication-flow-behind-the-scenes-d56da63f04fa。 接下来让我们创建一个 JWTFilter。 JWTFilter 将通过实现 OncePerRequestFilter 接口为每个请求运行,并检查授权标头中是否存在承载令牌。 如果存在令牌,则将验证令牌,并通过使用 SecurityContextHolder 设置 SecurityContext 的身份验证属性来为该请求的用户设置身份验证数据。 这是您的 JWT 发挥作用的地方,并确保您已通过身份验证并可以访问需要您登录/验证的受保护资源。 要将这些放在一起并配置应用程序的安全性,请创建一个类 SecurityConfig 在配置中,需要注意的重要部分是 最后,让我们把它们放在一起。 创建一个控制器包。 在包中,创建两个类 register 方法将实体持久化,然后以 JWT 响应,而 login 方法验证登录凭据,然后以 JWT 响应。 请注意,不会将用户的电子邮件作为输入。 它是从 SecurityContext 中提取的,因为电子邮件是在 JWTFilter 中设置的 在 IntelliJ IDEA 上运行应用程序时,这是我得到的输出。 看起来一切都很好。 现在让我们提出一些要求。 为了提出请求,我将使用 IDEA 自带的 HTTP REQUEST。 万岁!! 您刚刚使用 Spring Security 生成了您的第一个 JWT。 现在让我们测试受保护的端点 → user 端点。 复制此令牌,因为您很快就会需要它。 让我们在请求文件中向 user 端点创建一个请求,而不添加令牌。 如您所见,该请求以 “Unauthorized” 状态被拒绝。 现在让我们添加令牌。 现在如果你再次发送请求,你会看到这个 瞧! 现在你得到了用户数据。 您可以自行测试登录路径。 就是这样了。 现在,您可以使用 Spring Boot Security 和 Spring Boot 完全实现 JWT 身份验证流程。介绍
JWT 是什么?
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.FGK5PCL49k3jfNCq6wZtn6T-uG9Dv4hOYIm55xTux8w
{
"typ": "JWT",
"alg": "HS256"
}
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
JWT 身份验证流程
动作执行计划
设置项目
com
└───yyit
└───springsecurityjwttutorial
│ SpringSecurityJwtTutorialApplication.java
│
├───controllers
│ AuthController.java
│ UserController.java
│
├───entity
│ User.java
│
├───models
│ LoginCredentials.java
│
├───repository
│ UserRepo.java
│
└───security
JWTFilter.java
JWTUtil.java
MyUserDetailsService.java
SecurityConfig.java
计划执行
package com.yyit.springsecurityjwttutorial.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String email;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
}
package com.yyit.springsecurityjwttutorial.repository;
import com.yyit.springsecurityjwttutorial.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepo extends JpaRepository<User, Long> {
public Optional<User> findByEmail(String email);
}
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
package com.yyit.springsecurityjwttutorial.security;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JWTUtil {
@Value("${jwt_secret}")
private String secret;
public String generateToken(String email) throws IllegalArgumentException, JWTCreationException {
return JWT.create()
.withSubject("User Details")
.withClaim("email", email)
.withIssuedAt(new Date())
.withIssuer("YOUR APPLICATION/PROJECT/COMPANY NAME")
.sign(Algorithm.HMAC256(secret));
}
public String validateTokenAndRetrieveSubject(String token)throws JWTVerificationException {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret))
.withSubject("User Details")
.withIssuer("YOUR APPLICATION/PROJECT/COMPANY NAME")
.build();
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaim("email").asString();
}
}
jwt_secret=REPLACE_THIS_WITH_YOUR_SECRET
package com.yyit.springsecurityjwttutorial.security;
import com.yyit.springsecurityjwttutorial.entity.User;
import com.yyit.springsecurityjwttutorial.repository.UserRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Optional;
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired private UserRepo userRepo;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Optional<User> userRes = userRepo.findByEmail(email);
if(userRes.isEmpty())
throw new UsernameNotFoundException("Could not findUser with email = " + email);
User user = userRes.get();
return new org.springframework.security.core.userdetails.User(
email,
user.getPassword(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
}
}
package com.yyit.springsecurityjwttutorial.security;
import com.auth0.jwt.exceptions.JWTVerificationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JWTFilter extends OncePerRequestFilter {
@Autowired private MyUserDetailsService userDetailsService;
@Autowired private JWTUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if(authHeader != null && !authHeader.isBlank() && authHeader.startsWith("Bearer ")){
String jwt = authHeader.substring(7);
if(jwt == null || jwt.isBlank()){
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid JWT Token in Bearer Header");
}else {
try{
String email = jwtUtil.validateTokenAndRetrieveSubject(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(email, userDetails.getPassword(), userDetails.getAuthorities());
if(SecurityContextHolder.getContext().getAuthentication() == null){
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}catch(JWTVerificationException exc){
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid JWT Token");
}
}
}
filterChain.doFilter(request, response);
}
}
package com.yyit.springsecurityjwttutorial.security;
import com.yyit.springsecurityjwttutorial.repository.UserRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletResponse;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private UserRepo userRepo;
@Autowired private JWTFilter filter;
@Autowired private MyUserDetailsService uds;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic().disable()
.cors()
.and()
.authorizeHttpRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/user/**").hasRole("USER")
.and()
.userDetailsService(uds)
.exceptionHandling()
.authenticationEntryPoint(
(request, response, authException) ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
package com.yyit.springsecurityjwttutorial.models;
import lombok.*;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class LoginCredentials {
private String email;
private String password;
}
package com.yyit.springsecurityjwttutorial.controllers;
import com.yyit.springsecurityjwttutorial.entity.User;
import com.yyit.springsecurityjwttutorial.models.LoginCredentials;
import com.yyit.springsecurityjwttutorial.repository.UserRepo;
import com.yyit.springsecurityjwttutorial.security.JWTUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired private UserRepo userRepo;
@Autowired private JWTUtil jwtUtil;
@Autowired private AuthenticationManager authManager;
@Autowired private PasswordEncoder passwordEncoder;
@PostMapping("/register")
public Map<String, Object> registerHandler(@RequestBody User user){
String encodedPass = passwordEncoder.encode(user.getPassword());
user.setPassword(encodedPass);
user = userRepo.save(user);
String token = jwtUtil.generateToken(user.getEmail());
return Collections.singletonMap("jwt-token", token);
}
@PostMapping("/login")
public Map<String, Object> loginHandler(@RequestBody LoginCredentials body){
try {
UsernamePasswordAuthenticationToken authInputToken =
new UsernamePasswordAuthenticationToken(body.getEmail(), body.getPassword());
authManager.authenticate(authInputToken);
String token = jwtUtil.generateToken(body.getEmail());
return Collections.singletonMap("jwt-token", token);
}catch (AuthenticationException authExc){
throw new RuntimeException("Invalid Login Credentials");
}
}
}
package com.yyit.springsecurityjwttutorial.controllers;
import com.yyit.springsecurityjwttutorial.entity.User;
import com.yyit.springsecurityjwttutorial.repository.UserRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired private UserRepo userRepo;
@GetMapping("/info")
public User getUserDetails(){
String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userRepo.findByEmail(email).get();
}
}
执行时间线
总结