您的位置 首页 php

使用 Spring Security 实现 JSON Web Token (JWT) 身份验证 详细的演示

介绍

在本教程中,您将学习使用 Spring Boot 和 Spring Security 实现 Json Web Token (JWT) 身份验证。 首先,您将了解一些有关 JWT 的基本理论,然后您将切换到动手模式并在您的 Spring 应用程序中实现它。 我会详细解释每一步,所以坚持到最后。

Note

如果您不熟悉 JWT,请继续阅读。 但是,如果您已经使用过 JWT 或了解它们并希望开始实现,请单击此处。

JWT 是什么?

JSON Web Token (JWT) 是一种开放的互联网标准,用于在两方之间共享安全信息。 令牌包含一个 JSON“有效负载”,该“有效负载”使用加密算法进行数字签名(使用私有密钥或公/私密钥)。 数字签名使令牌不会被篡改,因为被篡改的令牌变得无效。

JWT 看起来像这样:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.FGK5PCL49k3jfNCq6wZtn6T-uG9Dv4hOYIm55xTux8w  

相当令人生畏的一段文字,嗯?

如果仔细观察,您会注意到 JWT 中有两个句点 (.) 符号。 这些句点符号将 JWT 分为三个部分—— Header Payload Signature

JWT 的一般形式是 → header.payload.signature

Header

令牌的第一部分,header 是一个 JSON 对象,包含两个属性, typ (表示令牌的类型,即 JWT)和 alg (用于签名的算法)。

 {
  "typ": "JWT",
  "alg": "HS256"
}  

此 JSON 对象经过 Base64Url 编码以形成 字符串 的第一部分。

Payload

令牌的第二部分,有效负载包含您希望使用此 JWT 传输的数据或 “claims”。 有一些明确的声明,例如

  • sub — 令牌的主题
  • iss — 令牌的发行者
  • exp — 令牌的过期时间
  • aud — 令牌的受众您还可以添加双方都同意的自定义声明,并提供有关令牌的额外信息。 在下面的示例中,“role” 是自定义声明。 { “sub”: “john.doe@gmail.com” , “iss”: “ABC Pvt Ltd” , “role”: “ADMIN” }

Signature

签名是通过获取前两部分的编码字符串并将其与您的秘钥一起传递给签名算法来创建的。

 HMACSHA256(
 base64UrlEncode(header) + "." + base64UrlEncode(payload),
 secret
)  

输出是你之前看到的 JWT

JWT 身份验证流程

下图显示了 JWT 身份验证的流程。 如下图所示,服务器端没有存储任何内容。

动作执行计划

您将构建一个公开三个端点的 REST API —

  1. /api/auth/register — 创建并持久化一个 User 实体对象,并以使用此实体构建的 JWT 进行响应。
  2. /api/auth/login — 验证用户凭据并生成 JWT
  3. /api/user/info — 受保护的路由,它响应经过身份验证的用户的用户信息

设置项目

是时候动手实践一下了,看看这一切的实际效果。 要设置您的 Spring Boot 项目,请访问 。 确保您选择了 Maven 项目和最新版本的 Spring Boot(一个没有 SNAPSHOT)。

添加以下依赖项:-

  1. Spring Web : 用于构建 Web 应用程序
  2. Spring Security : 为您的应用程序增加安全
  3. Spring Data JPA : 用于持久化
  4. H2 Database : 用于存储我们的应用程序数据的内存数据库
  5. Lombok : 使用注解帮助减少样板代码

您可以根据需要填写 artifact name description 字段。 最后,它应该看起来像这样。

单击 Generate ,它将下载包含 starter 文件的存档。 提取文件并在您喜欢的 IDE 中打开它们。 这将是项目的文件结构

 com
    └───yyit
        └─── SpringSecurity jwttutorial
            │   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  

计划执行

Entity

首先让我们创建我们的用户实体。 创建一个新包 entity 并创建一个类 User 。 此类使用 id、email 和 password 字段定义 User POJO。 @Entity 注解将这个类标记为实体,其他注解是 Lombok 注解,以减少样板代码(例如添加 getter、setter、构造函数等)。

Note

注意: @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 防止 password 字段包含在 JSON 响应中。

 package com.yyit.springsecurityjwttutorial.entity;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgs Constructor ;
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;

}  

Repository

现在我们有了实体,让我们创建一种持久化它的方法。 创建一个新包 repository 并创建一个新接口 UserRepo 。 我们定义了一个自定义方法 findByEmail(String email) ,它根据用户的电子邮件检索用户实体。 (有点不言自明,嗯? _( . )_/ )

 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);
}  

现在让我们进入最重要的部分 —— 安全性

Security

在我们做任何与安全相关的事情之前,让我们首先创建一个类来处理 JWT 的创建和验证。 创建一个包 security 并在其中创建一个类 JWTUtil 。 要执行 JWT 相关操作,我建议您使用 java-jwt 包。 要将包包含在您的项目中,请将以下依赖项添加到您的 pom .xml 文件中,然后重新构建项目。

 <dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.18.3</version>
</dependency>  

Note

注意:最好从 github 站点复制依赖项,因为在您阅读本文时,最新版本可能会有所不同。 您可以在此处找到该软件包的 github 站点 → 。

 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. Exception s.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();
    }

}  

generateToken 方法创建一个带有 subject issuer 、发布时间和自定义声明 “email” 的令牌,第二个方法验证相同并提取 email。 现在为了让它工作,你需要提供一个秘钥。 秘钥是一个字符串(您的项目/团队/公司专用),用于签署您的令牌。 永远不要分享你的秘钥。 打开 resources/application.properties 文件并添加以下属性。

 jwt_secret=REPLACE_THIS_WITH_YOUR_SECRET  

确保您选择一个随机且长的字符串作为您的秘钥,以确保您的令牌的安全性。 一种行之有效的方法是让你的猫在键盘上跑来跑去(开个玩笑;p)

现在让我们创建用户详细信息服务。 UserDetailsService 用于提供自定义实现以获取尝试在应用程序中进行身份验证的用户的用户详细信息。 这是在 loadUserByUsername 方法中完成的。 如果没有找到这样的用户,则会抛出 UsernameNotFoundException 。 创建一个类 MyUserDetailsService 来实现 UserDetailsService 接口。

 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")));
    }
}  

如果您想了解更多关于 UserDetailsService 的工作原理以及 Spring Security 中的一般身份验证如何工作,请查看 → 。

接下来让我们创建一个 JWTFilter JWTFilter 将通过实现 OncePer Request Filter 接口为每个请求运行,并检查授权标头中是否存在承载令牌。 如果存在令牌,则将验证令牌,并通过使用 SecurityContextHolder 设置 SecurityContext 的身份验证属性来为该请求的用户设置身份验证数据。 这是您的 JWT 发挥作用的地方,并确保您已通过身份验证并可以访问需要您登录/验证的受保护资源。

 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);
    }
}  

要将这些放在一起并配置应用程序的安全性,请创建一个类 SecurityConfig

 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();
    }
}  

在配置中,需要注意的重要部分是

  • “auth” 路由请求被授予所有人访问权限(这很明显,因为您需要访问登录和注册路由)
  • “user” 路由请求只能由具有 “USER” 角色的经过身份验证的用户访问,该角色在 MyUserDetailsService 中设置
  • UserDetailsService 使用自定义 MyUserDetailsService bean 进行配置
  • 服务器被配置为在到达入口点时拒绝未经授权的请求。 如果达到这一点,则意味着当前请求需要身份验证,并且没有发现 JWT 令牌附加到当前请求的 Authorization header。
  • JWTFilter 被添加到过滤器链中以处理传入的请求。
  • 为密码编码器创建一个 bean
  • 公开身份验证管理器的 bean,它将用于在 AuthController 中运行身份验证过程 Models 创建一个包 models 并创建一个类 LoginCredentials 。 此类将用于接受来自请求体的登录数据。 这个类有两个简单的属性—— email 和 password 以及相关的 Lombok 注解。
 package com.yyit.springsecurityjwttutorial.models;

import lombok.*;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class LoginCredentials {

    private String email;
    private String password;

}  

最后,让我们把它们放在一起。 创建一个控制器包。 在包中,创建两个类

  • AuthController — 处理 auth 路由注册和登录。
 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");
        }
    }


}  

register 方法将实体持久化,然后以 JWT 响应,而 login 方法验证登录凭据,然后以 JWT 响应。

  • UserController — 处理 user 路由
 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();
    }


}  

请注意,不会将用户的电子邮件作为输入。 它是从 SecurityContext 中提取的,因为电子邮件是在 JWTFilter 中设置的

执行时间线

在 IntelliJ IDEA 上运行应用程序时,这是我得到的输出。 看起来一切都很好。

现在让我们提出一些要求。 为了提出请求,我将使用 IDEA 自带的 HTTP REQUEST。

万岁!! 您刚刚使用 Spring Security 生成了您的第一个 JWT。 现在让我们测试受保护的端点 → user 端点。 复制此令牌,因为您很快就会需要它。

让我们在请求文件中向 user 端点创建一个请求,而不添加令牌。

如您所见,该请求以 “Unauthorized” 状态被拒绝。

现在让我们添加令牌。

现在如果你再次发送请求,你会看到这个

瞧! 现在你得到了用户数据。 您可以自行测试登录路径。

总结

就是这样了。 现在,您可以使用 Spring Boot Security 和 Spring Boot 完全实现 JWT 身份验证流程。

文章来源:智云一二三科技

文章标题:使用 Spring Security 实现 JSON Web Token (JWT) 身份验证 详细的演示

文章地址:https://www.zhihuclub.com/79969.shtml

关于作者: 智云科技

热门文章

网站地图