SpringBoot采用jwt作为REST API安全机制的应用示例

Posted 左直拳

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot采用jwt作为REST API安全机制的应用示例相关的知识,希望对你有一定的参考价值。

对外提供数据接口,安全性是必须考虑的问题。JWT无需存储客户端状态,也无须预先分配api_key、secrity_key,仅需校验数据签名,检查时间戳,就能保证请求的身份信息的完整性和防止重放攻击;而对于客户端来说,也没有跨域问题,不失为一种轻量的REST API安全机制。

一、概述

WebService有两种方案:基于SOAP的RPC和基于HTTP的REST API。REST API轻便,无状态,伸缩性更好,得到广泛使用,但缺少对安全性的直接支持,需要自己解决安全性问题。

安全性设计是个宏大的命题。这里说的REST API的安全性,只包括身份验证和授权策略2个方面,再有就是数据传输。数据传输现在一般都采用https,传输过程是加密的;授权策略,对于REST API来说,主要工作在服务器端,不会有什么大问题。所以REST API的安全性设计主要是身份验证。

REST API的身份验证方案,大约有这么几种:HTTP Basic、HTTP Digest、API KEY、Oauth 和 JWT。

1、HTTP Basic
每次请求,简单的将用户名和密码 base64 编码放到header中,传送给服务器。所以安全性较低。一定要配合ssl进行数据传输,否则无异于裸奔。

2.API KEY
Client 端向服务端注册,获得api_key以及security_key。请求的时候,客户端根据 api_key、secrity_key、timestrap、rest_uri 采用 hmacsha256 算法得到一个 hash 值 sign,发送给服务端。

服务端收到该请求后,首先验证 api_key 和 security_key,接着验证 timestrap 是否超过时间限制,最后计算 sign 值,和传过来的sign 值做校验。这样的设计就防止了数据被篡改和重放攻击。

最典型的应用,莫过于微信的接口。

3.Oauth1.0a 或者 Oauth2
OAuth 协议适用于为外部应用授权访问本站资源的情况。可见拙作:oAuth

4.JWT
JWT(JSON Web Token)。第一次身份认证后,服务器生成一个token给客户端,客户端之后每次请求,都带上这个token。听上去与第一点的http basic类似,但这个token有时间戳和签名,同样可以保证token的完整性和防止重放攻击。

不过,token里的信息,除了签名,其余部分也只是使用base64简单处理,跟明文无异,靠签名校验来保证安全性,因此也应该使用SSL进行数据传输,同时有效时间也不宜设置过久,30分钟足矣。(当然,原始token产生以后,我们自行进行加密也是可以的)

二、JWT的工作原理

1、工作过程

2、JWT的数据结构
大约类似这样:

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下。

Header(头部)
Payload(负载)
Signature(签名)

写成一行,就是下面的样子

Header.Payload.Signature

3、Signature
Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

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

三、示例

1、服务器端代码结构(JAVA)

2、代码概述
控制器与外部交互,提供接口给客户端,包括身份认证接口、数据获取接口等;

身份认证时,系统验证客户端提交的用户名和密码,通过后生成JWT返回客户端;

客户端请求数据时,将JWT附在header中。拦截器会检查JWT,有效则返回数据。

3、代码明细
1)TokenUser.java

public class TokenUser 
    private String name;
    private String password;
    private String ip;

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    

    public String getPassword() 
        return password;
    

    public void setPassword(String password) 
        this.password = password;
    

    public String getIp() 
        return ip;
    

    public void setIp(String ip) 
        this.ip = ip;
    

2)JWT静态类及相关pom.xml

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.Map;

public class JwtUtils 
    private static String SECRET = "略去"; //密钥
    
    /*
        为了保证令牌的安全性,jwt令牌由三个部分组成,分别是:
        header:令牌头部,记录了整个令牌的类型和签名算法
        payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
        signature:令牌签名,按照头部固定的签名算法对整个令牌进行签名,该签名的作用是:保证令牌不被伪造和篡改
        它们组合而成的完整格式是:header.payload.signature
     */

    /**
     * 生成token
     *
     * @param map //传入payload
     *            payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
     * @return 返回token
     */
    public static String getToken(Map<String, String> map) 
        JWTCreator.Builder builder = JWT.create();
        map.forEach((k, v) -> 
            builder.withClaim(k, v);
        );
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.MINUTE, 30);//有效期30分钟?
        builder.withExpiresAt(instance.getTime());
        return builder.sign(Algorithm.HMAC256(SECRET)).toString();
    

    /**
     * 验证token
     *
     * @param token
     */
    public static void verify(String token) 
        JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
    

    /**
     * 获取token明文
     *
     * @param token
     * @return
     */
    public static DecodedJWT getTokenPlainText(String token) 
        return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
    

pom.xml中要引入jwt类库

        <!--引入JWT-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.0</version>
        </dependency>

3)拦截器及拦截器使能

(1)拦截器

mport com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gzdd.rainapi.utils.JwtUtils;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT验证拦截器
 */
@Component
public class JwtInterceptor implements HandlerInterceptor 

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        /**
         * 前后端分离有时候会有两次请求,第一次为OPTIONS请求,默认会拦截所有请求,但是第一次请求又获取不到jwt,所以会出错。
         **/
        if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) 
            return true;
        

        Map<String, Object> map = new HashMap<>();
        //令牌建议是放在请求头中,获取请求头中令牌
        String token = request.getHeader("token");
        try 
            JwtUtils.verify(token);//验证令牌
            return true;//放行请求
         catch (SignatureVerificationException e) 
            map.put("msg", "无效签名");
            map.put("status", 401);
         catch (TokenExpiredException e) 
            map.put("msg", "token过期");
            map.put("status", 401);
         catch (AlgorithmMismatchException e) 
            map.put("msg", "token算法不一致");
            map.put("status", 401);
         catch (Exception e) 
            map.put("msg", "token失效");
            map.put("status", 401);
        
        map.put("state", false);//设置状态
        //将map转化成json,response使用的是Jackson
        System.out.println(map);

        Map res = new HashMap();
        res.put("data", map);
        String json = new ObjectMapper().writeValueAsString(res);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().print(json);

        return false;
    

(2)使用拦截器

import com.gzdd.rainapi.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class SecurityConfiguration implements WebMvcConfigurer 

    @Autowired
    private JwtInterceptor jwtI;

    public void addInterceptors(InterceptorRegistry registry) 
        registry.addInterceptor(jwtI)
                .addPathPatterns("/data/*");//取数据的接口,必须检查JWT
    

4)控制器
(1)身份认证

import com.gzdd.rainapi.config.AppConfig;
import com.gzdd.rainapi.pojo.TokenUser;
import com.gzdd.rainapi.utils.IPUtils;
import com.gzdd.rainapi.utils.JwtUtils;
import com.gzdd.rainapi.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
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 javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@RestController
public class IndexController 
    @Autowired
    AppConfig appConfig;

    @RequestMapping(value = "/test")
    public String noCheck(Model model) 
        return "Hello world!";
    

	//登录时生成token
    @PostMapping(value = "/token")
    public Result getToken(HttpServletRequest request, @RequestBody TokenUser user) 
        if (!appConfig.isValidUser(user.getName(), user.getPassword())) 
            return Result.error("非法的用户");
        

        Map<String, String> payload = new HashMap<>();
        payload.put("ip", IPUtils.getIpAddr(request));
        payload.put("name", user.getName());
        payload.put("time", System.currentTimeMillis() + "");
        String token = JwtUtils.getToken(payload);//生成JWT令牌

        return Result.ok().put("token", token);
    

(2)数据接口

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

//注意这些接口以/data/开头,符合拦截器使能条件
@RestController
@RequestMapping(value = "/data")
public class DataController 
    @RequestMapping(value = "/test")
    public String test(Model model) 
        return "welcome to data api!";
    

5)测试结果
(1)身份认证

(2)数据获取

参考文章:
REST API 安全设计指南
JSON Web Token 入门教程

以上是关于SpringBoot采用jwt作为REST API安全机制的应用示例的主要内容,如果未能解决你的问题,请参考以下文章

使用 Spring Boot 和 JWT 保护 REST Api

使用 jwt 令牌的 Django API Rest Framework 和 Angular 7 身份验证

使用JWT或OAuthv2保护REST + AngularJS

JSON:带有 django-rest-framework-json-api 和 JWT 的 API

JWT 令牌作为 API 中用户详细信息的来源?

REST API 发送 JWT