我是靠谱客的博主 和谐裙子,最近开发中收集的这篇文章主要介绍前后端分离 获取用户ip_前后端分离应用——用户信息传递,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

593305812b35402e488bfe967020e22d.png

前言

记录前后端分离的系统应用下应用场景————用户信息传递

需求缘起

照例先看看web系统的一张经典架构图,这张图参考自网络:

de0ae38ec0ba1c34ad98f0dbb93b04d8.png

在 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官方文档,有隐式参数功能:

6a976040907a4db9fb33f5f578b2dc06.png

文档很清晰,只需要在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应用场景,后续会不定期更新原创文章,欢迎关注公众号 「张少林同学」!

dc5662d5a8d2b171ad87919acdb895c6.png

最后

以上就是和谐裙子为你收集整理的前后端分离 获取用户ip_前后端分离应用——用户信息传递的全部内容,希望文章能够帮你解决前后端分离 获取用户ip_前后端分离应用——用户信息传递所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(60)

评论列表共有 0 条评论

立即
投稿
返回
顶部