【springboot开发】JWT实现用户验证登录

前言:相比于传统的session或者cookie登录验证,JWT更为安全。JWT 是 JSON Web Token 的缩写,是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准((RFC 7519)。定义了一种简洁的,自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT 可以使用 HMAC 算法或者是 RSA 的公私秘钥对进行签名。

1. JWT简介

1.1 JWT流程

JWT完整流程如下:
在这里插入图片描述
主要包含以下几个步骤:

  • 用户使用账号和密码发起 POST 请求;
  • 服务器使用私钥创建一个 JWT;
  • 服务器返回这个 JWT 给浏览器;
  • 浏览器将该 JWT 串在请求头中像服务器发送请求;
  • 服务器验证该 JWT;
  • 返回响应的资源给浏览器。

1.2 数据结构

JWT字符串包含三个部分,依次为:

  • 头部:Header
    Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
  "alg": "HS256",
  "typ": "JWT"
}
  • 负载:Payload
    Payload 部分也是一个 JSON 对象,用来存放实际需要传递的有效信息。有效信息包含三个部分:标准中注册的声明、公共的声明、私有的声明

  • 签名:Signature。
    Signature 部分是对前两部分的签名,防止数据篡改。
    首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符 +、 / 和 =,在 URL 里面有特殊含义,所以要被替换掉:= 被省略、+ 替换成 -,/ 替换成 _ 。这就是 Base64URL 算法。

1.3 JWT使用方法

客户端收到服务器返回的 JWT 之后需要在本地做保存。此后,客户端每次与服务器通信,都要带上这个 JWT。一般的的做法是放在 HTTP 请求的头信息 Authorization 字段里面。

Authorization: Bearer <token>

1.4 优势

  • 不需要在服务端保存会话信息,特别适用于分布式微服务。
  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据
  • 简洁(Compact):可以通过URL, POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
  • 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。

1.5 注意事项

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  2. JWT 不加密的情况下,不能将秘密数据写入 JWT。
  3. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

2. Springboot整合JWT

2.1 引入依赖

jwt有两种库文件:io.jsonwebtoken:jjwt和com.auth0:java-jwt,前者是轻量级的,后者更全面,这里我们使用com.auth0:java-jwt。

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>${java-jwt.version}</version>
            <scope>compile</scope>
        </dependency>

2.2 修改配置文件

# 生成JWT相关配置
jwt:
  private-key-path: auto-gen-1024
  expiration-minutes: 1440
  algorithm: RSA

存放了密钥,公钥也可以存放在配置文件中,也可以为空。这里我们不设置公钥,仅设置密钥、有效时间、加密算法。

2.3 JWT初始化类

主要需要包含将用户信息序列化为JWT、通过JWT进行字符串解析、验证用户的功能。类中的常量主要包含以下:

    private static final String PREFIX = "Bearer ";
    private static final String ISSUER = "nwpu.dev";
    private static final String SUBJECT = "buildBaseFrame";
    private static final String USER_ID_KEY = "id";
    private static final String KEY_AUTO_GEN_KEY = "auto-gen";
    private static final String KEY_SPILT_SIGN = "-";
  • 序列化用户信息
public String createJwt(Long userId) {
        Calendar instance = Calendar.getInstance();
        Date now = instance.getTime();
        instance.add(Calendar.MINUTE, expirationMinutes);
        return PREFIX + JWT.create()
                .withIssuer(ISSUER)
                .withSubject(SUBJECT)
                .withIssuedAt(now)
                .withExpiresAt(instance.getTime())
                .withClaim(USER_ID_KEY, userId.toString())
                .sign(createAlgorithm());
    }
  • 解析JWT字符串
public <T> T parseJwt(String token, Class<T> valueType) throws JsonProcessingException {
        if (StringUtils.hasText(token)) {
            if (!token.startsWith(PREFIX)) {
                throw new JwtException("身份令牌格式错误").setJwtMsg("Authorization要求以\"Bearer \"开头");
            }
        } else {
            throw new NoAuthException();
        }

        DecodedJWT decodedJwt = JWT.decode(token.substring(PREFIX.length()));
        String decoded = new String(Base64.getMimeDecoder().decode(decodedJwt.getPayload()));
        return objectMapper.readValue(decoded, valueType);
    }
  • 验证JWT字符串
    public <T> T verifyJwt(String token, Class<T> valueType) throws JsonProcessingException {
        if (publicKey == null) {
            throw new LogicException("缺少公钥错误: 验证Jwt要求指定传入公钥");
        }
        if (StringUtils.hasText(token)) {
            if (!token.startsWith(PREFIX)) {
                throw new JwtException("身份令牌格式错误").setJwtMsg("Authorization要求以\"Bearer \"开头");
            }
        } else {
            throw new NoAuthException();
        }

        JWTVerifier jwtVerifier = JWT.require(createAlgorithm())
                .withIssuer(ISSUER)
                .withSubject(SUBJECT)
                .withClaimPresence(USER_ID_KEY)
                .build();
        DecodedJWT jwt;
        try {
            jwt = jwtVerifier.verify(token.substring(PREFIX.length()));
        } catch (JWTVerificationException e) {
            log.info("身份令牌无效:{}", e.getMessage());
            throw new JwtException("身份令牌无效", e.getCause()).setJwtMsg(e.getMessage());
        }
        String decoded = new String(Base64.getMimeDecoder().decode(jwt.getPayload()));
        return objectMapper.readValue(decoded, valueType);
    }

完整代码如下:

@Slf4j
@Component
public class JwtGenerator implements InitializingBean {

    private static final String PREFIX = "Bearer ";
    private static final String ISSUER = "nwpu.dev";
    private static final String SUBJECT = "buildBaseFrame";
    private static final String USER_ID_KEY = "id";
    private static final String KEY_AUTO_GEN_KEY = "auto-gen";
    private static final String KEY_SPILT_SIGN = "-";

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 私钥文件路径,可为:auto_gen_xxx(此模式公钥同步自动生成,不读取公钥路径)、私钥文件路径
     */
    @Value("${jwt.private-key-path}")
    public String privateKeyPath;
    @Value("${jwt.private-key-output-path:}")
    public String privateKeyOutputPath;
    /**
     * 公钥文件路径,可为:空(不加载公钥模式,不可进行验证)、公钥文件路径
     */
    @Value("${jwt.public-key-path:}")
    public String publicKeyPath;
    @Value("${jwt.public-key-output-path:}")
    public String publicKeyOutputPath;
    /**
     * 令牌有效时间(单位:分钟)
     */
    @Value("${jwt.expiration-minutes}")
    public int expirationMinutes;
    @Value("${jwt.algorithm:RSA}")
    public String algorithm;

    private PrivateKey privateKey;
    private PublicKey publicKey;

    @Override
    public void afterPropertiesSet() throws Exception {
        if (privateKeyPath.startsWith(KEY_AUTO_GEN_KEY + KEY_SPILT_SIGN)) {
            String[] words = privateKeyPath.split(KEY_SPILT_SIGN);
            int keySize = Integer.parseInt(words[words.length - 1]);
            KeyPair keyPair = genKeyPair(algorithm, keySize);
            privateKey = keyPair.getPrivate();
            publicKey = keyPair.getPublic();
            if (StringUtils.hasText(privateKeyOutputPath)) {
                Files.write(Paths.get(privateKeyOutputPath), privateKey.getEncoded());
            }
            if (StringUtils.hasText(publicKeyOutputPath)) {
                Files.write(Paths.get(publicKeyOutputPath), publicKey.getEncoded());
            }
        } else {
            privateKey = loadPrivateKeyFromFile(algorithm, privateKeyPath);
            if (StringUtils.hasText(publicKeyPath)) {
                publicKey = loadPublicKeyFromFile(algorithm, publicKeyPath);
            }
        }
    }

    /**
     * 将 User 基本信息序列化创建 jwt
     */
    public String createJwt(Long userId) {
        Calendar instance = Calendar.getInstance();
        Date now = instance.getTime();
        instance.add(Calendar.MINUTE, expirationMinutes);
        return PREFIX + JWT.create()
                .withIssuer(ISSUER)
                .withSubject(SUBJECT)
                .withIssuedAt(now)
                .withExpiresAt(instance.getTime())
                .withClaim(USER_ID_KEY, userId.toString())
                .sign(createAlgorithm());
    }

    /**
     * 验证 jwt token,将 payload 反序列化为指定对象并返回,若验证不通过返回 null
     */
    public <T> T verifyJwt(String token, Class<T> valueType) throws JsonProcessingException {
        if (publicKey == null) {
            throw new LogicException("缺少公钥错误: 验证Jwt要求指定传入公钥");
        }
        if (StringUtils.hasText(token)) {
            if (!token.startsWith(PREFIX)) {
                throw new JwtException("身份令牌格式错误").setJwtMsg("Authorization要求以\"Bearer \"开头");
            }
        } else {
            throw new NoAuthException();
        }

        JWTVerifier jwtVerifier = JWT.require(createAlgorithm())
                .withIssuer(ISSUER)
                .withSubject(SUBJECT)
                .withClaimPresence(USER_ID_KEY)
                .build();
        DecodedJWT jwt;
        try {
            jwt = jwtVerifier.verify(token.substring(PREFIX.length()));
        } catch (JWTVerificationException e) {
            log.info("身份令牌无效:{}", e.getMessage());
            throw new JwtException("身份令牌无效", e.getCause()).setJwtMsg(e.getMessage());
        }
        String decoded = new String(Base64.getMimeDecoder().decode(jwt.getPayload()));
        return objectMapper.readValue(decoded, valueType);
    }

    /**
     * 解析 JWT
     */
    public <T> T parseJwt(String token, Class<T> valueType) throws JsonProcessingException {
        if (StringUtils.hasText(token)) {
            if (!token.startsWith(PREFIX)) {
                throw new JwtException("身份令牌格式错误").setJwtMsg("Authorization要求以\"Bearer \"开头");
            }
        } else {
            throw new NoAuthException();
        }

        DecodedJWT decodedJwt = JWT.decode(token.substring(PREFIX.length()));
        String decoded = new String(Base64.getMimeDecoder().decode(decodedJwt.getPayload()));
        return objectMapper.readValue(decoded, valueType);
    }

    private Algorithm createAlgorithm() {
        switch (algorithm) {
            case "RSA":
                return Algorithm.RSA256((RSAPublicKey) publicKey, (RSAPrivateKey) privateKey);
            default:
                throw new LogicException("未知的jwt加密算法");
        }
    }

    private KeyPair genKeyPair(String keyAlgorithm, int keySize) throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyAlgorithm);
        keyPairGenerator.initialize(keySize);
        return keyPairGenerator.genKeyPair();
    }

    private static PublicKey loadPublicKeyFromFile(String algorithm, String filePath) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        byte[] pubKeyBytes = Files.readAllBytes(Paths.get(filePath));
        X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(pubKeyBytes);
        return keyFactory.generatePublic(pubSpec);
    }

    private static PrivateKey loadPrivateKeyFromFile(String algorithm, String filePath) throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        byte[] priKeyBytes = Files.readAllBytes(Paths.get(filePath));
        PKCS8EncodedKeySpec priSpec = new PKCS8EncodedKeySpec(priKeyBytes);
        return keyFactory.generatePrivate(priSpec);
    }

    private static String bytes2Base64UTF8(byte[] bytes) {
        return new String(Base64.getMimeEncoder().encode(bytes), StandardCharsets.UTF_8);
    }

}

2.4 添加JWT过滤器

@Slf4j
@Component
public class AuthFilter extends OncePerRequestFilter {

    private static final String TRACE_ID_KEY_IN_REQUEST = "X-B3-Traceid";
    private static final String TRACE_ID_KEY_IN_LOG = "traceId";
    private static final String USER_ID_KEY_IN_LOG = "userId";
    private static final String USER_INFO_KEY_IN_HEADER = "Authorization";

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private JwtGenerator jwtGenerator;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //在日志中添加traceId和userId
        String traceId = String.valueOf(request.getHeader(TRACE_ID_KEY_IN_REQUEST));
        MDC.put(TRACE_ID_KEY_IN_LOG, traceId);
        response.addHeader(TRACE_ID_KEY_IN_REQUEST, traceId);

        // 不拦截注册页面
        String uri = request.getRequestURI();
        if (uri.equals("/register")) {
            filterChain.doFilter(request, response);
        }

        //从Header中获取用户信息
        String token = request.getHeader(USER_INFO_KEY_IN_HEADER);
        try {
            Authentication auth = jwtGenerator.parseJwt(token, Authentication.class);
            SecurityContextHolder.set(auth);
        } catch (JwtException e) {
            log.info("{}: {}", e.getMessage(), e.getJwtMsg());
            //过滤器中抛出的异常是无法被全局异常处理器捕获的!所以只能手动设置返回值,不能依赖全局异常处理器。
            setAuthorizedErrorResponse(response, CommonResult.failure(e.getMessage()));

            // setAuthorizedErrorResponse 方法把响应内容写入了 response 对象的 Writer 中,但没有调用 flush() 方法来刷新缓冲区。因此,如果响应内容太小(例如只有几个字符),可能无法立即返回客户端。需要在方法最后添加一句 response.flushBuffer() 来强制刷新缓冲区,保证响应内容能够立即返回客户端。
            response.flushBuffer();
            return;
        } catch (NoAuthException e) {
            log.info("{}: {}", e.getMessage(), "用户没有权限");
            SecurityContextHolder.set(new Authentication(-1L));
        }

        //在日志中添加userId
        MDC.put(USER_ID_KEY_IN_LOG, String.valueOf(SecurityContextHolder.getUserId()));
        //打印HTTP请求入站日志
        log.info("入站请求 {} {}, 参数: {}, 用户: {}", request.getMethod(), request.getRequestURI(), request.getQueryString(), SecurityContextHolder.getUserId());

        //放行请求
        filterChain.doFilter(request, response);
        //清空上下文
        SecurityContextHolder.remove();
    }

    private void setAuthorizedErrorResponse(HttpServletResponse response, CommonResult result) throws IOException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().print(objectMapper.writeValueAsString(result));
    }

}

OncePerRequestFilter是指对每个request请求都进行过滤,代表请求的每个页面都需要进行登录验证,除了注册页面。从前端发送的header中找到"Authorization"对应的token,对其进行解析验证,如果通过则放行request,否则报错。
为了存储用户信息,新建一个类SecurityContextHolder,JWT解析验证通过后,需要使用这个类存储用户信息,以后再需要用户信息时可以通过这个类来获取。

public class SecurityContextHolder {

    private static final ThreadLocal<Authentication> HOLDER = new ThreadLocal<>();

    public static void set(Authentication authentication) {
        HOLDER.set(authentication);
    }

    public static Long getUserId() {
        return HOLDER.get().getId();
    }

    public static void remove() {
        HOLDER.remove();
    }

}

使用id来代表用户信息。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Authentication {

    /**
     * 用户ID
     */
    private Long id;

}

2.5 密码验证登录

    @RequestMapping(value = "login/pwd", method = RequestMethod.POST)
    @SneakyThrows(JsonProcessingException.class)
    public CommonResult login(@RequestBody String json,
                              HttpServletResponse response) throws NotFoundException{

        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode rootNode = objectMapper.readTree(json);
        String nickname = rootNode.get("nickname").asText();
        String password = rootNode.get("password").asText();

        UserInfoDto user = service.getUserByname(nickname);

        if(user==null){
            return CommonResult.failure("账号或密码错误");
        }
        UserInfoVo userInfoVo = userApiConverter.toUserInfoVo(user);

        boolean flag = pwdLoginService.verifyPwdLogin(user, password);

        String jwt = jwtGenerator.createJwt(user.getId());

        response.addHeader("Authorization", jwt);

        if(flag){
            return CommonResult.success(userInfoVo);
        }else {
            return CommonResult.failure("账号或密码错误");

        }

    }

验证用户密码通过后需要生成jwt添加到header中,以后每次客户端发送请求都应该携带这个"Authorization"字段。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 可以参考下面的代码实现 Springboot 使用 JWT 实现 Token 登录验证:@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { // ... @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/**").authenticated() .and() .addFilter(new JWTAuthenticationFilter(authenticationManager())) .addFilter(new JWTAuthorizationFilter(authenticationManager())); } } ### 回答2: Spring Boot是一个开发框架,可用于构建独立的、基于Spring的应用程序。JWT(Json Web Token)是一种用于认证和授权的开放标准,它将用户信息加密在令牌中。 下面是使用Spring BootJWT实现Token登录验证的代码示例: 1. 首先,需要导入所需的依赖项。在pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> ``` 2. 创建一个JWT工具类,用于生成和解析JWT的方法: ```java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JwtUtil { private static final String SECRET_KEY = "your_secret_key"; private static final long EXPIRATION_TIME = 86400000; // 24小时 public static String generateToken(String username) { Date now = new Date(); Date expirationTime = new Date(now.getTime() + EXPIRATION_TIME); return Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expirationTime) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public static boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } } ``` 3. 创建一个控制器,包含登录验证方法: ```java import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api") public class AuthController { @PostMapping("/login") public ResponseEntity<?> login(@RequestBody UserCredentials credentials) { // 检查用户凭据并生成令牌 if (credentials.getUsername().equals("admin") && credentials.getPassword().equals("admin123")) { String token = JwtUtil.generateToken(credentials.getUsername()); return ResponseEntity.ok(new AuthResponse(token)); } else { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } @GetMapping("/protected") public ResponseEntity<?> protectedResource(@RequestHeader("Authorization") String token) { // 验证令牌并提供受保护的资源 if (JwtUtil.validateToken(token)) { return ResponseEntity.ok("Protected resource accessed successfully"); } else { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } } ``` 4. 定义用户凭据模型类: ```java public class UserCredentials { private String username; private String password; // getter和setter方法 } ``` 5. 定义认证响应模型类: ```java public class AuthResponse { private String token; // 构造函数和getter方法 } ``` 上述代码示例实现了使用JWT进行Token登录验证的功能。用户通过登录接口提供正确的凭据后,将获得一个JWT令牌。然后,可以使用此令牌访问受保护的资源,该资源通过令牌进行验证。如果验证成功,则允许访问,否则返回未授权的HTTP状态码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值