微服务: Token的处理
简介
在 Win10-安装-Redis 和 微服务-SpringBoot-集成-Redis 分别介绍了如何安装和使用 Redis,今天继续结合 Redis,聊聊 token 授权登录的事情。
今天聊的主角是 JWT,聊完 JWT 之后再结合实例实现用户 token 登录。
JWT 介绍
JWT,JSON Web Token 的缩写,基于 RFC 7519 标准。
下面内容来自 jwd.io,如下:
1
| 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
1 2 3 4 5
| <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 并进行相关的校验。核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @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; } String token = request.getHeader(REQUEST_TOKEN_KEY); 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
中进行管理,关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @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/**"); excludePaths.add("/signout/**"); excludePaths.add("/static/**"); excludePaths.add("/assets/**");
registry.addInterceptor(authInterceptor) .addPathPatterns("/**") .excludePathPatterns(excludePaths);
WebMvcConfigurer.super.addInterceptors(registry); } }
|
接下来重点说一下 MSAuthTokenUtil.java
里面如何生成 token 的,MSAuthTokenUtil.java
主要是完成生成、检验、刷新 token 等工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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 登录三个接口,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @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 如下:
待办事项
只有弱者才去争取公平,这句话虽然残忍但很现实~