登录校验功能是每个项目都必须的功能,常见的登陆校验方式有JWT和session.
JWT的优点是无状态,缺点很多,明文传输,无法提前终止,字段过长。所以我们采用JWT加session的方式。JWT中只存放随机生成的token,再通过token去redis中找用户信息。当然也可以直接传token,就是安全性比放JWT中差一点。
public String saveUserToken(Integer userId, Integer platform, String ip,
Boolean autoLogin, Boolean checkUrl) {
UserToken userToken = new UserToken();
String token = UUIDUtils.getUUID();
userToken.setToken(token);
userToken.setUserId(userId);
userToken.setLoginTime(LocalDateTime.now());
userToken.setIp(ip);
userToken.setPlatform(platform);
userToken.setAutoLogin(autoLogin);
userToken.setCheckUrl(checkUrl);
//保存token与用户对应关系,便于后面自动登录
save(userToken);
User user = userService.getById(userId);
//获取用户中不常变动的信息放入redis,节省内存
UserCacheBean userBean = Convert.convert(UserCacheBean.class, user);
userBean.setCheckUrl(checkUrl);
userBean.setToken(token);
//保存用户信息到redis
saveUserInSession(userBean);
//生成JWT
JwtBean jwtBean = new JwtBean();
jwtBean.setToken(token);
token = JWTUtil.createToken(BeanUtil.beanToMap(jwtBean), systemConfig.getJwtPassword().getBytes());
return token;
}
首先,新建一个过滤器来过滤所有请求
@Component
public class SecurityFilter implements GlobalFilter, Ordered {
接着,我们要排除不需要校验的url,比如登录就不需要校验,通常我们会把不需要校验的路径放入public路径下,比如/basic/public/login,就这样配置/basic/public/**,然后使用AntPathMatcher来校验
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
//对于排除的url放行
String excludePath = systemConfig.getExcludePath();
if (StringUtils.isNotEmpty(excludePath)) {
String[] excludePaths = excludePath.split(",");
for (String pattern : excludePaths) {
if (pathMatcher.match(pattern, url)) {
return chain.filter(exchange);
}
}
}
然后从头字段中取出jwt
//从头字段或者请求参数中取出token
String token = exchange.getRequest().getHeaders().getFirst(ConstantKey.TOKEN_HEADER);
if (StringUtils.isEmpty(token)) {
token = exchange.getRequest().getQueryParams().getFirst(ConstantKey.TOKEN_HEADER);
}
接下来校验JWT的有效性
ServerHttpResponse resp = exchange.getResponse();
if (StringUtils.isEmpty(token)) {
return printMsg(Result.UNAUTHORIZED, resp, "请登录");
}
//校验jwt签名有效性
if (!JWTUtil.verify(token, systemConfig.getJwtPassword().getBytes())) {
return printMsg(Result.UNAUTHORIZED, resp, "token签名错误");
}
如果JWT没问题则从中取出token,然后去redis中找用户信息
//解析jwt取出标识redis的token
final JWT jwt = JWTUtil.parseToken(token);
JwtBean jwtBean = JsonUtils.parse(jwt.getPayload().toString(), JwtBean.class);
token = jwtBean.getToken();
//从session中取user
UserCacheBean simpleUser = redisSessionService.getUser(token);
if (simpleUser == null) {
//自动登录
Result result = basicPublicService.autoLogin(token);
if (result.getData() == null) {
return printMsg(Result.UNAUTHORIZED, resp, "token已过期");
}
simpleUser = result.getData();
}
如果正常找到用户信息,则把用户信息放入token,这样微服务就能直接使用了
String authUserVo = JsonUtils.serialize(simpleUser);
ServerHttpRequest newHttpRequest = FilterRequestResponseUtil.getNewHttpRequest(exchange.getRequest()
, FilterRequestResponseUtil.getNewHttpHeadersConsumer(ConstantKey.TOKEN_HEADER, authUserVo));
return chain.filter(exchange.mutate()
.request(newHttpRequest).build());
这是操作gateway输入输出的工具类
public final class FilterRequestResponseUtil {
public static ServerHttpRequest getNewHttpRequest(ServerHttpRequest httpRequest
, Consumer httpHeadersConsumer, Flux dataBufferFlux) {
ServerHttpRequest newHttpRequest = httpRequest.mutate()
.headers(httpHeadersConsumer)
.build();
return new ServerHttpRequestDecorator(newHttpRequest) {
@Override
public Flux getBody() {
return dataBufferFlux;
}
};
}
public static ServerHttpRequest getNewHttpRequest(ServerHttpRequest httpRequest
, Consumer httpHeadersConsumer) {
return httpRequest.mutate()
.headers(httpHeadersConsumer)
.build();
}
public static Consumer getNewHttpHeadersConsumer(String headerName, String headerVal) {
Consumer consumer = headers -> {
headers.set(headerName, headerVal);
};
return consumer;
}
/**
* 认证错误输出
*
* @param resp 响应对象
* @param message 错误信息
* @return
*/
public static Mono printMsg(int status, ServerHttpResponse resp, String message) {
resp.setStatusCode(HttpStatus.OK);
resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
Result result = new Result();
result.setStatus(status);
result.setMessage(message);
String returnStr = JsonUtils.serialize(result);
DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
return resp.writeWith(Flux.just(buffer));
}
}
微服务的校验就比较简单了,由于微服务是处于内网的,可以明文传输,拿到用户信息放入ThreadLocal直接用就行了
public class SecurityInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader(ConstantKey.TOKEN_HEADER);
if (StringUtils.isEmpty(token)) {
token = request.getParameter(ConstantKey.TOKEN_HEADER);
}
if (StringUtils.isEmpty(token)) {
AjaxUtils.printMsg(Result.UNAUTHORIZED, response, "请登录");
return false;
}
//从头字段中取user
UserCacheBean simpleUser = JsonUtils.parse(token, UserCacheBean.class);
if (simpleUser == null || simpleUser.getId() == null) {
AjaxUtils.printMsg(Result.UNAUTHORIZED, response, "无效的token");
return false;
}
UserThreadLocal.set(simpleUser);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserThreadLocal.remove();
}
}