微服务: 集成 JWT

简介

Win10-安装-Redis微服务-SpringBoot-集成-Redis 分别介绍了如何安装和使用 Redis,今天继续结合 Redis,聊聊 token 授权登录的事情。

今天聊的主角是 JWT,聊完 JWT 之后再结合实例实现用户 token 登录。

JWT 介绍

JWT,JSON Web Token 的缩写,基于 RFC 7519 标准。

下面内容来自 jwd.io,如下:

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

JWT 定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。该信息可以被验证和信任(因为它是数字签名的)。

JWT 可应用于但不仅限于下面的几种场景:

1、跨域认证

JWT 是一种比较流行的跨域认证解决方案,JWT 的诞生并不是解决 CSRF 跨域攻击,而是解决跨域认证的难题。

A 网站和 B 网站是同一家公司的关联服务,现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,这应该如何实现呢?客户端保存 Token,每次请求都发回给服务器即可。

2、授权(Authorization)

用户一旦登录成功后,后续用户的每个请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的 JWT 的一个特性,因为它的开销很小,并且可以轻松地跨域使用。授权,是使用 JWT 的最常见的场景之一。

3、信息交换(Information Exchange)

对于安全的在各方之间传输信息而言,JWT 是一种很好的方式。JWT 可以被签名,例如,用公钥/私钥对,可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,还可以验证内容没有被篡改。

可以参考阮一峰老师的 JSON Web Token 入门教程,更多详细的介绍可以参考 jwd.io 的相关资料。

使用 JWT

Spring Boot 集成 jjwt

本文以集成 https://github.com/jwtk/jjwt 为例。如果你有兴趣也可以试着去使用 https://github.com/auth0/java-jwt,它是 JWT 的另一个 Java 实现。

截止到该文发布,在 maven repository 仓库中 jjwt 最新版本是 0.9.1

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

修改了哪些文件

本次涉及修改和新增的文件如下:

  • 【修改】MSUserSigninService.java:登录服务的接口;
  • 【修改】MSUserSigninServiceImpl.java:登录服务的接口实现;
  • 【修改】MSSigninController.java:登录的Controller;
  • 【新增】MSAuthTokenUtil.java:token工具类;
  • 【新增】MSAuthConfigurer.java:token配置管理;
  • 【新增】MSAuthInterceptor.java:自定义拦截器;

具体的实现步骤为:

  • 写 token 工具类,实现 token 的生成,校验等工作即 MSAuthTokenUtil.java;
  • 写自定义拦截器,即 MSAuthInterceptor.java,该类实现了 HandlerInterceptor 接口;
    • 拦截客户端相关的 API 请求,对相关的接口进行token的校验;
    • 有了统一的拦截器不需要在每个 Controller 或者对应的 Service 中去做 token 的判断;
  • 写自定义拦截器的配置管理类即 MSAuthConfigurer.java,该类实现了 WebMvcConfigurer 接口;
  • 增加 token 登录的 API,并实现 Redis 缓存 token 的逻辑;

实例演练

用户登录完成后,根据 userID 生成 token,将 token 保存到 Redis 中按照 userID 为 key 来进行存储的。

MSAuthInterceptor.java 是自定义的拦截器,在该拦截器中获取请求的 token 并进行相关的校验。核心代码如下:

@Component
public class MSAuthInterceptor implements HandlerInterceptor {
    private static final String REQUEST_TOKEN_KEY = "token";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestMethod = request.getMethod();

        if ("OPTIONS".equalsIgnoreCase(requestMethod)) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }
        // 请求的Header中拿
        String token = request.getHeader(REQUEST_TOKEN_KEY);
        // Header中拿不到token
        if (null == token) {
            String[] tokens = request.getParameterValues("token");
            if (null != tokens && tokens.length > 0) {
                token = tokens[0];
            }
        }

        if (MSAuthTokenUtil.verifyToken(token)) {
            return true;
        }

        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            writer = response.getWriter();
            Map<String, Object> result = new HashMap<>(2);
            result.put("code", 400);
            result.put("msg", "用户令牌token无效");
            result.put("data", null);
            writer.print(result);
        } catch (IOException e) {
			
        } finally {
            if (null != writer) {
                writer.close();
            }
        }

        return false;
    }
}

拦截器的配置在 MSAuthConfigurer.java 中进行管理,关键代码如下:

@Configuration
public class MSAuthConfigurer implements WebMvcConfigurer {

    private MSAuthInterceptor authInterceptor;

    public MSAuthConfigurer(MSAuthInterceptor authInterceptor) {
        this.authInterceptor = authInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 如下路径不做拦截
        List<String> excludePaths = new ArrayList<>();
        excludePaths.add("/signup/**"); //注册
        excludePaths.add("/signin/name/**"); //用户名登录
        excludePaths.add("/signin/get/token/**"); //获取token
        excludePaths.add("/signout/**"); //登出
        excludePaths.add("/static/**");  //静态资源
        excludePaths.add("/assets/**");  //静态资源

        // 除了 excludePaths 外的请求地址都做拦截
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludePaths);

        WebMvcConfigurer.super.addInterceptors(registry);
    }
}

接下来重点说一下 MSAuthTokenUtil.java 里面如何生成 token 的,MSAuthTokenUtil.java 主要是完成生成、检验、刷新 token 等工作。

public static String generateToken(String userID) {
    String token = "";

    Date date = new Date();
    // 过期时间
    Date expireDate = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME);

    token = Jwts.builder().setId(JWTSID)
        .setSubject(SUBJECT)
        .setAudience(AUDIENCE)
        .setIssuedAt(date)
        .setExpiration(expireDate)
        .claim(CLAIMS_USERID, userID)
        .signWith(SignatureAlgorithm.HS256, TOKEN_SECRET)
        .compact();

    log.info("generateToken token: " + token);

    return token;
}

根据用户ID 生成 token,其中 claim(CLAIMS_USERID, userID) 是用于自定义字段的,便于解析 token 时获取相关的信息。

当我们调用用户名+密码登录的时候,会生成对应的 token,然后将该 token 保存到 Redis 中。下次调用 token 登录的接口时,会从 Redis 中取出对应的 token 信息进行校对,校对通过就返回成功,否则返回失败无法登录。

MSSigninController.java 分别实现了获取 token、刷新 token,token 登录三个接口,如下:

@RequestMapping(value = "/get/token", method = RequestMethod.GET)
@ApiOperation(value = "获取token", httpMethod = "GET", notes = "获取登录")
@ApiImplicitParams({
    @ApiImplicitParam(name = "userID", value = "userID", required = true)
})
public MSResponse getToken(@RequestParam(value = "userid") String userID) {
    MSResponse response = userSigninService.fetchUserToken(userID);

    return response;
}

@RequestMapping(value = "/token", method = RequestMethod.GET)
@ApiOperation(value = "Token登录", httpMethod = "GET", notes = "Token登录")
@ApiImplicitParams({
    @ApiImplicitParam(name = "userID", value = "userID", required = true),
    @ApiImplicitParam(name = "token", value = "token", required = true)
})
public MSResponse siginWithToken(@RequestParam(value = "userid") String userID, @RequestParam(value = "token") String token) {
    MSResponse response = userSigninService.signinUsingToken(userID, token);

    return response;
}

@RequestMapping(value = "/refresh/token", method = RequestMethod.GET)
@ApiOperation(value = "刷新Token", httpMethod = "GET", notes = "Token刷新")
@ApiImplicitParams({
    @ApiImplicitParam(name = "token", value = "token", required = true)
})
public MSResponse refreshToken(@RequestParam(value = "token") String token) {
    MSResponse response = userSigninService.refreshUserToken(token);

    return response;
}

为了方便使用了 GET 方式进行网络请求。后续可以改为 POST 请求。

登录逻辑都在 MSUserSigninServiceImpl.java 中,大家可以自行去看源码,这里不再赘述。

API 调用效果

启动 MySQL,启动 Redis,再启动项目即可。

用户登录成功后,调用 /get/token API,如下:
在这里插入图片描述
调用 /token 进行登录的 API,如下:
在这里插入图片描述
调用 refresh/token API 如下:
在这里插入图片描述

待办事项

  • Redis 中设置 token 的过期时间;
  • 调用刷新 token 的 API 后更新 Redis 中 token 的有效时间;
  • 刷新 token、使用 token 登录的API 修改为 POST 方式;
  • Token 的加密,减少 Token 登录的数据库查询次数;

只有弱者才去争取公平,这句话虽然残忍但很现实~
在这里插入图片描述

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页