成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

前后端分離應(yīng)用——用戶信息傳遞

PAMPANG / 3488人閱讀

摘要:前言記錄前后端分離的系統(tǒng)應(yīng)用下應(yīng)用場(chǎng)景用戶信息傳遞需求緣起照例先看看系統(tǒng)的一張經(jīng)典架構(gòu)圖,這張圖參考自網(wǎng)絡(luò)在自定義異常,你是怎么處理的中已經(jīng)對(duì)該架構(gòu)做了簡(jiǎn)單說(shuō)明,這里不再描述。

前言

記錄前后端分離的系統(tǒng)應(yīng)用下應(yīng)用場(chǎng)景————用戶信息傳遞

需求緣起

照例先看看web系統(tǒng)的一張經(jīng)典架構(gòu)圖,這張圖參考自網(wǎng)絡(luò):

在?。膗bbo 自定義異常,你是怎么處理的? 中已經(jīng)對(duì)該架構(gòu)做了簡(jiǎn)單說(shuō)明,這里不再描述。

簡(jiǎn)單描述下在該架構(gòu)中用戶信息(如userId)的傳遞方式

現(xiàn)在絕大多數(shù)的項(xiàng)目都是前后端分離的開(kāi)發(fā)模式,采用token方式進(jìn)行用戶鑒權(quán):

客戶端(pc,移動(dòng)端,平板等)首次登錄,服務(wù)端簽發(fā)token,在token中放入用戶信息(如userId)等返回給客戶端

客戶端訪問(wèn)服務(wù)端接口,需要在頭部攜帶token,跟表單一并提交到服務(wù)端

服務(wù)端在web層統(tǒng)一解析token鑒權(quán),同時(shí)取出用戶信息(如userId)并繼續(xù)向底層傳遞,傳到服務(wù)層操作業(yè)務(wù)邏輯

服務(wù)端在service層取到用戶信息(如userId)后,執(zhí)行相應(yīng)的業(yè)務(wù)邏輯操作

問(wèn)題:

為什么一定要把用戶信息(如userId)藏在token中,服務(wù)端再解析token取出?直接登錄后向客戶端返回用戶信息(如userId)不是更方便么?

跟用戶強(qiáng)相關(guān)的信息是相當(dāng)敏感的,一般用戶信息(如userId)不會(huì)直接明文暴露給客戶端,會(huì)帶來(lái)風(fēng)險(xiǎn)。

單體應(yīng)用下用戶信息(如userId)的傳遞流程

什么是單體應(yīng)用? 簡(jiǎn)要描述就是web層,service層全部在一個(gè)jvm進(jìn)程中,更通俗的講就是只有一個(gè)項(xiàng)目。

登錄簽發(fā) token

看看下面的登錄接口偽代碼:

web層接口:

    @Loggable(descp = "用戶登錄", include = "loginParam")
    @PostMapping("/login")
    public BaseResult accountLogin(LoginParam loginParam) {
        return mAccountService.login(loginParam);
    }

service層接口偽代碼:

public BaseResult login(LoginParam param) throws BaseException {
        //1.登錄邏輯判斷
        LoginVo loginVo = handleLogin(param);
        //2.簽發(fā)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);
    }

注意到上述偽代碼中,簽發(fā)token時(shí)把userId放入客戶標(biāo)識(shí)subject中,簽發(fā)到token中返回給客戶端。這里使用的是JJWT生成的token

引入依賴:

        
        
            io.jsonwebtoken
            jjwt
            0.9.0
        
        
            com.fasterxml.jackson.core
            jackson-databind
            2.8.9
        

相關(guān)工具類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();
    
    //私有化構(gòu)造
    private JsonWebTokenUtil() {
    }
    /* *
     * @Description  json web token 簽發(fā)
     * @param id 令牌ID
     * @param subject 用戶標(biāo)識(shí)
     * @param issuer 簽發(fā)人
     * @param period 有效時(shí)間(秒)
     * @param roles 訪問(wèn)主張-角色
     * @param permissions 訪問(wèn)主張-權(quán)限
     * @param algorithm 加密算法
     * @Return java.lang.String
     */
    public static String issueJWT(String id,String subject, String issuer, Long period,
                                  String roles, String permissions, SignatureAlgorithm algorithm) {
        // 當(dāng)前時(shí)間戳
        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);
        }
        // 設(shè)置簽發(fā)時(shí)間
        jwtBuilder.setIssuedAt(new Date(currentTimeMillis));
        // 設(shè)置到期時(shí)間
        if (null != period) {
            jwtBuilder.setExpiration(new Date(currentTimeMillis + period*1000));
        }
        if (StringUtils.isNotBlank(roles)) {
            jwtBuilder.claim("roles",roles);
        }
        if (StringUtils.isNotBlank(permissions)) {
            jwtBuilder.claim("perms",permissions);
        }
        // 壓縮,可選GZIP
        jwtBuilder.compressWith(CompressionCodecs.DEFLATE);
        // 加密設(shè)置
        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 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;
    }

    /**
     * 驗(yàn)簽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());
        //客戶標(biāo)識(shí)
        String subject = claims.getSubject();
        jwtAccount.setSubject(subject);
        //用戶id
        jwtAccount.setUserId(subject);
        //簽發(fā)者
        jwtAccount.setIssuer(claims.getIssuer());
        //簽發(fā)時(shí)間
        jwtAccount.setIssuedAt(claims.getIssuedAt());
        //接收方
        jwtAccount.setAudience(claims.getAudience());
        //訪問(wèn)主張-角色
        jwtAccount.setRoles(claims.get("roles", String.class));
        //訪問(wèn)主張-權(quán)限
        jwtAccount.setPerms(claims.get("perms", String.class));
        return jwtAccount;
    }
    
     public static Map readValue(String val) {
        try {
            return MAPPER.readValue(val, Map.class);
        } catch (IOException e) {
            throw new MalformedJwtException("Unable to userpager JSON value: " + val, e);
        }
    }
}

JWT相關(guān)實(shí)體JwtAccount

@Data
public class JwtAccount implements Serializable {

    private static final long serialVersionUID = -895875540581785581L;

    /**
     * 令牌id
     */
    private String tokenId;

    /**
     * 客戶標(biāo)識(shí)(用戶id)
     */
    private String subject;

    /**
     * 用戶id
     */
    private String userId;

    /**
     * 簽發(fā)者(JWT令牌此項(xiàng)有值)
     */
    private String issuer;

    /**
     * 簽發(fā)時(shí)間
     */
    private Date issuedAt;

    /**
     * 接收方(JWT令牌此項(xiàng)有值)
     */
    private String audience;

    /**
     * 訪問(wèn)主張-角色(JWT令牌此項(xiàng)有值)
     */
    private String roles;

    /**
     * 訪問(wèn)主張-資源(JWT令牌此項(xiàng)有值)
     */
    private String perms;

    /**
     * 客戶地址
     */
    private String host;

    public JwtAccount() {

    }
}
web層統(tǒng)一鑒權(quán),解析token

客戶端訪問(wèn)服務(wù)端接口,需要在頭部攜帶token,跟表單一并提交到服務(wù)端,服務(wù)端則在web層新增MVC攔截器統(tǒng)一做處理

新增MVC攔截器如下:

public class UpmsInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        BaseResult result = null;
        //獲取請(qǐng)求uri
        String requestURI = request.getRequestURI();
        
        ...省略部分邏輯

        //獲取認(rèn)證token
        String jwt = request.getHeader(BaseConstant.AUTHORIZATION);
        //不傳認(rèn)證token,判斷為無(wú)效請(qǐng)求
        if (StringUtils.isBlank(jwt)) {
            result = ResultUtil.error(ResultEnum.ERROR_REQUEST);
            RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);
            return false;
        }
        //其他請(qǐng)求均需驗(yàn)證token有效性
        JwtAccount jwtAccount = null;
        String payload = null;
        try {
            // 解析Payload
            payload = JsonWebTokenUtil.parseJwtPayload(jwt);
            //取出payload中字段信息
            if (payload.charAt(0) == "{"
                    && payload.charAt(payload.length() - 1) == "}") {
                Map payloadMap = JsonWebTokenUtil.readValue(payload);
                //客戶標(biāo)識(shí)(userId)
                String subject = (String) payloadMap.get("sub");

                //查詢用戶簽發(fā)秘鑰

            }
            //驗(yàn)簽token
            jwtAccount = JsonWebTokenUtil.parseJwt(jwt, JsonWebTokenUtil.SECRET_KEY);
        } catch (SignatureException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
            //令牌錯(cuò)誤
            result = ResultUtil.error(ResultEnum.ERROR_JWT);
            RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);
            return false;
        } catch (ExpiredJwtException e) {
            //令牌過(guò)期
            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) {
            //令牌錯(cuò)誤
            result = ResultUtil.error(ResultEnum.ERROR_JWT);
            RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);
            return false;
        }

        //將用戶信息放入threadLocal中,線程共享
        ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());
        return true;
    }
    
    //...省略部分代碼
}

整個(gè)token解析過(guò)程已經(jīng)在代碼注釋中說(shuō)明,可以看到解析完token后取出userId,將用戶信息放入了threadLocal中,關(guān)于threadLocal的用法,本文暫不討論.

    //將用戶信息放入threadLocal中,線程共享
    ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());

添加配置使攔截器生效:



       
    
    
        
            
            
        
    
    

相關(guān)工具代碼ThreadLocalUtil

public class ThreadLocalUtil {

    private ThreadLocal userInfoThreadLocal = new ThreadLocal<>();
    
    //new一個(gè)實(shí)例
    private static final ThreadLocalUtil instance = new ThreadLocalUtil();
    
    //私有化構(gòu)造
    private ThreadLocalUtil() {
    }
    
    //獲取單例
    public static ThreadLocalUtil getInstance() {
        return instance;
    }

    /**
     * 將用戶對(duì)象綁定到當(dāng)前線程中,鍵為userInfoThreadLocal對(duì)象,值為userInfo對(duì)象
     *
     * @param userInfo
     */
    public void bind(UserInfo userInfo) {
        userInfoThreadLocal.set(userInfo);
    }

    /**
     * 將用戶數(shù)據(jù)綁定到當(dāng)前線程中,鍵為userInfoThreadLocal對(duì)象,值為userInfo對(duì)象
     *
     * @param companyId
     * @param userId
     */
    public void bind(String userId) {
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(userId);
        bind(userInfo);
    }

    /**
     * 得到綁定的用戶對(duì)象
     *
     * @return
     */
    public UserInfo getUserInfo() {
        UserInfo userInfo = userInfoThreadLocal.get();
        remove();
        return userInfo;
    }

    /**
     * 移除綁定的用戶對(duì)象
     */
    public void remove() {
        userInfoThreadLocal.remove();
    }
}

那么在web層和service都可以這樣拿到userId

    @Loggable(descp = "用戶個(gè)人資料", include = "")
    @GetMapping(value = "/info")
    public BaseResult userInfo() {
        //拿到用戶信息
        UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
        return mUserService.userInfo();
    }

service層獲取userId

public BaseResult userInfo() throws BaseException {
        //拿到用戶信息
        UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
        UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId);
        return ResultUtil.success(userInfoVo);
    }
分布式應(yīng)用下(Dubbo)用戶信息(如userId)的傳遞流程

分布式應(yīng)用與單體應(yīng)用最大的區(qū)別就是從單個(gè)應(yīng)用拆分成多個(gè)應(yīng)用,service層與web層分為兩個(gè)獨(dú)立的應(yīng)用,使用rpc調(diào)用方式處理業(yè)務(wù)邏輯。而上述做法中我們將用戶信息放入了threadLocal中,是相對(duì)單應(yīng)用進(jìn)程而言的,假如service層接口在另外一個(gè)服務(wù)進(jìn)程中,那么將獲取不到。

有什么辦法能解決跨進(jìn)程傳遞用戶信息呢?翻看了下Dubbo官方文檔,有隱式參數(shù)功能:

文檔很清晰,只需要在web層統(tǒng)一的攔截器中調(diào)用如下代碼,就能將用戶id傳到service

RpcContext.getContext().setAttachment("userId", xxx);

相應(yīng)地調(diào)整web層攔截器代碼:

public class UpmsInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //...省略部分代碼
        
        //將用戶信息放入threadLocal中,線程共享
        ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());
        
        //將用戶信息隱式透?jìng)鞯椒?wù)層
        RpcContext.getContext().setAttachment("userId", jwtAccount.getUserId());
        return true;
    }
    
    //...省略部分代碼
}

那么服務(wù)層可以這樣獲取用戶id了:

public BaseResult userInfo() throws BaseException {
        //拿到用戶信息
        String userId = RpcContext.getContext().getAttachment("userId");
        UserInfoVo userInfoVo = getUserInfoVo(userId);
        return ResultUtil.success(userInfoVo);
    }

為了便于統(tǒng)一管理,我們可以在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層透?jìng)鬟^(guò)來(lái)的用戶參數(shù)
        String userId = RpcContext.getContext().getAttachment("userId");
        //放入全局threadlocal 線程共享
        if (StringUtils.isNotBlank(userId)) {
            ThreadLocalUtil.getInstance().bind(userId);
        }
        //執(zhí)行業(yè)務(wù)邏輯 返回結(jié)果
        Result result = invoker.invoke(invocation);
        //清除 防止內(nèi)存泄露
        ThreadLocalUtil.getInstance().remove();
        
        //...省略部分邏輯
        return result;
    }
}

這樣處理,service層依然可以通過(guò)如下代碼獲取用戶信息了:

public BaseResult userInfo() throws BaseException {
        //拿到用戶信息
        UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
        UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId);
        return ResultUtil.success(userInfoVo);
    }
參考文檔

關(guān)于jwt:https://blog.leapoahead.com/2015/09/06/understanding-jwt/

關(guān)于dubbo:http://dubbo.apache.org/zh-cn/docs/user/demos/attachment.html

最后

篇幅較長(zhǎng),總結(jié)一個(gè)較為實(shí)用的web應(yīng)用場(chǎng)景,后續(xù)會(huì)不定期更新原創(chuàng)文章,歡迎關(guān)注公眾號(hào) 「張少林同學(xué)」!

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/72901.html

相關(guān)文章

  • 前后分離——token超時(shí)刷新策略

    摘要:實(shí)現(xiàn)目標(biāo)延長(zhǎng)過(guò)期時(shí)間活躍用戶在過(guò)期時(shí),在用戶無(wú)感知的情況下動(dòng)態(tài)刷新,做到一直在線狀態(tài)不活躍用戶在過(guò)期時(shí),直接定向到登錄頁(yè)登錄返回字段如何簽發(fā),請(qǐng)看上一篇推文,這里不做過(guò)多介紹。如果你有更好的做法,歡迎留言告知我,謝謝啦。 前言 記錄一下前后端分離下————token超時(shí)刷新策略! 需求場(chǎng)景 昨天發(fā)了一篇記錄 前后端分離應(yīng)用——用戶信息傳遞 中介紹了token認(rèn)證機(jī)制,跟幾位群友討論了...

    hatlonely 評(píng)論0 收藏0
  • ajax基礎(chǔ)知識(shí)

    摘要:各瀏覽器都有自己的關(guān)于最大長(zhǎng)度的限制谷歌火狐超過(guò)限制長(zhǎng)度的部分,瀏覽器會(huì)自動(dòng)截取掉,導(dǎo)致傳遞給服務(wù)器的數(shù)據(jù)缺失。 AJAX基礎(chǔ)知識(shí)及核心原理解讀 AJAX基礎(chǔ)知識(shí) 什么是AJAX?async javascript and xml,異步的JS和XML xml:可擴(kuò)展的標(biāo)記語(yǔ)言 作用是用來(lái)存儲(chǔ)數(shù)據(jù)的(通過(guò)自己擴(kuò)展的標(biāo)記名稱清晰的展示出數(shù)據(jù)結(jié)構(gòu))ajax之所以稱為異步的js和xml,主要原因...

    wangshijun 評(píng)論0 收藏0
  • 前后分離架構(gòu)下CSRF防御機(jī)制

    摘要:延伸這里再順便提一下,新架構(gòu)下的防御。不過(guò),還有一點(diǎn)值得一提前后端分離框架下,路由由控制我自己要獲取的后端參數(shù)和需要用在業(yè)務(wù)邏輯的參數(shù),在主觀上前端同學(xué)更好把握一些。 原文: http://feclub.cn/post/content... 背景 1、什么是CSRF攻擊? 這里不再介紹CSRF,已經(jīng)了解CSRF原理的同學(xué)可以直接跳到:3、前后端分離下有何不同?。 不太了解的同學(xué)可以看這...

    Moxmi 評(píng)論0 收藏0
  • 使用 vue2.0 開(kāi)發(fā)微信公眾號(hào)下前后分離的SPA站點(diǎn)的填坑之旅

    摘要:目前正在寫(xiě)一個(gè)微信公眾號(hào)的小項(xiàng)目,記錄一下遇到的問(wèn)題和解決方法主要是前端。前端提交時(shí)使用,在后端再取出對(duì)應(yīng)的微信支付看了下文檔,以前是需要用喚起支付,而現(xiàn)在則是把微信內(nèi)置到了微信的瀏覽器中。 目前正在寫(xiě)一個(gè)微信公眾號(hào)的小項(xiàng)目,記錄一下遇到的問(wèn)題和解決方法(主要是前端)。內(nèi)容持續(xù)更新中~ 主要實(shí)現(xiàn) 前后端分離前端為 SPA 單頁(yè)面使用微信的JSSDK微信支付 技術(shù)方案 后端使用 php ...

    afishhhhh 評(píng)論0 收藏0
  • 使用 vue2.0 開(kāi)發(fā)微信公眾號(hào)下前后分離的SPA站點(diǎn)的填坑之旅

    摘要:目前正在寫(xiě)一個(gè)微信公眾號(hào)的小項(xiàng)目,記錄一下遇到的問(wèn)題和解決方法主要是前端。前端提交時(shí)使用,在后端再取出對(duì)應(yīng)的微信支付看了下文檔,以前是需要用喚起支付,而現(xiàn)在則是把微信內(nèi)置到了微信的瀏覽器中。 目前正在寫(xiě)一個(gè)微信公眾號(hào)的小項(xiàng)目,記錄一下遇到的問(wèn)題和解決方法(主要是前端)。內(nèi)容持續(xù)更新中~ 主要實(shí)現(xiàn) 前后端分離前端為 SPA 單頁(yè)面使用微信的JSSDK微信支付 技術(shù)方案 后端使用 php ...

    Taonce 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

PAMPANG

|高級(jí)講師

TA的文章

閱讀更多
最新活動(dòng)
閱讀需要支付1元查看
<