简介
在 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 登录的数据库查询次数;
只有弱者才去争取公平,这句话虽然残忍但很现实~

45

被折叠的 条评论
为什么被折叠?



