国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

api權(quán)限管理系統(tǒng)與前后端分離實(shí)踐

tianlai / 1451人閱讀

摘要:自己在前后端分離上的實(shí)踐要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開(kāi)的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)管理,這次實(shí)踐包含兩個(gè)模塊基于搭建的權(quán)限管理系統(tǒng)后臺(tái)編寫(xiě)的前端管理。

自己在前后端分離上的實(shí)踐

要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開(kāi)的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)restful api管理,這次實(shí)踐包含兩個(gè)模塊,基于springBoot + shiro搭建的權(quán)限管理系統(tǒng)后臺(tái)bootshiro, angular5 + typeScript編寫(xiě)的前端管理usthe。(ps:考慮到我幼小的心靈和水平,大神誤噴啊^_^~)

項(xiàng)目的基礎(chǔ)框架設(shè)計(jì):

總的長(zhǎng)這樣~:

前端usthe

基于angular5 + angular-cli + typeScript + rxjs + bootstrap + adminLTE,踐行angular最佳實(shí)踐。
過(guò)程中node,webpack等有用到過(guò),但我不熟。。。

后端bootshiro

基于springboot + apache shiro + mybatis框架,restful風(fēng)格api,自定義狀態(tài)碼,json-web-token,druid數(shù)據(jù)庫(kù)連接池,swagger文檔生成,redis存儲(chǔ)refreshtoken和動(dòng)態(tài)秘鑰,maven,MD5單向加密和AES雙向等。。。

gate -nginx

這個(gè)nginx作為反向代理服務(wù)器,解決了跨域請(qǐng)求的問(wèn)題。另一個(gè)nginx作為angular應(yīng)用服務(wù)器,tomcat作為bootshiro的服務(wù)器。

反向代理的nginx.conf見(jiàn): conf

持續(xù)集成

流程長(zhǎng)這樣~

詳細(xì)實(shí)現(xiàn)技術(shù)見(jiàn)另一篇: docker學(xué)習(xí)

一些實(shí)現(xiàn)細(xì)節(jié)方案

對(duì)加密認(rèn)證簽發(fā),api動(dòng)態(tài)權(quán)限,token過(guò)期刷新,前后端交互等等實(shí)現(xiàn)的細(xì)節(jié),慢慢更。

密碼動(dòng)態(tài)加密解密

在用戶密碼登錄認(rèn)證中,明文傳輸用戶輸入的密碼是不可取的。在沒(méi)有用https的情況下,這里需要對(duì)用戶密碼加密傳輸,保證即使密碼泄露也不影響。
這里的前后端加密解密下圖:

由于介紹的是動(dòng)態(tài)加密信息方案,這里并不會(huì)涉及之后的JWT簽發(fā)等。
下面是實(shí)現(xiàn)細(xì)節(jié):
angular 前端發(fā)送get動(dòng)態(tài)秘鑰請(qǐng)求后會(huì)對(duì)對(duì)象進(jìn)行監(jiān)聽(tīng),在回調(diào)函數(shù)里獲取后端返回的秘鑰后再進(jìn)行加密處理,之后再發(fā)送登錄請(qǐng)求。在angular我把請(qǐng)求服務(wù)化了,下面的代碼片段會(huì)有點(diǎn)凌亂。

 // 調(diào)用獲取tokenKey秘鑰服務(wù)
    this.loginService.getTokenKey().subscribe(
      data => {
        this.responseData = data;
        if (this.responseData.data.tokenKey !== undefined) {
          const tokenKey = this.responseData.data.tokenKey;
          // 調(diào)用服務(wù),發(fā)送認(rèn)證請(qǐng)求
          this.loginService.login(this.appId, this.password, tokenKey).subscribe(
            data2 => {
              // 認(rèn)證成功返回jwt
              this.responseData = data2;
              if (this.responseData.meta.code === 1003 && this.responseData.data.jwt != null) {
                this.authService.updateAuthorizationToken(this.responseData.data.jwt);
                this.authService.updateUid(this.appId);
                this.authService.updateUser(this.responseData.data.user);
                this.router.navigateByUrl("/index");
              } else {
                this.msg = "用戶名密碼錯(cuò)誤";
                this.isDisabled = true;
              }
            },
            error => {
              console.error(error);
              this.msg = error;
              this.isDisabled = true;
            }
          );
        }
      }
    );
@Injectable()
export class LoginService {
  constructor(private httpUtil: HttpUtil) {
  }
  getTokenKey() {
    const url = "account/login?tokenKey=get";
    // 先向后臺(tái)申請(qǐng)加密tokenKey tokenKey=get
    // const getKeyParam = new HttpParams().set("tokenKey", "get");
    return this.httpUtil.get(url);
  }

  login(appId: string, password: string, tokenKey: string) {
    const url = "account/login";
    tokenKey = CryptoJS.enc.Utf8.parse(tokenKey);
    password = CryptoJS.enc.Utf8.parse(password);
    password = CryptoJS.AES.encrypt(password, tokenKey, {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7}).toString();
    console.log(password);
    const param = new HttpParams().append("appId", appId)
      .append("password", password)
      .append("methodName", "login")
      .append("timestamp", new Date().toUTCString());
    return this.httpUtil.post(url, param);
  }
}

后端是在一個(gè)filter中對(duì)登錄注冊(cè)請(qǐng)求進(jìn)行攔截,判斷其是正常登錄注冊(cè)還是獲取動(dòng)態(tài)加密秘鑰請(qǐng)求,正常認(rèn)證就走shiro,判斷為獲取秘鑰則生成16隨機(jī)碼默認(rèn)AES加密秘鑰為約定16位,小于16位會(huì)報(bào)錯(cuò),將秘鑰以<遠(yuǎn)程IP,秘鑰>的形式存儲(chǔ)到redis,設(shè)置其有效時(shí)間為5秒5秒看自己情況不要太大也不要太短,設(shè)置有效期是為了防止被其他人截取到加密密碼冒充用戶的情況,把風(fēng)險(xiǎn)降更低。

 // 判斷若為獲取登錄注冊(cè)加密動(dòng)態(tài)秘鑰請(qǐng)求
        if (isPasswordTokenGet(request)) {
            //動(dòng)態(tài)生成秘鑰,redis存儲(chǔ)秘鑰供之后秘鑰驗(yàn)證使用,設(shè)置有效期5秒用完即丟棄
            String tokenKey = CommonUtil.getRandomString(16);
            try {
                redisTemplate.opsForValue().set("PASSWORD_TOKEN_KEY_"+request.getRemoteAddr().toUpperCase(),tokenKey,5, TimeUnit.SECONDS);
                // 動(dòng)態(tài)秘鑰response返回給前端
                Message message = new Message();
                message.ok(1000,"issued tokenKey success")
                        .addData("tokenKey",tokenKey);
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);

            }catch (Exception e) {
                LOGGER.warn(e.getMessage(),e);
                // 動(dòng)態(tài)秘鑰response返回給前端
                Message message = new Message();
                message.ok(1000,"issued tokenKey fail");
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);
            }
            return false;
        }
// 創(chuàng)建認(rèn)證信息,其中就有包括獲取redis中對(duì)應(yīng)IP的動(dòng)態(tài)秘鑰
    private AuthenticationToken createPasswordToken(ServletRequest request) {
        Map map = RequestResponseUtil.getRequestParameters(request);
        String appId = map.get("appId");
        String timestamp = map.get("timestamp");
        String password = map.get("password");
        String host = request.getRemoteAddr();
        String tokenKey = redisTemplate.opsForValue().get("PASSWORD_TOKEN_KEY_"+host.toUpperCase());
        return new PasswordToken(appId,password,timestamp,host,tokenKey);
    }
jwt令牌(json web token)

jwt是自包含的令牌,自包含即整個(gè)令牌已經(jīng)包含自己的角色,權(quán)限,用戶信息等各種認(rèn)證一個(gè)用戶的必要信息,這樣就不用后端根據(jù)用戶標(biāo)識(shí)再去數(shù)據(jù)庫(kù)查詢對(duì)應(yīng)用戶的角色權(quán)限等。

jwt包含頭信息,載荷信息,簽名信息三個(gè)部分:

Header //頭信息
{
  "alg": "HS256",  //摘要算法
  "typ": "JWT"     //token類型
}
payload //載荷信息
{
  "sub": "1234567890", //用戶標(biāo)識(shí),subject
  "name": "John Doe",  //用戶名
  "exp": "Mon Nov 13 15:28:41 CST 2018" //有效期
}
verify signature //簽名信息
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
secret
)

詳細(xì)到官網(wǎng)jwt試一波吧,輸入對(duì)應(yīng)信息可以生成JWT

jwt簽發(fā)解析使用的是jjwt,maven導(dǎo)入如下:

        
            io.jsonwebtoken
            jjwt
            0.9.0
        

jwt簽發(fā)解析工具類:

/* *
 * @Author tomsun28
 * @Description 
 * @Date 16:29 2018/3/8
 */
public class JsonWebTokenUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(JsonWebTokenUtil.class);

    public static final String SECRET_KEY = "?::4343fdf4fdf6cvf):";
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static  CompressionCodecResolver CODECRESOLVER = new DefaultCompressionCodecResolver();
    /* *
     * @Description  json web token 簽發(fā)
     * @param id 令牌ID
     * @param subject 用戶ID
     * @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.isEmpty(id)) {
            jwtBuilder.setId(id);
        }
        if (!StringUtils.isEmpty(subject)) {
            jwtBuilder.setSubject(subject);
        }
        if (!StringUtils.isEmpty(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.isEmpty(roles)) {
            jwtBuilder.claim("roles",roles);
        }
        if (!StringUtils.isEmpty(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) {
        Claims claims = Jwts.parser()
                .setSigningKey(DatatypeConverter.parseBase64Binary(appKey))
                .parseClaimsJws(jwt)
                .getBody();
        JwtAccount jwtAccount = new JwtAccount();
        jwtAccount.setTokenId(claims.getId());// 令牌ID
        jwtAccount.setAppId(claims.getSubject());// 客戶標(biāo)識(shí)
        jwtAccount.setIssuer(claims.getIssuer());// 簽發(fā)者
        jwtAccount.setIssuedAt(claims.getIssuedAt());// 簽發(fā)時(shí)間
        jwtAccount.setAudience(claims.getAudience());// 接收方
        jwtAccount.setRoles(claims.get("roles", String.class));// 訪問(wèn)主張-角色
        jwtAccount.setPerms(claims.get("perms", String.class));// 訪問(wèn)主張-權(quán)限
        return jwtAccount;
    }
基于shiro的改造集成真正支持restful請(qǐng)求

首先說(shuō)明設(shè)計(jì)的這個(gè)安全體系是是RBAC(基于角色的權(quán)限訪問(wèn)控制)授權(quán)模型,即用戶--角色--資源,用戶不直接和權(quán)限打交道,角色擁有資源,用戶擁有這個(gè)角色就有權(quán)使用角色所用戶的資源。所有這里沒(méi)有權(quán)限一說(shuō),簽發(fā)jwt里面也就只有用戶所擁有的角色而沒(méi)有權(quán)限。

為啥說(shuō)是真正的restful風(fēng)格集成,雖說(shuō)shiro對(duì)rest不友好但他本身是有支持rest集成的filter--HttpMethodPermissionFilter,這個(gè)shiro rest的 風(fēng)格攔截器,會(huì)自動(dòng)根據(jù)請(qǐng)求方法構(gòu)建權(quán)限字符串( GET=read,POST=create,PUT=update,DELETE=delete)構(gòu)建權(quán)限字符串;eg: /users=rest[user] , 會(huì) 自動(dòng)拼接出user:read,user:create,user:update,user:delete”權(quán)限字符串進(jìn)行權(quán)限匹配(所有都得匹配,isPermittedAll)。

但是這樣感覺(jué)不利于基于jwt的角色的權(quán)限控制,在細(xì)粒度上驗(yàn)權(quán)url(即支持get,post,delete鑒別)就更沒(méi)法了(個(gè)人見(jiàn)解)。打個(gè)比方:我們對(duì)一個(gè)用戶簽發(fā)的jwt寫(xiě)入角色列(role_admin,role_customer)。對(duì)不同request請(qǐng)求:url="api/resource/",httpMethod="GET"url="api/resource",httpMethod="POST",在基于角色-資源的授權(quán)模型中,這兩個(gè)url相同的請(qǐng)求對(duì)HttpMethodPermissionFilter是一種請(qǐng)求,用戶對(duì)應(yīng)的角色擁有的資源url="api/resource",只要請(qǐng)求的url是"api/resource",不論它的請(qǐng)求方式是什么,都會(huì)判定通過(guò)這個(gè)請(qǐng)求,這在restful風(fēng)格的api中肯定是不可取的,對(duì)同一資源有些角色可能只要查詢的權(quán)限而沒(méi)有修改增加的權(quán)限。

可能會(huì)說(shuō)在jwt中再增加權(quán)限列就好了嘛,但是在基于用戶-資源的授權(quán)模型中,雖然能判別是不同的請(qǐng)求,但是太麻煩了,對(duì)每個(gè)資源我們都要設(shè)計(jì)對(duì)應(yīng)的權(quán)限列然后再塞入到j(luò)wt中,對(duì)每個(gè)用戶都要多帶帶授權(quán)資源這也是不可取的。

對(duì)shiro的改造這里自定義了一些規(guī)則:
shiro過(guò)濾器鏈的url=url+"=="+httpMethod
eg:對(duì)于url="api/resource/",httpMethod="GET"的資源,其拼接出來(lái)的過(guò)濾器鏈匹配url=api/resource==GET
這樣對(duì)相同的url而不同的訪問(wèn)方式,會(huì)判定為不同的資源,即資源不再簡(jiǎn)單是url,而是url和httpMethod的組合?;诮巧氖跈?quán)模型中,角色所擁有的資源形式為url+"=="+httpMethod。
這里改變了過(guò)濾器的過(guò)濾匹配url規(guī)則,重寫(xiě)PathMatchingFilterChainResolver的getChain方法,增加對(duì)上述規(guī)則的url的支持。

/* *
 * @Author tomsun28
 * @Description 
 * @Date 21:12 2018/4/20
 */
public class RestPathMatchingFilterChainResolver extends PathMatchingFilterChainResolver {

    private static final Logger LOGGER = LoggerFactory.getLogger(RestPathMatchingFilterChainResolver.class);

    public RestPathMatchingFilterChainResolver() {
        super();
    }

    public RestPathMatchingFilterChainResolver(FilterConfig filterConfig) {
        super(filterConfig);
    }

    /* *
     * @Description 重寫(xiě)filterChain匹配
     * @Param [request, response, originalChain]
     * @Return javax.servlet.FilterChain
     */
    @Override
    public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        FilterChainManager filterChainManager = this.getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        } else {
            String requestURI = this.getPathWithinApplication(request);
            Iterator var6 = filterChainManager.getChainNames().iterator();

            String pathPattern;
            boolean flag = true;
            String[] strings = null;
            do {
                if (!var6.hasNext()) {
                    return null;
                }

                pathPattern = (String)var6.next();

                strings = pathPattern.split("==");
                if (strings.length == 2) {
                    // 分割出url+httpMethod,判斷httpMethod和request請(qǐng)求的method是否一致,不一致直接false
                    if (WebUtils.toHttp(request).getMethod().toUpperCase().equals(strings[1].toUpperCase())) {
                        flag = false;
                    } else {
                        flag = true;
                    }
                } else {
                    flag = false;
                }
                pathPattern = strings[0];
            } while(!this.pathMatches(pathPattern, requestURI) || flag);

            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  Utilizing corresponding filter chain...");
            }
            if (strings.length == 2) {
                pathPattern = pathPattern.concat("==").concat(WebUtils.toHttp(request).getMethod().toUpperCase());
            }

            return filterChainManager.proxy(originalChain, pathPattern);
        }
    }

}

重寫(xiě)PathMatchingFilter的路徑匹配方法pathsMatch(),加入httpMethod支持。

/* *
 * @Author tomsun28
 * @Description 重寫(xiě)過(guò)濾鏈路徑匹配規(guī)則,增加REST風(fēng)格post,get.delete,put..支持
 * @Date 23:37 2018/4/19
 */
public abstract class BPathMatchingFilter extends PathMatchingFilter {

    public BPathMatchingFilter() {

    }
    /* *
     * @Description 重寫(xiě)URL匹配  加入httpMethod支持
     * @Param [path, request]
     * @Return boolean
     */
    @Override
    protected boolean pathsMatch(String path, ServletRequest request) {
        String requestURI = this.getPathWithinApplication(request);
        // path: url==method eg: http://api/menu==GET   需要解析出path中的url和httpMethod
        String[] strings = path.split("==");
        if (strings.length <= 1) {
            // 分割出來(lái)只有URL
            return this.pathsMatch(strings[0], requestURI);
        } else {
            // 分割出url+httpMethod,判斷httpMethod和request請(qǐng)求的method是否一致,不一致直接false
            String httpMethod = WebUtils.toHttp(request).getMethod().toUpperCase();
            return httpMethod.equals(strings[1].toUpperCase()) && this.pathsMatch(strings[0], requestURI);
        }
    }
}

這樣增加httpMethod的改造就完成了,重寫(xiě)ShiroFilterFactoryBean使其使用改造后的chainResolver:RestPathMatchingFilterChainResolver

/* *
 * @Author tomsun28
 * @Description rest支持的shiroFilterFactoryBean
 * @Date 21:35 2018/4/20
 */
public class RestShiroFilterFactoryBean extends ShiroFilterFactoryBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(RestShiroFilterFactoryBean.class);

    public RestShiroFilterFactoryBean() {
        super();
    }

    @Override
    protected AbstractShiroFilter createInstance() throws Exception {
        LOGGER.debug("Creating Shiro Filter instance.");
        SecurityManager securityManager = this.getSecurityManager();
        String msg;
        if (securityManager == null) {
            msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        } else if (!(securityManager instanceof WebSecurityManager)) {
            msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        } else {
            FilterChainManager manager = this.createFilterChainManager();
            RestPathMatchingFilterChainResolver chainResolver = new RestPathMatchingFilterChainResolver();
            chainResolver.setFilterChainManager(manager);
            return new RestShiroFilterFactoryBean.SpringShiroFilter((WebSecurityManager)securityManager, chainResolver);
        }
    }

    private static final class SpringShiroFilter extends AbstractShiroFilter {
        protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
            if (webSecurityManager == null) {
                throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
            } else {
                this.setSecurityManager(webSecurityManager);
                if (resolver != null) {
                    this.setFilterChainResolver(resolver);
                }

            }
        }
    }
}

上面是一些核心的代碼片段,更多請(qǐng)看項(xiàng)目代碼。

對(duì)用戶賬戶登錄注冊(cè)的過(guò)濾filter:PasswordFilter

/* *
 * @Author tomsun28
 * @Description 基于 用戶名密碼 的認(rèn)證過(guò)濾器
 * @Date 20:18 2018/2/10
 */
public class PasswordFilter extends AccessControlFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(PasswordFilter.class);

    private StringRedisTemplate redisTemplate;

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        Subject subject = getSubject(request,response);
        // 如果其已經(jīng)登錄,再此發(fā)送登錄請(qǐng)求
        if(null != subject && subject.isAuthenticated()){
            return true;
        }
        //  拒絕,統(tǒng)一交給 onAccessDenied 處理
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {

        // 判斷若為獲取登錄注冊(cè)加密動(dòng)態(tài)秘鑰請(qǐng)求
        if (isPasswordTokenGet(request)) {
            //動(dòng)態(tài)生成秘鑰,redis存儲(chǔ)秘鑰供之后秘鑰驗(yàn)證使用,設(shè)置有效期5秒用完即丟棄
            String tokenKey = CommonUtil.getRandomString(16);
            try {
                redisTemplate.opsForValue().set("PASSWORD_TOKEN_KEY_"+request.getRemoteAddr().toUpperCase(),tokenKey,5, TimeUnit.SECONDS);
                // 動(dòng)態(tài)秘鑰response返回給前端
                Message message = new Message();
                message.ok(1000,"issued tokenKey success")
                        .addData("tokenKey",tokenKey);
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);

            }catch (Exception e) {
                LOGGER.warn(e.getMessage(),e);
                // 動(dòng)態(tài)秘鑰response返回給前端
                Message message = new Message();
                message.ok(1000,"issued tokenKey fail");
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);
            }
            return false;
        }

        // 判斷是否是登錄請(qǐng)求
        if(isPasswordLoginPost(request)){
            AuthenticationToken authenticationToken = createPasswordToken(request);
            Subject subject = getSubject(request,response);
            try {
                subject.login(authenticationToken);
                //登錄認(rèn)證成功,進(jìn)入請(qǐng)求派發(fā)json web token url資源內(nèi)
                return true;
            }catch (AuthenticationException e) {
                LOGGER.warn(authenticationToken.getPrincipal()+"::"+e.getMessage(),e);
                // 返回response告訴客戶端認(rèn)證失敗
                Message message = new Message().error(1002,"login fail");
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);
                return false;
            }catch (Exception e) {
                LOGGER.error(e.getMessage(),e);
                // 返回response告訴客戶端認(rèn)證失敗
                Message message = new Message().error(1002,"login fail");
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);
                return false;
            }
        }
        // 判斷是否為注冊(cè)請(qǐng)求,若是通過(guò)過(guò)濾鏈進(jìn)入controller注冊(cè)
        if (isAccountRegisterPost(request)) {
            return true;
        }
        // 之后添加對(duì)賬戶的找回等

        // response 告知無(wú)效請(qǐng)求
        Message message = new Message().error(1111,"error request");
        RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);
        return false;
    }

    private boolean isPasswordTokenGet(ServletRequest request) {
//        String tokenKey = request.getParameter("tokenKey");
        String tokenKey = RequestResponseUtil.getParameter(request,"tokenKey");

        return (request instanceof HttpServletRequest)
                && ((HttpServletRequest) request).getMethod().toUpperCase().equals("GET")
                && null != tokenKey && "get".equals(tokenKey);
    }

    private boolean isPasswordLoginPost(ServletRequest request) {
//        String password = request.getParameter("password");
//        String timestamp = request.getParameter("timestamp");
//        String methodName = request.getParameter("methodName");
//        String appId = request.getParameter("appId");
        Map map = RequestResponseUtil.getRequestParameters(request);
        String password = map.get("password");
        String timestamp = map.get("timestamp");
        String methodName = map.get("methodName");
        String appId = map.get("appId");
        return (request instanceof HttpServletRequest)
                && ((HttpServletRequest) request).getMethod().toUpperCase().equals("POST")
                && null != password
                && null != timestamp
                && null != methodName
                && null != appId
                && methodName.equals("login");
    }

    private boolean isAccountRegisterPost(ServletRequest request) {
//        String uid = request.getParameter("uid");
//        String methodName = request.getParameter("methodName");
//        String username = request.getParameter("username");
//        String password = request.getParameter("password");
        Map map = RequestResponseUtil.getRequestParameters(request);
        String uid = map.get("uid");
        String username = map.get("username");
        String methodName = map.get("methodName");
        String password = map.get("password");
        return (request instanceof HttpServletRequest)
                && ((HttpServletRequest) request).getMethod().toUpperCase().equals("POST")
                && null != username
                && null != password
                && null != methodName
                && null != uid
                && methodName.equals("register");
    }

    private AuthenticationToken createPasswordToken(ServletRequest request) {

//        String appId = request.getParameter("appId");
//        String password = request.getParameter("password");
//        String timestamp = request.getParameter("timestamp");
        Map map = RequestResponseUtil.getRequestParameters(request);
        String appId = map.get("appId");
        String timestamp = map.get("timestamp");
        String password = map.get("password");
        String host = request.getRemoteAddr();
        String tokenKey = redisTemplate.opsForValue().get("PASSWORD_TOKEN_KEY_"+host.toUpperCase());
        return new PasswordToken(appId,password,timestamp,host,tokenKey);
    }

    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

}

支持restful風(fēng)格的jwt鑒權(quán)filter:BJwtFilter

/* *
 * @Author tomsun28
 * @Description 支持restful url 的過(guò)濾鏈  JWT json web token 過(guò)濾器,無(wú)狀態(tài)驗(yàn)證
 * @Date 0:04 2018/4/20
 */
public class BJwtFilter extends BPathMatchingFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(BJwtFilter.class);


    private StringRedisTemplate redisTemplate;
    private AccountService accountService;

    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception {
        Subject subject = getSubject(servletRequest,servletResponse);
        // 判斷是否為JWT認(rèn)證請(qǐng)求
        if ((null == subject || !subject.isAuthenticated()) && isJwtSubmission(servletRequest)) {
            AuthenticationToken token = createJwtToken(servletRequest);
            try {
                subject.login(token);
//                return this.checkRoles(subject,mappedValue) && this.checkPerms(subject,mappedValue);
                return this.checkRoles(subject,mappedValue);
            }catch (AuthenticationException e) {
                LOGGER.info(e.getMessage(),e);
                // 如果是JWT過(guò)期
                if (e.getMessage().equals("expiredJwt")) {
                    // 這里初始方案先拋出令牌過(guò)期,之后設(shè)計(jì)為在Redis中查詢當(dāng)前appId對(duì)應(yīng)令牌,其設(shè)置的過(guò)期時(shí)間是JWT的兩倍,此作為JWT的refresh時(shí)間
                    // 當(dāng)JWT的有效時(shí)間過(guò)期后,查詢其refresh時(shí)間,refresh時(shí)間有效即重新派發(fā)新的JWT給客戶端,
                    // refresh也過(guò)期則告知客戶端JWT時(shí)間過(guò)期重新認(rèn)證

                    // 當(dāng)存儲(chǔ)在redis的JWT沒(méi)有過(guò)期,即refresh time 沒(méi)有過(guò)期
                    String appId = WebUtils.toHttp(servletRequest).getHeader("appId");
                    String jwt = WebUtils.toHttp(servletRequest).getHeader("authorization");
                    String refreshJwt = redisTemplate.opsForValue().get("JWT-SESSION-"+appId);
                    if (null != refreshJwt && refreshJwt.equals(jwt)) {
                        // 重新申請(qǐng)新的JWT
                        // 根據(jù)appId獲取其對(duì)應(yīng)所擁有的角色(這里設(shè)計(jì)為角色對(duì)應(yīng)資源,沒(méi)有權(quán)限對(duì)應(yīng)資源)
                        String roles = accountService.loadAccountRole(appId);
                        long refreshPeriodTime = 36000L;  //seconds為單位,10 hours
                        String newJwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(),appId,
                                "token-server",refreshPeriodTime >> 2,roles,null, SignatureAlgorithm.HS512);
                        // 將簽發(fā)的JWT存儲(chǔ)到Redis: {JWT-SESSION-{appID} , jwt}
                        redisTemplate.opsForValue().set("JWT-SESSION-"+appId,newJwt,refreshPeriodTime, TimeUnit.SECONDS);
                        Message message = new Message().ok(1005,"new jwt").addData("jwt",newJwt);
                        RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
                        return false;
                    }else {
                        // jwt時(shí)間失效過(guò)期,jwt refresh time失效 返回jwt過(guò)期客戶端重新登錄
                        Message message = new Message().error(1006,"expired jwt");
                        RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
                        return false;
                    }

                }
                // 其他的判斷為JWT錯(cuò)誤無(wú)效
                Message message = new Message().error(1007,"error Jwt");
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
                return false;

            }catch (Exception e) {
                // 其他錯(cuò)誤
                LOGGER.warn(servletRequest.getRemoteAddr()+"JWT認(rèn)證"+e.getMessage(),e);
                // 告知客戶端JWT錯(cuò)誤1005,需重新登錄申請(qǐng)jwt
                Message message = new Message().error(1007,"error jwt");
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
                return false;
            }
        }else {
            // 請(qǐng)求未攜帶jwt 判斷為無(wú)效請(qǐng)求
            Message message = new Message().error(1111,"error request");
            RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
            return false;
        }
    }

    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        Subject subject = getSubject(servletRequest,servletResponse);

        // 未認(rèn)證的情況
        if (null == subject || !subject.isAuthenticated()) {
            // 告知客戶端JWT認(rèn)證失敗需跳轉(zhuǎn)到登錄頁(yè)面
            Message message = new Message().error(1006,"error jwt");
            RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
        }else {
            //  已經(jīng)認(rèn)證但未授權(quán)的情況
            // 告知客戶端JWT沒(méi)有權(quán)限訪問(wèn)此資源
            Message message = new Message().error(1008,"no permission");
            RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
        }
        // 過(guò)濾鏈終止
        return false;
    }

    private boolean isJwtSubmission(ServletRequest request) {

        String jwt = RequestResponseUtil.getHeader(request,"authorization");
        String appId = RequestResponseUtil.getHeader(request,"appId");
        return (request instanceof HttpServletRequest)
                && !StringUtils.isEmpty(jwt)
                && !StringUtils.isEmpty(appId);
    }

    private AuthenticationToken createJwtToken(ServletRequest request) {

        Map maps = RequestResponseUtil.getRequestHeaders(request);
        String appId = maps.get("appId");
        String ipHost = request.getRemoteAddr();
        String jwt = maps.get("authorization");
        String deviceInfo = maps.get("deviceInfo");

        return new JwtToken(ipHost,deviceInfo,jwt,appId);
    }

    // 驗(yàn)證當(dāng)前用戶是否屬于mappedValue任意一個(gè)角色
    private boolean checkRoles(Subject subject, Object mappedValue){
        String[] rolesArray = (String[]) mappedValue;
        return rolesArray == null || rolesArray.length == 0 || Stream.of(rolesArray).anyMatch(role -> subject.hasRole(role.trim()));
    }

    // 驗(yàn)證當(dāng)前用戶是否擁有mappedValue任意一個(gè)權(quán)限
    private boolean checkPerms(Subject subject, Object mappedValue){
        String[] perms = (String[]) mappedValue;
        boolean isPermitted = true;
        if (perms != null && perms.length > 0) {
            if (perms.length == 1) {
                if (!subject.isPermitted(perms[0])) {
                    isPermitted = false;
                }
            } else {
                if (!subject.isPermittedAll(perms)) {
                    isPermitted = false;
                }
            }
        }
        return isPermitted;
    }

    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }


    public void setAccountService(AccountService accountService) {
        this.accountService = accountService;
    }
}

realm數(shù)據(jù)源,數(shù)據(jù)提供service,匹配matchs,自定義token,spring集成shiro配置等其他詳見(jiàn)項(xiàng)目代碼。
最后項(xiàng)目實(shí)現(xiàn)了基于jwt的動(dòng)態(tài)restful api權(quán)限認(rèn)證。

簽發(fā)的用戶認(rèn)證token超時(shí)刷新策略

對(duì)于登錄的用戶簽發(fā)其對(duì)應(yīng)的jwt,我們?cè)趈wt設(shè)置他的固定有效期時(shí)間,在有效期內(nèi)用戶攜帶jwt訪問(wèn)沒(méi)問(wèn)題,當(dāng)過(guò)有效期后jwt失效,用戶需要重新登錄獲取新的jwt。這個(gè)體驗(yàn)不太好,好的體驗(yàn)應(yīng)該是:活躍的用戶應(yīng)該在無(wú)感知的情況下在jwt失效后獲取到新的jwt,攜帶這個(gè)新的jwt進(jìn)行訪問(wèn),而長(zhǎng)時(shí)間不活躍的用戶應(yīng)該在jwt失效后需要進(jìn)行重新的登錄認(rèn)證。

這里就涉及到了token的超時(shí)刷新問(wèn)題,解決方案看圖:

在簽發(fā)有效期為 t 時(shí)間的jwt后,把jwt用("JWT-SESSION-"+appId,jwt)的key-value形式存儲(chǔ)到redis中,有效期設(shè)置為2倍的 t 。這樣jwt在有效期過(guò)后的 t 時(shí)間段內(nèi)可以申請(qǐng)刷新token。
還有個(gè)問(wèn)題是用戶攜帶過(guò)期的jwt對(duì)后臺(tái)請(qǐng)求,在可刷新時(shí)間段內(nèi)返回了新的jwt,應(yīng)該在用戶無(wú)感知的情況下返回請(qǐng)求的內(nèi)容,而不是接收一個(gè)刷新的jwt。我們是不是可以在每次request請(qǐng)求回調(diào)的時(shí)候判斷返回的是不是刷新jwt,但是判斷是之后我們是否放棄之前的用戶請(qǐng)求,如果不放棄,那是不是應(yīng)該在最開(kāi)始的用戶request請(qǐng)求前先保存這個(gè)請(qǐng)求,在之后的回調(diào)中如果是返回刷新jwt,我們?cè)贁y帶這個(gè)新的jwt再請(qǐng)求一次保存好的request請(qǐng)求?但對(duì)于前端這么大量的不同請(qǐng)求,這樣是不是太麻煩了?

這困擾了我很久哎,直到我用到了angualr的HttpInterceptor哈哈哈哈哈哈哈哈哈哈哈哈哈哈。

angualr的HttpInterceptor就是前端的攔截過(guò)濾器,發(fā)起請(qǐng)求會(huì)攔截處理,接收請(qǐng)求也會(huì)攔截處理。最大的好處對(duì)每次的原始request他都會(huì)完整的保存下來(lái),我們向后臺(tái)發(fā)生的request是他的clone。next.handle(request.clone)
繼承HttpInterceptor的AuthInterceptor,攔截response判斷是否為refresh token,是則攜帶新token再次發(fā)起保存的request:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private authService: AuthService, private router: Router) {}

  intercept(req: HttpRequest, next: HttpHandler): Observable> {
    const authToken = this.authService.getAuthorizationToken();
    const uid = this.authService.getUid();
    let authReq: any;
    if (authToken != null && uid != null) {
      authReq = req.clone({
        setHeaders: {
          "authorization": authToken,
          "appId": uid
        }
      });
    } else {
      authReq = req.clone();
    }

    console.log(authReq);
    return next.handle(authReq).pipe(
      mergeMap(event => {
        // 返回response
        if (event instanceof HttpResponse) {
          if (event.status === 200) {
            // 若返回JWT過(guò)期但refresh token未過(guò)期,返回新的JWT 狀態(tài)碼為1005
            if (event.body.meta.code === 1005) {
              const jwt = event.body.data.jwt;
              // 更新AuthorizationToken
              this.authService.updateAuthorizationToken(jwt);
              // clone request 重新發(fā)起請(qǐng)求
              // retry(1);
              authReq = req.clone({
                setHeaders: {
                  "authorization": jwt,
                  "appId": uid
                }
              });
              return next.handle(authReq);

            }
          }
          if (event.status === 404) {
            // go to 404 html
            this.router.navigateByUrl("/404");
          }
          if (event.status === 500) {
            // go to 500 html
            this.router.navigateByUrl("/500");
          }
        }
        console.log(event);
        // 返回正常情況的可觀察對(duì)象
        return of(event);
      }),
      catchError(this.handleError)
    );
  }

  private handleError(error: HttpErrorResponse) {
    if (error.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      console.error("An error occurred:", error.error.message);
    } else {
      console.error( `Backend returned code ${error.status}, ` +
        `body was: ${error.error}`);
    }
    repeat(1);
    return new ErrorObservable("親請(qǐng)檢查網(wǎng)絡(luò)");

  }
}

后端簽發(fā)jwt時(shí)所做的:

 /* *
     * @Description 這里已經(jīng)在 passwordFilter 進(jìn)行了登錄認(rèn)證
     * @Param [] 登錄簽發(fā) JWT
     * @Return java.lang.String
     */
    @ApiOperation(value = "用戶登錄",notes = "POST用戶登錄簽發(fā)JWT")
    @PostMapping("/login")
    public Message accountLogin(HttpServletRequest request, HttpServletResponse response) {
        Map params = RequestResponseUtil.getRequestParameters(request);
        String appId = params.get("appId");
        // 根據(jù)appId獲取其對(duì)應(yīng)所擁有的角色(這里設(shè)計(jì)為角色對(duì)應(yīng)資源,沒(méi)有權(quán)限對(duì)應(yīng)資源)
        String roles = accountService.loadAccountRole(appId);
        // 時(shí)間以秒計(jì)算,token有效刷新時(shí)間是token有效過(guò)期時(shí)間的2倍
        long refreshPeriodTime = 36000L;
        String jwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(),appId,
                "token-server",refreshPeriodTime >> 2,roles,null, SignatureAlgorithm.HS512);
        // 將簽發(fā)的JWT存儲(chǔ)到Redis: {JWT-SESSION-{appID} , jwt}
        redisTemplate.opsForValue().set("JWT-SESSION-"+appId,jwt,refreshPeriodTime, TimeUnit.SECONDS);
        AuthUser authUser = userService.getUserByAppId(appId);

        return new Message().ok(1003,"issue jwt success").addData("jwt",jwt).addData("user",authUser);
    }

后端refresh token時(shí)所做的:

protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception {
        Subject subject = getSubject(servletRequest,servletResponse);
        // 判斷是否為JWT認(rèn)證請(qǐng)求
        if ((null == subject || !subject.isAuthenticated()) && isJwtSubmission(servletRequest)) {
            AuthenticationToken token = createJwtToken(servletRequest);
            try {
                subject.login(token);
//                return this.checkRoles(subject,mappedValue) && this.checkPerms(subject,mappedValue);
                return this.checkRoles(subject,mappedValue);
            }catch (AuthenticationException e) {
                LOGGER.info(e.getMessage(),e);
                // 如果是JWT過(guò)期
                if (e.getMessage().equals("expiredJwt")) {
                    // 這里初始方案先拋出令牌過(guò)期,之后設(shè)計(jì)為在Redis中查詢當(dāng)前appId對(duì)應(yīng)令牌,其設(shè)置的過(guò)期時(shí)間是JWT的兩倍,此作為JWT的refresh時(shí)間
                    // 當(dāng)JWT的有效時(shí)間過(guò)期后,查詢其refresh時(shí)間,refresh時(shí)間有效即重新派發(fā)新的JWT給客戶端,
                    // refresh也過(guò)期則告知客戶端JWT時(shí)間過(guò)期重新認(rèn)證

                    // 當(dāng)存儲(chǔ)在redis的JWT沒(méi)有過(guò)期,即refresh time 沒(méi)有過(guò)期
                    String appId = WebUtils.toHttp(servletRequest).getHeader("appId");
                    String jwt = WebUtils.toHttp(servletRequest).getHeader("authorization");
                    String refreshJwt = redisTemplate.opsForValue().get("JWT-SESSION-"+appId);
                    if (null != refreshJwt && refreshJwt.equals(jwt)) {
                        // 重新申請(qǐng)新的JWT
                        // 根據(jù)appId獲取其對(duì)應(yīng)所擁有的角色(這里設(shè)計(jì)為角色對(duì)應(yīng)資源,沒(méi)有權(quán)限對(duì)應(yīng)資源)
                        String roles = accountService.loadAccountRole(appId);
                        long refreshPeriodTime = 36000L;  //seconds為單位,10 hours
                        String newJwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(),appId,
                                "token-server",refreshPeriodTime >> 2,roles,null, SignatureAlgorithm.HS512);
                        // 將簽發(fā)的JWT存儲(chǔ)到Redis: {JWT-SESSION-{appID} , jwt}
                        redisTemplate.opsForValue().set("JWT-SESSION-"+appId,newJwt,refreshPeriodTime, TimeUnit.SECONDS);
                        Message message = new Message().ok(1005,"new jwt").addData("jwt",newJwt);
                        RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
                        return false;
                    }else {
                        // jwt時(shí)間失效過(guò)期,jwt refresh time失效 返回jwt過(guò)期客戶端重新登錄
                        Message message = new Message().error(1006,"expired jwt");
                        RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
                        return false;
                    }

                }
                // 其他的判斷為JWT錯(cuò)誤無(wú)效
                Message message = new Message().error(1007,"error Jwt");
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
                return false;

            }catch (Exception e) {
                // 其他錯(cuò)誤
                LOGGER.warn(servletRequest.getRemoteAddr()+"JWT認(rèn)證"+e.getMessage(),e);
                // 告知客戶端JWT錯(cuò)誤1005,需重新登錄申請(qǐng)jwt
                Message message = new Message().error(1007,"error jwt");
                RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
                return false;
            }
        }else {
            // 請(qǐng)求未攜帶jwt 判斷為無(wú)效請(qǐng)求
            Message message = new Message().error(1111,"error request");
            RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);
            return false;
        }
    }




。。。。。持續(xù)更新中。。。。

效果展示

github:
bootshiro
usthe

碼云:
bootshiro
usthe



持續(xù)更新。。。。。。


分享一波阿里云代金券快速上云

轉(zhuǎn)載請(qǐng)注明 from tomsun28

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

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/69034.html

相關(guān)文章

  • api權(quán)限管理系統(tǒng)前后分離實(shí)踐

    摘要:自己在前后端分離上的實(shí)踐要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開(kāi)的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)管理,這次實(shí)踐包含兩個(gè)模塊基于搭建的權(quán)限管理系統(tǒng)后臺(tái)編寫(xiě)的前端管理。 自己在前后端分離上的實(shí)踐 要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開(kāi)的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)restful api管理,這次實(shí)踐包含兩個(gè)模塊,基于springBoot + shiro搭建的權(quán)限管理系統(tǒng)后臺(tái)bootshir...

    bawn 評(píng)論0 收藏0
  • 細(xì)說(shuō) Django—web 前后分離

    摘要:理解這個(gè)統(tǒng)一標(biāo)準(zhǔn)的不符規(guī)范的資源沒(méi)有統(tǒng)一實(shí)踐一個(gè)例子圖書(shū)管理系統(tǒng)技術(shù)棧前端后端要點(diǎn)驗(yàn)證前端路由統(tǒng)一的請(qǐng)求響應(yīng)攔截處理權(quán)限控制表級(jí)對(duì)象級(jí)如下圖最后,是否要做前后端分離的開(kāi)發(fā)模式,取決于實(shí)際情況的多方位考量,適合的才是更好的。 所謂的前后端分離 淵源 前端發(fā)展史 特點(diǎn) 前端:負(fù)責(zé) View 和 Controller 層 后端:只負(fù)責(zé) Model 層,業(yè)務(wù)處理/數(shù)據(jù)等 優(yōu)缺點(diǎn) 優(yōu)點(diǎn):解...

    adam1q84 評(píng)論0 收藏0
  • 實(shí)現(xiàn)前后分離的心得

    摘要:實(shí)現(xiàn)前后端分離的心得對(duì)目前的來(lái)說(shuō),前后端分離已經(jīng)變得越來(lái)越流行了,越來(lái)越多的企業(yè)網(wǎng)站都開(kāi)始往這個(gè)方向靠攏。前后端工作分配不均。 實(shí)現(xiàn)前后端分離的心得 對(duì)目前的web來(lái)說(shuō),前后端分離已經(jīng)變得越來(lái)越流行了,越來(lái)越多的企業(yè)/網(wǎng)站都開(kāi)始往這個(gè)方向靠攏。那么,為什么要選擇前后端分離呢?前后端分離對(duì)實(shí)際開(kāi)發(fā)有什么好處呢? 為什么選擇前后端分離 在以前傳統(tǒng)的網(wǎng)站開(kāi)發(fā)中,前端一般扮演的只是切圖的工作...

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

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

0條評(píng)論

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