Spring Boot REST 风格 API 接口 JWT Token 认证(简易版)




1 摘要

接口的权限认证能够有效保护接口不被滥用。常用的REST风格接口权限认证方式有签名校验和基于Auth2.0的Token校验方式。本文将介绍基于 JWT 实现的接口 Token 认证解决方案1.0。

2 需求分析

接口认证需求:

  • 1 能够有选择地过滤没有权限(Token)的请求
  • 2 Token 具有时效性
  • 3 如果用户连续操作,Token 能够自动刷新(自动延长有效期,eg: 加入 Token 有效期为 10 小时,若用户在 10 小时以内有操作,则在时间到达 10 时,Token 能够自动刷新,而不是失效,从而影响用户体验)

解决思路:

  • 1 使用过滤器,可针对需要的接口进行权限认证,对于不需要校验的接口进行放行
  • 2 使用 JWT 设置 Token 有效期
  • 3 用户每次操作都返回最新的 Token ,用户的新操作都基于上一次请求返回的 Token(Token 放在 Response Header 中,前端自行获取)
  • 4 对 Token 设置刷新时间(刷新时间 < 有效时间),当 Token 的时间小于刷新时间时, Token 不用更新,可连续使用;当 Token 时间超过刷新时间,但是在有效时间以内时,刷新 Token ,刷新 Token 的有效期从刷新时间开始算起,往后延长有效时间,返回用户最新 Token,当 Token 操作时间在有效时间以外时,提示 Token 失效。

3 核心依赖

../pom.xml
../demo-common/pom.xml
            <!-- JWT -->
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>${auth.jwt.version}</version>
            </dependency>

其中 ${auth.jwt.version} 版本为 3.8.3

4 核心代码

4.1 JWT Token 生成与校验工具类
../demo-common/src/main/java/com/ljq/demo/springboot/common/util/JwtUtil.java
package com.ljq.demo.springboot.common.util;

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.JWTDecodeException;

import java.io.Serializable;
import java.util.Date;

/**
 * @Description: JWT 工具类
 * @Author: junqiang.lu
 * @Date: 2019/12/3
 */
public class JwtUtil implements Serializable {

    private static final long serialVersionUID = -9101115541530000111L;

    /**
     * 默认秘钥
     */
    private final static String DEFAULT_SECRET = "TS2XIUYNOKJZZDXD8YA9JJH5PM1IAHXPYCX7Q3JO";

    private JwtUtil(){

    }

    /**
     * 加密
     *
     * @param key
     * @param value
     * @return
     * @throws JWTCreationException
     */
    public static String encode(String key, String value) throws Exception {
        return encode(key, value, 0);
    }

    /**
     * 加密
     *
     * @param key
     * @param value
     * @param expTime
     * @return
     * @throws JWTCreationException
     */
    public static String encode(String key, String value, long expTime) throws Exception {
        return encode(null, key, value, expTime);
    }

    /**
     * 加密
     *
     * @param secret
     * @param key
     * @param value
     * @param expMillis
     * @return
     */
    public static String encode(String secret, String key, String value, long expMillis) throws Exception {
        if (secret == null || secret.length() < 1) {
            secret = DEFAULT_SECRET;
        }
        Date expDate = null;
        if (expMillis > 1) {
            expDate = new Date(System.currentTimeMillis() + expMillis);
        }
        Algorithm algorithm = Algorithm.HMAC256(secret);
        String token = JWT.create()
                .withIssuer("auth0")
                .withClaim(key,value)
                .withExpiresAt(expDate)
                .sign(algorithm);
        return token;
    }

    /**
     * 解密
     *
     * @param key
     * @param encodedToken
     * @return
     * @throws JWTDecodeException
     */
    public static String decode(String key, String encodedToken) throws Exception {
        return decode(null, key, encodedToken);
    }

    /**
     * 解密
     *
     * @param secret
     * @param key
     * @param encodedToken
     * @return
     */
    public static String decode(String secret, String key, String encodedToken) throws Exception {
        if (secret == null || secret.length() < 1) {
            secret = DEFAULT_SECRET;
        }
        Algorithm algorithm = Algorithm.HMAC256(secret);
        JWTVerifier verifier = JWT.require(algorithm)
                .withIssuer("auth0")
                .build();
        return verifier.verify(encodedToken).getClaim(key).asString();
    }


}
4.2 Token 拦截器
../demo-web/src/main/java/com/ljq/demo/springboot/web/acpect/SimpleCORSFilter.java
package com.ljq.demo.springboot.web.acpect;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ResponseCode;
import com.ljq.demo.springboot.common.constant.TokenConst;
import com.ljq.demo.springboot.common.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;

/**
 * @Description: 跨域请求拦截器(简易版)
 * @Author: junqiang.lu
 * @Date: 2019/5/21
 */
@Slf4j
public class SimpleCORSFilter implements Filter {

    /**
     * 不需要 Token 校验的接口
     */
    private final static String[] NO_TOKEN_API_PATHS ={
            "/api/rest/user/save",
            "/api/rest/user/info"
    };

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
        // *表示允许所有域名跨域
        httpResponse.addHeader("Access-Control-Allow-Origin", "*");
        httpResponse.addHeader("Access-Control-Allow-Headers","*");
        // 允许跨域的Http方法
        httpResponse.addHeader("Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,TRACE");
        // 允许浏览器访问 Token 认证响应头
        httpResponse.addHeader("Access-Control-Expose-Headers", TokenConst.TOKEN_HEADERS_FIELD);
        // 默认返回原 Token
        httpResponse.setHeader(TokenConst.TOKEN_HEADERS_FIELD, httpRequest.getHeader(TokenConst.TOKEN_HEADERS_FIELD));
        // 应对探针模式请求(OPTIONS)
        String methodOptions = "OPTIONS";
        if (httpRequest.getMethod().equals(methodOptions)) {
            httpResponse.setStatus(HttpServletResponse.SC_ACCEPTED);
            return;
        }
        /**
         * 校验用户 Token
         */
        boolean flag = !Arrays.asList(NO_TOKEN_API_PATHS).contains(httpRequest.getRequestURI());
        if (flag) {
            ResponseCode responseCode = checkToken(httpRequest, httpResponse);
            if (!Objects.equals(responseCode, ResponseCode.SUCCESS)) {
                log.warn("{}", responseCode);
                httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                httpResponse.setContentType("application/json; charset=utf-8");
                httpResponse.setCharacterEncoding("utf-8");
                PrintWriter writer = httpResponse.getWriter();
                writer.write(new ObjectMapper().writeValueAsString(ApiResult.failure(responseCode)));
                return;
            }
        }

        filterChain.doFilter(httpRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }

    /**
     * 用户 Token 校验
     *
     * @param request
     * @param response
     * @return
     */
    private ResponseCode checkToken(HttpServletRequest request, HttpServletResponse response) {
        try {
            String token = request.getHeader(TokenConst.TOKEN_HEADERS_FIELD);
            if (token == null || token.length() < 1) {
                return ResponseCode.USER_TOKEN_NULL_ERROR;
            }
            String tokenValue = JwtUtil.decode(TokenConst.TOKEN_KEY, token);
            long time = Long.parseLong(tokenValue.substring(tokenValue.indexOf("@") + 1));
            String userPhone = tokenValue.substring(0, tokenValue.indexOf("@"));
            log.info("{}, date: {}, userPhone: {}", tokenValue, new Date(time), userPhone);
            // 校验 Token 有效性
            long subResult = System.currentTimeMillis() - time;
            if (subResult >= TokenConst.TOKEN_EXPIRE_TIME_MILLIS) {
                return ResponseCode.USER_TOKEN_ERROR;
            }
            if (subResult < TokenConst.TOKEN_REFRESH_TIME_MILLIS) {
                return ResponseCode.SUCCESS;
            }
            // 刷新 Token
            String newToken = JwtUtil.encode(TokenConst.TOKEN_KEY,userPhone + "@" + System.currentTimeMillis());
            response.setHeader(TokenConst.TOKEN_HEADERS_FIELD, newToken);
        } catch (Exception e) {
            log.warn("Token 校验失败,{}:{}", e.getClass().getName(), e.getMessage());
            return ResponseCode.USER_TOKEN_ERROR;
        }
        return ResponseCode.SUCCESS;
    }
}

其中 NO_TOKEN_API_PATHS 为一些不需要 Token 校验的接口

4.3 Token 相关的常量
../demo-common/src/main/java/com/ljq/demo/springboot/common/constant/TokenConst.java
package com.ljq.demo.springboot.common.constant;

/**
 * @Description: Token 相关常量
 * @Author: junqiang.lu
 * @Date: 2019/12/3
 */
public class TokenConst {


    /**
     * Token headers 字段
     */
    public static final String TOKEN_HEADERS_FIELD = "Authorization";
    /**
     * token key
     */
    public static final String TOKEN_KEY = "tokenPhone";
    /**
     * Token 刷新时间(单位: 毫秒)
     */
    public static final long TOKEN_REFRESH_TIME_MILLIS = 1000 * 60 * 60 * 2L;
    /**
     * token 有效期(单位: 毫秒)
     */
    public static final long TOKEN_EXPIRE_TIME_MILLIS = 1000 * 60 * 60 * 24 * 30L;


}
4.4 其他相关类
../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ResponseCode.java

5 测试

Token 生成,参考:

com.ljq.demo.springboot.common.util.JwtUtil#encode(java.lang.String, java.lang.String)
5.1 不需要 Token

请求接口:

/api/rest/user/save

请求参数&返回结果:

在这里插入图片描述

5.2 需要 Token,请求成功

请求接口:

/api/rest/user/save2

请求参数&返回结果:

在这里插入图片描述

5.3 需要 Token,请求失败

请求接口:

/api/rest/user/save2

请求参数与返回结果:

在这里插入图片描述

6 参考资料推荐

SpringBoot系列 - 集成JWT实现接口权限认证

JWT(JSON Web Token)自动延长到期时间

基于无状态的token刷新机制

Response返回JSON数据

7 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

  • 5
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
首先,我们需要安装 `Microsoft.AspNet.WebApi` 和 `Microsoft.Owin.Security.Jwt` NuGet 包。 接下来,我们需要在 `WebApiConfig.cs` 文件中配置 Web API 路由: ```csharp public static void Register(HttpConfiguration config) { // 配置路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); // 配置 JWT 认证 ConfigureJwtAuth(config); } ``` 然后,我们需要在 `Web.config` 文件中配置 JWT 令牌的密钥和有效期: ```xml <appSettings> <add key="jwtSecret" value="my_secret_key" /> <add key="jwtExpireDays" value="7" /> </appSettings> ``` 接下来,我们需要创建一个 `JwtAuthManager` 类来管理 JWT 认证: ```csharp using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; public class JwtAuthManager : IJwtAuthManager { private readonly string _jwtSecret; private readonly double _jwtExpireDays; public JwtAuthManager(string jwtSecret, double jwtExpireDays) { _jwtSecret = jwtSecret; _jwtExpireDays = jwtExpireDays; } public string GenerateToken(IEnumerable<Claim> claims) { var key = Encoding.ASCII.GetBytes(_jwtSecret); var jwtToken = new JwtSecurityToken( claims: claims, expires: DateTime.Now.AddDays(_jwtExpireDays), signingCredentials: new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) ); var token = new JwtSecurityTokenHandler().WriteToken(jwtToken); return token; } } ``` 然后,我们需要创建一个 `JwtAuthAttribute` 特性,用于在控制器或操作方法上应用 JWT 认证: ```csharp [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class JwtAuthAttribute : AuthorizeAttribute { public override void OnAuthorization(HttpActionContext actionContext) { try { var token = actionContext.Request.Headers.Authorization.Parameter; var jwtAuthManager = actionContext.ControllerContext.Configuration .DependencyResolver.GetService(typeof(IJwtAuthManager)) as IJwtAuthManager; var principal = jwtAuthManager.ValidateToken(token); Thread.CurrentPrincipal = principal; HttpContext.Current.User = principal; } catch (Exception) { actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized); return; } base.OnAuthorization(actionContext); } } ``` 最后,我们需要在 `ConfigureJwtAuth` 方法中注册依赖项并配置 JWT 认证: ```csharp private static void ConfigureJwtAuth(HttpConfiguration config) { var jwtSecret = ConfigurationManager.AppSettings["jwtSecret"]; var jwtExpireDays = double.Parse(ConfigurationManager.AppSettings["jwtExpireDays"]); var container = new UnityContainer(); container.RegisterType<IJwtAuthManager, JwtAuthManager>( new InjectionConstructor(jwtSecret, jwtExpireDays)); config.DependencyResolver = new UnityResolver(container); config.Filters.Add(new JwtAuthAttribute()); } ``` 现在,我们可以在控制器或操作方法上应用 `JwtAuth` 特性来启用 JWT 认证: ```csharp [RoutePrefix("api/products")] public class ProductsController : ApiController { [HttpGet] [Route("")] [JwtAuth] public IHttpActionResult Get() { // ... } [HttpGet] [Route("{id}")] [JwtAuth] public IHttpActionResult Get(int id) { // ... } [HttpPost] [Route("")] [JwtAuth] public IHttpActionResult Post([FromBody] Product product) { // ... } // ... } ``` 这样,我们就成功地基于 JWT 实现了 Token 签名认证
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值