概述
前言
记录前后端分离的系统应用下应用场景————用户信息传递
需求缘起
照例先看看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
层接口:
1 @Loggable(descp = "用户登录", include = "loginParam")
2 @PostMapping("/login")
3 public BaseResult accountLogin(LoginParam loginParam) {
4 return mAccountService.login(loginParam);
5 }
service
层接口伪代码:
1public BaseResult login(LoginParam param) throws BaseException {
2 //1.登录逻辑判断
3 LoginVo loginVo = handleLogin(param);
4 //2.签发token
5 String subject = userId;
6 String jwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(), subject,
7 "token-server", BaseConstants.TOKEN_PERIOD_TIME, "", null, SignatureAlgorithm.HS512);
8 loginVo.setJwt(jwt);
9 return ResultUtil.success(loginVo);
10 }
注意到上述伪代码中,签发token
时把userId
放入客户标识subject
中,签发到token
中返回给客户端。这里使用的是JJWT
生成的token
引入依赖:
1
2 3 io.jsonwebtoken 4 jjwt 5 0.9.0 6
7 8 com.fasterxml.jackson.core 9 jackson-databind10 2.8.911
相关工具类JsonWebTokenUtil
:
1public class JsonWebTokenUtil {
2 //秘钥
3 public static final String SECRET_KEY = BaseConstant.SECRET_KEY;
4 private static final ObjectMapper MAPPER = new ObjectMapper();
5 private static CompressionCodecResolver codecResolver = new DefaultCompressionCodecResolver();
6
7 //私有化构造
8 private JsonWebTokenUtil() {
9 }
10 /* * 11 * @Description json web token 签发 12 * @param id 令牌ID 13 * @param subject 用户标识 14 * @param issuer 签发人 15 * @param period 有效时间(秒) 16 * @param roles 访问主张-角色 17 * @param permissions 访问主张-权限 18 * @param algorithm 加密算法 19 * @Return java.lang.String 20 */
21 public static String issueJWT(String id,String subject, String issuer, Long period, 22 String roles, String permissions, SignatureAlgorithm algorithm) {
23 // 当前时间戳
24 Long currentTimeMillis = System.currentTimeMillis();
25 // 秘钥
26 byte[] secreKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY);
27 JwtBuilder jwtBuilder = Jwts.builder();
28 if (StringUtils.isNotBlank(id)) {
29 jwtBuilder.setId(id);
30 }
31 if (StringUtils.isNotBlank(subject)) {
32 jwtBuilder.setSubject(subject);
33 }
34 if (StringUtils.isNotBlank(issuer)) {
35 jwtBuilder.setIssuer(issuer);
36 }
37 // 设置签发时间
38 jwtBuilder.setIssuedAt(new Date(currentTimeMillis));
39 // 设置到期时间
40 if (null != period) {
41 jwtBuilder.setExpiration(new Date(currentTimeMillis + period*1000));
42 }
43 if (StringUtils.isNotBlank(roles)) {
44 jwtBuilder.claim("roles",roles);
45 }
46 if (StringUtils.isNotBlank(permissions)) {
47 jwtBuilder.claim("perms",permissions);
48 }
49 // 压缩,可选GZIP
50 jwtBuilder.compressWith(CompressionCodecs.DEFLATE);
51 // 加密设置
52 jwtBuilder.signWith(algorithm,secreKeyBytes);
53
54 return jwtBuilder.compact();
55 }
56
57 /** 58 * 解析JWT的Payload 59 */
60 public static String parseJwtPayload(String jwt){
61 Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
62 String base64UrlEncodedHeader = null;
63 String base64UrlEncodedPayload = null;
64 String base64UrlEncodedDigest = null;
65 int delimiterCount = 0;
66 StringBuilder sb = new StringBuilder(128);
67 for (char c : jwt.toCharArray()) {
68 if (c == '.') {
69 CharSequence tokenSeq = io.jsonwebtoken.lang.Strings.clean(sb);
70 String token = tokenSeq!=null?tokenSeq.toString():null;
71
72 if (delimiterCount == 0) {
73 base64UrlEncodedHeader = token;
74 } else if (delimiterCount == 1) {
75 base64UrlEncodedPayload = token;
76 }
77
78 delimiterCount++;
79 sb.setLength(0);
80 } else {
81 sb.append(c);
82 }
83 }
84 if (delimiterCount != 2) {
85 String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
86 throw new MalformedJwtException(msg);
87 }
88 if (sb.length() > 0) {
89 base64UrlEncodedDigest = sb.toString();
90 }
91 if (base64UrlEncodedPayload == null) {
92 throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
93 }
94 // =============== Header =================
95 Header header = null;
96 CompressionCodec compressionCodec = null;
97 if (base64UrlEncodedHeader != null) {
98 String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
99 Map m = readValue(origValue);100 if (base64UrlEncodedDigest != null) {101 header = new DefaultJwsHeader(m);102 } else {103 header = new DefaultHeader(m);104 }105 compressionCodec = codecResolver.resolveCompressionCodec(header);106 }107 // =============== Body =================108 String payload;109 if (compressionCodec != null) {110 byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));111 payload = new String(decompressed, io.jsonwebtoken.lang.Strings.UTF_8);112 } else {113 payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);114 }115 return payload;116 }117118 /**119 * 验签JWT120 *121 * @param jwt json web token122 */123 public static JwtAccount parseJwt(String jwt, String appKey) throws ExpiredJwtException, UnsupportedJwtException,124 MalformedJwtException, SignatureException, IllegalArgumentException {125 Claims claims = Jwts.parser()126 .setSigningKey(DatatypeConverter.parseBase64Binary(appKey))127 .parseClaimsJws(jwt)128 .getBody();129 JwtAccount jwtAccount = new JwtAccount();130 //令牌ID131 jwtAccount.setTokenId(claims.getId());132 //客户标识133 String subject = claims.getSubject();134 jwtAccount.setSubject(subject);135 //用户id136 jwtAccount.setUserId(subject);137 //签发者138 jwtAccount.setIssuer(claims.getIssuer());139 //签发时间140 jwtAccount.setIssuedAt(claims.getIssuedAt());141 //接收方142 jwtAccount.setAudience(claims.getAudience());143 //访问主张-角色144 jwtAccount.setRoles(claims.get("roles", String.class));145 //访问主张-权限146 jwtAccount.setPerms(claims.get("perms", String.class));147 return jwtAccount;148 }149150 public static Map readValue(String val) {151 try {152 return MAPPER.readValue(val, Map.class);153 } catch (IOException e) {154 throw new MalformedJwtException("Unable to userpager JSON value: " + val, e);155 }156 }157}
JWT
相关实体JwtAccount
:
1@Data
2public class JwtAccount implements Serializable {
3
4 private static final long serialVersionUID = -895875540581785581L;
5
6 /** 7 * 令牌id 8 */
9 private String tokenId;
10
11 /**12 * 客户标识(用户id)13 */
14 private String subject;
15
16 /**17 * 用户id18 */
19 private String userId;
20
21 /**22 * 签发者(JWT令牌此项有值)23 */
24 private String issuer;
25
26 /**27 * 签发时间28 */
29 private Date issuedAt;
30
31 /**32 * 接收方(JWT令牌此项有值)33 */
34 private String audience;
35
36 /**37 * 访问主张-角色(JWT令牌此项有值)38 */
39 private String roles;
40
41 /**42 * 访问主张-资源(JWT令牌此项有值)43 */
44 private String perms;
45
46 /**47 * 客户地址48 */
49 private String host;
50
51 public JwtAccount() {
52
53 }
54}
`web`层统一鉴权,解析`token`
客户端访问服务端接口,需要在头部携带token
,跟表单一并提交到服务端,服务端则在web
层新增MVC
拦截器统一做处理
新增MVC
拦截器如下:
1public class UpmsInterceptor extends HandlerInterceptorAdapter {
2
3 @Override
4 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
5 BaseResult result = null;
6 //获取请求uri
7 String requestURI = request.getRequestURI();
8
9 ...省略部分逻辑
10
11 //获取认证token
12 String jwt = request.getHeader(BaseConstant.AUTHORIZATION);
13 //不传认证token,判断为无效请求
14 if (StringUtils.isBlank(jwt)) {
15 result = ResultUtil.error(ResultEnum.ERROR_REQUEST);
16 RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);
17 return false;
18 }
19 //其他请求均需验证token有效性
20 JwtAccount jwtAccount = null;
21 String payload = null;
22 try {
23 // 解析Payload
24 payload = JsonWebTokenUtil.parseJwtPayload(jwt);
25 //取出payload中字段信息
26 if (payload.charAt(0) == '{'
27 && payload.charAt(payload.length() - 1) == '}') {
28 Map payloadMap = JsonWebTokenUtil.readValue(payload);29 //客户标识(userId)30 String subject = (String) payloadMap.get("sub");3132 //查询用户签发秘钥3334 }35 //验签token36 jwtAccount = JsonWebTokenUtil.parseJwt(jwt, JsonWebTokenUtil.SECRET_KEY);37 } catch (SignatureException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {38 //令牌错误39 result = ResultUtil.error(ResultEnum.ERROR_JWT);40 RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);41 return false;42 } catch (ExpiredJwtException e) {43 //令牌过期44 result = ResultUtil.error(ResultEnum.EXPIRED_JWT);45 RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);46 return false;47 } catch (Exception e) {48 //解析异常49 result = ResultUtil.error(ResultEnum.ERROR_JWT);50 RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);51 return false;52 }53 if (null == jwtAccount) {54 //令牌错误55 result = ResultUtil.error(ResultEnum.ERROR_JWT);56 RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);57 return false;58 }5960 //将用户信息放入threadLocal中,线程共享61 ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());62 return true;63 }6465 //...省略部分代码66}
整个token
解析过程已经在代码注释中说明,可以看到解析完token
后取出userId
,将用户信息放入了threadLocal
中,关于threadLocal
的用法,本文暂不讨论.
1 //将用户信息放入threadLocal中,线程共享
2 ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());
添加配置使拦截器生效:
1<?xml version="1.0" encoding="UTF-8"?>
2"http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 ...省略部分代码"> 5 6 7 8 9 /**"/>10 11 12 1314
15
相关工具代码ThreadLocalUtil
:
1public class ThreadLocalUtil {
2
3 private ThreadLocal userInfoThreadLocal = new ThreadLocal<>(); 4 5 //new一个实例 6 private static final ThreadLocalUtil instance = new ThreadLocalUtil(); 7 8 //私有化构造 9 private ThreadLocalUtil() {10 }1112 //获取单例13 public static ThreadLocalUtil getInstance() {14 return instance;15 }1617 /**18 * 将用户对象绑定到当前线程中,键为userInfoThreadLocal对象,值为userInfo对象19 *20 * @param userInfo21 */22 public void bind(UserInfo userInfo) {23 userInfoThreadLocal.set(userInfo);24 }2526 /**27 * 将用户数据绑定到当前线程中,键为userInfoThreadLocal对象,值为userInfo对象28 *29 * @param companyId30 * @param userId31 */32 public void bind(String userId) {33 UserInfo userInfo = new UserInfo();34 userInfo.setUserId(userId);35 bind(userInfo);36 }3738 /**39 * 得到绑定的用户对象40 *41 * @return42 */43 public UserInfo getUserInfo() {44 UserInfo userInfo = userInfoThreadLocal.get();45 remove();46 return userInfo;47 }4849 /**50 * 移除绑定的用户对象51 */52 public void remove() {53 userInfoThreadLocal.remove();54 }55}
那么在web
层和service
都可以这样拿到userId
:
1 @Loggable(descp = "用户个人资料", include = "")
2 @GetMapping(value = "/info")
3 public BaseResult userInfo() {
4 //拿到用户信息
5 UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
6 return mUserService.userInfo();
7 }
service
层获取userId
:
1public BaseResult userInfo() throws BaseException {
2 //拿到用户信息
3 UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
4 UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId);
5 return ResultUtil.success(userInfoVo);
6 }
分布式应用下(Dubbo)`用户信息(如userId)`的传递流程
分布式应用与单体应用最大的区别就是从单个应用拆分成多个应用,service
层与web
层分为两个独立的应用,使用rpc
调用方式处理业务逻辑。而上述做法中我们将用户信息放入了threadLocal
中,是相对单应用进程而言的,假如service
层接口在另外一个服务进程中,那么将获取不到。
有什么办法能解决跨进程传递用户信息呢?翻看了下Dubbo
官方文档,有隐式参数
功能:
文档很清晰,只需要在web
层统一的拦截器中调用如下代码,就能将用户id
传到service
层
1RpcContext.getContext().setAttachment("userId", xxx);
相应地调整web
层拦截器代码:
1public class UpmsInterceptor extends HandlerInterceptorAdapter {
2
3 @Override
4 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
5 //...省略部分代码
6
7 //将用户信息放入threadLocal中,线程共享
8 ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());
9
10 //将用户信息隐式透传到服务层
11 RpcContext.getContext().setAttachment("userId", jwtAccount.getUserId());
12 return true;
13 }
14
15 //...省略部分代码
16}
那么服务层可以这样获取用户id
了:
1public BaseResult userInfo() throws BaseException {
2 //拿到用户信息
3 String userId = RpcContext.getContext().getAttachment("userId");
4 UserInfoVo userInfoVo = getUserInfoVo(userId);
5 return ResultUtil.success(userInfoVo);
6 }
为了便于统一管理,我们可以在service
层拦截器中将获取到的userId
再放入threadLocal
中,service
层拦截器可以看看这篇推文:Dubbo自定义日志拦截器
1public class DubboServiceFilter implements Filter {
2
3 private static final Logger LOGGER = LoggerFactory.getLogger(DubboServiceFilter.class);
4
5 @Override
6 public Result invoke(Invoker> invoker, Invocation invocation) throws RpcException {
7
8 //...省略部分逻辑
9
10 //获取web层透传过来的用户参数
11 String userId = RpcContext.getContext().getAttachment("userId");
12 //放入全局threadlocal 线程共享
13 if (StringUtils.isNotBlank(userId)) {
14 ThreadLocalUtil.getInstance().bind(userId);
15 }
16 //执行业务逻辑 返回结果
17 Result result = invoker.invoke(invocation);
18 //清除 防止内存泄露
19 ThreadLocalUtil.getInstance().remove();
20
21 //...省略部分逻辑
22 return result;
23 }
24}
这样处理,service
层依然可以通过如下代码获取用户信息了:
1public BaseResult userInfo() throws BaseException {
2 //拿到用户信息
3 UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
4 UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId);
5 return ResultUtil.success(userInfoVo);
6 }
参考文档
关于jwt
:https://blog.leapoahead.com/2015/09/06/understanding-jwt/
关于dubbo
:http://dubbo.apache.org/zh-cn/docs/user/demos/attachment.html
最后
篇幅较长,总结一个较为实用的web
应用场景,后续会不定期更新原创文章,欢迎关注公众号 「张少林同学」!
最后
以上就是和谐裙子为你收集整理的前后端分离 获取用户ip_前后端分离应用——用户信息传递的全部内容,希望文章能够帮你解决前后端分离 获取用户ip_前后端分离应用——用户信息传递所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复