前言记录前后端分离的系统应用下应用场景————用户信息传递需求缘起照例先看看web系统的一张经典架构图,这张图参考自网络:在 Dubbo 自定义异常,你是怎么处理的? 中已经对该架构做了简单说明,这里不再描述。简单描述下在该架构中用户信息(如userId)的传递方式:现在绝大多数的项目都是前后端分离的开发模式,采用token方式进行用户鉴权:客户端(pc,移动端,平板等)首次登录,服务端签发token,在token中放入用户信息(如userId)等返回给客户端客户端访问服务端接口,需要在头部携带token,跟表单一并提交到服务端服务端在web层统一解析token鉴权,同时取出用户信息(如userId)并继续向底层传递,传到服务层操作业务逻辑服务端在service层取到用户信息(如userId)后,执行相应的业务逻辑操作问题:为什么一定要把用户信息(如userId)藏在token中,服务端再解析token取出?直接登录后向客户端返回用户信息(如userId)不是更方便么?跟用户强相关的信息是相当敏感的,一般用户信息(如userId)不会直接明文暴露给客户端,会带来风险。单体应用下用户信息(如userId)的传递流程什么是单体应用? 简要描述就是web层,service层全部在一个jvm进程中,更通俗的讲就是只有一个项目。登录签发 token看看下面的登录接口伪代码:web层接口: @Loggable(descp = “用户登录”, include = “loginParam”) @PostMapping("/login") public BaseResult<LoginVo> accountLogin(LoginParam loginParam) { return mAccountService.login(loginParam); }service层接口伪代码:public BaseResult<LoginVo> login(LoginParam param) throws BaseException { //1.登录逻辑判断 LoginVo loginVo = handleLogin(param); //2.签发token String subject = userId; String jwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(), subject, “token-server”, BaseConstants.TOKEN_PERIOD_TIME, “”, null, SignatureAlgorithm.HS512); loginVo.setJwt(jwt); return ResultUtil.success(loginVo); }注意到上述伪代码中,签发token时把userId放入客户标识subject中,签发到token中返回给客户端。这里使用的是JJWT生成的token引入依赖: <!–jjwt–> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.8.9</version> </dependency>相关工具类JsonWebTokenUtil:public class JsonWebTokenUtil { //秘钥 public static final String SECRET_KEY = BaseConstant.SECRET_KEY; private static final ObjectMapper MAPPER = new ObjectMapper(); private static CompressionCodecResolver codecResolver = new DefaultCompressionCodecResolver(); //私有化构造 private JsonWebTokenUtil() { } /* * * @Description json web token 签发 * @param id 令牌ID * @param subject 用户标识 * @param issuer 签发人 * @param period 有效时间(秒) * @param roles 访问主张-角色 * @param permissions 访问主张-权限 * @param algorithm 加密算法 * @Return java.lang.String / public static String issueJWT(String id,String subject, String issuer, Long period, String roles, String permissions, SignatureAlgorithm algorithm) { // 当前时间戳 Long currentTimeMillis = System.currentTimeMillis(); // 秘钥 byte[] secreKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY); JwtBuilder jwtBuilder = Jwts.builder(); if (StringUtils.isNotBlank(id)) { jwtBuilder.setId(id); } if (StringUtils.isNotBlank(subject)) { jwtBuilder.setSubject(subject); } if (StringUtils.isNotBlank(issuer)) { jwtBuilder.setIssuer(issuer); } // 设置签发时间 jwtBuilder.setIssuedAt(new Date(currentTimeMillis)); // 设置到期时间 if (null != period) { jwtBuilder.setExpiration(new Date(currentTimeMillis + period1000)); } if (StringUtils.isNotBlank(roles)) { jwtBuilder.claim(“roles”,roles); } if (StringUtils.isNotBlank(permissions)) { jwtBuilder.claim(“perms”,permissions); } // 压缩,可选GZIP jwtBuilder.compressWith(CompressionCodecs.DEFLATE); // 加密设置 jwtBuilder.signWith(algorithm,secreKeyBytes); return jwtBuilder.compact(); } /** * 解析JWT的Payload / public static String parseJwtPayload(String jwt){ Assert.hasText(jwt, “JWT String argument cannot be null or empty.”); String base64UrlEncodedHeader = null; String base64UrlEncodedPayload = null; String base64UrlEncodedDigest = null; int delimiterCount = 0; StringBuilder sb = new StringBuilder(128); for (char c : jwt.toCharArray()) { if (c == ‘.’) { CharSequence tokenSeq = io.jsonwebtoken.lang.Strings.clean(sb); String token = tokenSeq!=null?tokenSeq.toString():null; if (delimiterCount == 0) { base64UrlEncodedHeader = token; } else if (delimiterCount == 1) { base64UrlEncodedPayload = token; } delimiterCount++; sb.setLength(0); } else { sb.append(c); } } if (delimiterCount != 2) { String msg = “JWT strings must contain exactly 2 period characters. Found: " + delimiterCount; throw new MalformedJwtException(msg); } if (sb.length() > 0) { base64UrlEncodedDigest = sb.toString(); } if (base64UrlEncodedPayload == null) { throw new MalformedJwtException(“JWT string ‘” + jwt + “’ is missing a body/payload.”); } // =============== Header ================= Header header = null; CompressionCodec compressionCodec = null; if (base64UrlEncodedHeader != null) { String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader); Map<String, Object> m = readValue(origValue); if (base64UrlEncodedDigest != null) { header = new DefaultJwsHeader(m); } else { header = new DefaultHeader(m); } compressionCodec = codecResolver.resolveCompressionCodec(header); } // =============== Body ================= String payload; if (compressionCodec != null) { byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload)); payload = new String(decompressed, io.jsonwebtoken.lang.Strings.UTF_8); } else { payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload); } return payload; } /* * 验签JWT * * @param jwt json web token / public static JwtAccount parseJwt(String jwt, String appKey) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(appKey)) .parseClaimsJws(jwt) .getBody(); JwtAccount jwtAccount = new JwtAccount(); //令牌ID jwtAccount.setTokenId(claims.getId()); //客户标识 String subject = claims.getSubject(); jwtAccount.setSubject(subject); //用户id jwtAccount.setUserId(subject); //签发者 jwtAccount.setIssuer(claims.getIssuer()); //签发时间 jwtAccount.setIssuedAt(claims.getIssuedAt()); //接收方 jwtAccount.setAudience(claims.getAudience()); //访问主张-角色 jwtAccount.setRoles(claims.get(“roles”, String.class)); //访问主张-权限 jwtAccount.setPerms(claims.get(“perms”, String.class)); return jwtAccount; } public static Map<String, Object> readValue(String val) { try { return MAPPER.readValue(val, Map.class); } catch (IOException e) { throw new MalformedJwtException(“Unable to userpager JSON value: " + val, e); } }}JWT相关实体JwtAccount:@Datapublic class JwtAccount implements Serializable { private static final long serialVersionUID = -895875540581785581L; /* * 令牌id / private String tokenId; /* * 客户标识(用户id) / private String subject; /* * 用户id / private String userId; /* * 签发者(JWT令牌此项有值) / private String issuer; /* * 签发时间 / private Date issuedAt; /* * 接收方(JWT令牌此项有值) / private String audience; /* * 访问主张-角色(JWT令牌此项有值) / private String roles; /* * 访问主张-资源(JWT令牌此项有值) / private String perms; /* * 客户地址 / private String host; public JwtAccount() { }}web层统一鉴权,解析token客户端访问服务端接口,需要在头部携带token,跟表单一并提交到服务端,服务端则在web层新增MVC拦截器统一做处理新增MVC拦截器如下:public class UpmsInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { BaseResult result = null; //获取请求uri String requestURI = request.getRequestURI(); …省略部分逻辑 //获取认证token String jwt = request.getHeader(BaseConstant.AUTHORIZATION); //不传认证token,判断为无效请求 if (StringUtils.isBlank(jwt)) { result = ResultUtil.error(ResultEnum.ERROR_REQUEST); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } //其他请求均需验证token有效性 JwtAccount jwtAccount = null; String payload = null; try { // 解析Payload payload = JsonWebTokenUtil.parseJwtPayload(jwt); //取出payload中字段信息 if (payload.charAt(0) == ‘{’ && payload.charAt(payload.length() - 1) == ‘}’) { Map<String, Object> payloadMap = JsonWebTokenUtil.readValue(payload); //客户标识(userId) String subject = (String) payloadMap.get(“sub”); //查询用户签发秘钥 } //验签token jwtAccount = JsonWebTokenUtil.parseJwt(jwt, JsonWebTokenUtil.SECRET_KEY); } catch (SignatureException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) { //令牌错误 result = ResultUtil.error(ResultEnum.ERROR_JWT); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } catch (ExpiredJwtException e) { //令牌过期 result = ResultUtil.error(ResultEnum.EXPIRED_JWT); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } catch (Exception e) { //解析异常 result = ResultUtil.error(ResultEnum.ERROR_JWT); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } if (null == jwtAccount) { //令牌错误 result = ResultUtil.error(ResultEnum.ERROR_JWT); RequestResponseUtil.responseWrite(JSON.toJSONString(result), response); return false; } //将用户信息放入threadLocal中,线程共享 ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId()); return true; } //…省略部分代码}整个token解析过程已经在代码注释中说明,可以看到解析完token后取出userId,将用户信息放入了threadLocal中,关于threadLocal的用法,本文暂不讨论. //将用户信息放入threadLocal中,线程共享 ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());添加配置使拦截器生效:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns=“http://www.springframework.org/schema/beans" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" …省略部分代码”> <!– web拦截器 –> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path=”/”/> <bean class=“com.easywits.upms.client.interceptor.UpmsInterceptor”/> </mvc:interceptor> </mvc:interceptors> </beans>相关工具代码ThreadLocalUtil:public class ThreadLocalUtil { private ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>(); //new一个实例 private static final ThreadLocalUtil instance = new ThreadLocalUtil(); //私有化构造 private ThreadLocalUtil() { } //获取单例 public static ThreadLocalUtil getInstance() { return instance; } / * 将用户对象绑定到当前线程中,键为userInfoThreadLocal对象,值为userInfo对象 * * @param userInfo / public void bind(UserInfo userInfo) { userInfoThreadLocal.set(userInfo); } / * 将用户数据绑定到当前线程中,键为userInfoThreadLocal对象,值为userInfo对象 * * @param companyId * @param userId / public void bind(String userId) { UserInfo userInfo = new UserInfo(); userInfo.setUserId(userId); bind(userInfo); } /* * 得到绑定的用户对象 * * @return / public UserInfo getUserInfo() { UserInfo userInfo = userInfoThreadLocal.get(); remove(); return userInfo; } /* * 移除绑定的用户对象 */ public void remove() { userInfoThreadLocal.remove(); }}那么在web层和service都可以这样拿到userId: @Loggable(descp = “用户个人资料”, include = “”) @GetMapping(value = “/info”) public BaseResult<UserInfoVo> userInfo() { //拿到用户信息 UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo(); return mUserService.userInfo(); }service层获取userId:public BaseResult<UserInfoVo> userInfo() throws BaseException { //拿到用户信息 UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo(); UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId); return ResultUtil.success(userInfoVo); }分布式应用下(Dubbo)用户信息(如userId)的传递流程分布式应用与单体应用最大的区别就是从单个应用拆分成多个应用,service层与web层分为两个独立的应用,使用rpc调用方式处理业务逻辑。而上述做法中我们将用户信息放入了threadLocal中,是相对单应用进程而言的,假如service层接口在另外一个服务进程中,那么将获取不到。有什么办法能解决跨进程传递用户信息呢?翻看了下Dubbo官方文档,有隐式参数功能:文档很清晰,只需要在web层统一的拦截器中调用如下代码,就能将用户id传到service层RpcContext.getContext().setAttachment(“userId”, xxx);相应地调整web层拦截器代码:public class UpmsInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //…省略部分代码 //将用户信息放入threadLocal中,线程共享 ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId()); //将用户信息隐式透传到服务层 RpcContext.getContext().setAttachment(“userId”, jwtAccount.getUserId()); return true; } //…省略部分代码}那么服务层可以这样获取用户id了:public BaseResult<UserInfoVo> userInfo() throws BaseException { //拿到用户信息 String userId = RpcContext.getContext().getAttachment(“userId”); UserInfoVo userInfoVo = getUserInfoVo(userId); return ResultUtil.success(userInfoVo); }为了便于统一管理,我们可以在service层拦截器中将获取到的userId再放入threadLocal中,service层拦截器可以看看这篇推文:Dubbo自定义日志拦截器public class DubboServiceFilter implements Filter { private static final Logger LOGGER = LoggerFactory.getLogger(DubboServiceFilter.class); @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { //…省略部分逻辑 //获取web层透传过来的用户参数 String userId = RpcContext.getContext().getAttachment(“userId”); //放入全局threadlocal 线程共享 if (StringUtils.isNotBlank(userId)) { ThreadLocalUtil.getInstance().bind(userId); } //执行业务逻辑 返回结果 Result result = invoker.invoke(invocation); //清除 防止内存泄露 ThreadLocalUtil.getInstance().remove(); //…省略部分逻辑 return result; }}这样处理,service层依然可以通过如下代码获取用户信息了:public BaseResult<UserInfoVo> userInfo() throws BaseException { //拿到用户信息 UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo(); UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId); return ResultUtil.success(userInfoVo); }参考文档关于jwt:https://blog.leapoahead.com/2015/09/06/understanding-jwt/关于dubbo:http://dubbo.apache.org/zh-cn/docs/user/demos/attachment.html最后篇幅较长,总结一个较为实用的web应用场景,后续会不定期更新原创文章,欢迎关注公众号 「张少林同学」!