Oltu在Jersey框架上实现oauth2.0授权模块
oltu是一个开源的oauth2.0协议的实现,本人在此开源项目的基础上进行修改,实现一个自定义的oauth2.0模块。
关于oltu的使用大家可以看这里:http://oltu.apache.org/
项目可以从这里下载:http://mirror.bit.edu.cn/apache/oltu/org.apache.oltu.oauth2/
项目中我将四种授权方式都做了实现(授权码模式、简化模式、密码模式及客户端模式),但是这里仅以授权码模式为例,服务端采用Jersey框架实现,而客户端采用spring mvc实现。

服务器端实现:为了方便开发,我将oltu的所有源码都拖进了项目中,而不是导入jar,因为很多地方可能在我所开发的项目中不适用,这样可以方便修改和跟踪代码。
其实服务端开发很简单,主要集中在两个比较主要的文件中,一个是请求授权的AuthzEndpoint.java,一个是生成令牌的TokenEndpoint.java,如图所示。

至于资源控制器由于我开发的项目中,资源访问控制是采用过滤器的方式,因此没有用到oltu提供的java类,两个主要类文件的代码修改如下:
AuthzEndpoint.java
/** * Copyright 2010 Newcastle University * * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.oltu.oauth2.integration.endpoints; import java.net.URI; import java.net.URISyntaxException; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; import java.util.Properties; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import org.apache.ibatis.session.SqlSession; import org.apache.oltu.oauth2.as.issuer.MD5Generator; import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl; import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest; import org.apache.oltu.oauth2.as.response.OAuthASResponse; import org.apache.oltu.oauth2.common.OAuth; import org.apache.oltu.oauth2.common.error.OAuthError; import org.apache.oltu.oauth2.common.error.ServerErrorType; import org.apache.oltu.oauth2.common.exception.OAuthProblemException; import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.apache.oltu.oauth2.common.message.OAuthResponse; import org.apache.oltu.oauth2.common.message.types.ResponseType; import org.apache.oltu.oauth2.integration.utils.Cache; import org.apache.oltu.oauth2.integration.utils.CacheManager; import com.cz.bean.App; import com.cz.bean.Authority; import com.cz.bean.RefreshToken; import com.cz.dao.AppMapper; import com.cz.dao.AuthorityMapper; import com.cz.dao.RefreshTokenMapper; import com.cz.util.DbUtil; /** * * client request authorization * */ @Path("/authz") public class AuthzEndpoint { SqlSession sqlSession = DbUtil.getSessionFactory().openSession(true); AppMapper appDao = sqlSession.getMapper(AppMapper.class); AuthorityMapper authorityDao = sqlSession.getMapper(AuthorityMapper.class); RefreshTokenMapper refreshTokenDao = sqlSession .getMapper(RefreshTokenMapper.class); //登录页面 private static String loginPage; //错误页面 private static String errorPage; static { Properties p = new Properties(); try { p.load(AuthzEndpoint.class.getClassLoader().getResourceAsStream( "config.properties")); loginPage = p.getProperty("loginPage"); errorPage = p.getProperty("errorPage"); } catch (Exception e) { e.printStackTrace(); } } public static final String INVALID_CLIENT_DESCRIPTION = "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)."; @GET public Response authorize(@Context HttpServletRequest request) throws URISyntaxException, OAuthSystemException { OAuthAuthzRequest oauthRequest = null; OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl( new MD5Generator()); try { oauthRequest = new OAuthAuthzRequest(request); /* * 当前登录的用户,模拟一个从session中获取的登录用户 * 该方法未实现,待模块与养老平台整合时,应调用养老平台方法判断用户是否已登录 * 并获得对应用户的userId */ String userId = "1"; if ("".equals(userId) || userId == null) { // 用户没有登录就跳转到登录页面 return Response.temporaryRedirect(new URI(loginPage)).build(); } App app = null; if(oauthRequest.getClientId()!=null && !"".equals(oauthRequest.getClientId())){ app = appDao.selectByPrimaryKey(oauthRequest.getClientId()); }else{ return Response.temporaryRedirect(new URI(errorPage+"?error="+ServerErrorType.CLIENT_ID_IS_NULL)).build(); } // 根据response_type创建response String responseType = oauthRequest .getParam(OAuth.OAUTH_RESPONSE_TYPE); OAuthASResponse.OAuthAuthorizationResponseBuilder builder = OAuthASResponse .authorizationResponse(request, HttpServletResponse.SC_FOUND); // 检查传入的客户端id是否正确 if (app == null) { return Response.temporaryRedirect(new URI(errorPage+"?error="+ServerErrorType.UNKOWN_CLIENT_ID)).build(); } String scope = oauthRequest.getParam(OAuth.OAUTH_SCOPE); // 授权请求类型 if (responseType.equals(ResponseType.CODE.toString())) { String code = oauthIssuerImpl.authorizationCode(); builder.setCode(code); CacheManager.putCache(userId+"_code", new Cache("code", code, 216000000, false)); CacheManager.putCache(userId+"_scope", new Cache("scope", scope, 216000000, false)); } if (responseType.equals(ResponseType.TOKEN.toString())) { // 校验client_secret if (!app.getSecret_key().equals(oauthRequest.getClientSecret())) { OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_OK) .setError(OAuthError.TokenResponse.INVALID_CLIENT).setErrorDescription(INVALID_CLIENT_DESCRIPTION) .buildJSONMessage(); return Response.status(response.getResponseStatus()).entity(response.getBody()).build(); } String accessToken = oauthIssuerImpl.accessToken(); builder.setAccessToken(accessToken); builder.setExpiresIn(3600l); //判断是否已经授权----待调整是放在authz部分还是token部分 Map<String,Object> aQueryParam = new HashMap<>(); aQueryParam.put("appKey",oauthRequest.getClientId()); aQueryParam.put("userId",Integer.valueOf(userId)); if(authorityDao.findUnique(aQueryParam)==null){ Authority authority = new Authority(); authority.setApp_key(oauthRequest.getClientId()); authority.setUser_id(Integer.valueOf(userId)); authorityDao.insert(authority); } // 存储token,已授权则更新令牌,未授权则新增令牌 Map<String,Object> rQueryParam = new HashMap<>(); rQueryParam.put("appKey", oauthRequest.getClientId()); rQueryParam.put("userId", Integer.valueOf(userId)); if (refreshTokenDao.findUnique(rQueryParam) != null) { Map<String,Object> map = new HashMap<>(); map.put("accessToken", accessToken); map.put("appKey", oauthRequest.getClientId()); map.put("userId", Integer.valueOf(userId)); map.put("createTime", getDate()); map.put("scope", scope); map.put("authorizationTime", getDate()); refreshTokenDao.updateAccessToken(map); } else { RefreshToken rt = new RefreshToken(); rt.setApp_key(oauthRequest.getClientId()); rt.setUser_id(Integer.valueOf(userId)); rt.setAccess_token(accessToken); rt.setCreate_time(getDate()); rt.setAuthorization_time(getDate()); rt.setExpire("3600"); rt.setScope(scope); rt.setAuthorization_time(getDate()); refreshTokenDao.insert(rt); } } // 客户端跳转URI String redirectURI = oauthRequest .getParam(OAuth.OAUTH_REDIRECT_URI); final OAuthResponse response = builder.location(redirectURI).setParam("scope", scope) .buildQueryMessage(); String test = response.getLocationUri(); URI url = new URI(response.getLocationUri()); return Response.status(response.getResponseStatus()).location(url) .build(); } catch (OAuthProblemException e) { return Response.temporaryRedirect(new URI(errorPage+"?error="+ServerErrorType.BAD_RQUEST)).build(); } } private String getDate() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(System.currentTimeMillis()); } } |
上面的代码是授权码认证的第一步,当用户同意授权之后向服务器请求授权码。你可以使用一下腾讯的授权功能来加深一下体会,因为我所开发的模块也是参考腾讯的授权认证流程来实现的,客户端通过提交请求,访问类似http://192.168.19.75:10087/oauth/authz?client_id=s6BhdRkqt3&client_secret=12345&redirect_uri=http://localhost:8080/redirect.jsp&state=y&response_type=authorization_code的链接来访问上面的程序,参数的含义如下
client_id :客户端id
client_secret:客户端密钥
redirect_uri:回调地址,第三方应用定义的地址
State:状态,服务器将返回一个一模一样的参数。
response_type:授权方式,这里必须是authorization_code,表示授权码 方式。
这个过程结束时,服务器会跳转至第三方应用定义的回调地址并附上授权码,而第三方通过这个回调地址获得授权码并进行相应的处理,而这个过程在oltu的实现中其实就是几行简单的代码:
// 创建response wrapper OAuthAuthzResponse oar = null; oar = OAuthAuthzResponse.oauthCodeAuthzResponse(request); // 获得授权码 String code = oar.getCode(); |
上面的代码就是oltu客户端接收服务器发回的授权码的代码,其中request是一个HttpServletRequest对象,获得了授权码之后,按照下一步的流程,自然就是向授权服务器请求令牌并附上上一步获得的授权码。服务器获得授权码并进行相应处理的代码如下:
TokenEndpoint.java
/** * Copyright 2010 Newcastle University * * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.oltu.oauth2.integration.endpoints; import java.net.URI; import java.net.URISyntaxException; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; import java.util.Properties; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import org.apache.ibatis.session.SqlSession; import org.apache.oltu.oauth2.as.issuer.MD5Generator; import org.apache.oltu.oauth2.as.issuer.OAuthIssuer; import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl; import org.apache.oltu.oauth2.as.request.OAuthTokenRequest; import org.apache.oltu.oauth2.as.response.OAuthASResponse; import org.apache.oltu.oauth2.common.OAuth; import org.apache.oltu.oauth2.common.error.OAuthError; import org.apache.oltu.oauth2.common.error.ServerErrorType; import org.apache.oltu.oauth2.common.exception.OAuthProblemException; import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.apache.oltu.oauth2.common.message.OAuthResponse; import org.apache.oltu.oauth2.common.message.types.GrantType; import org.apache.oltu.oauth2.integration.utils.CacheManager; import com.cz.bean.App; import com.cz.bean.Authority; import com.cz.bean.RefreshToken; import com.cz.bean.User; import com.cz.dao.AppMapper; import com.cz.dao.AuthorityMapper; import com.cz.dao.RefreshTokenMapper; import com.cz.dao.UserMapper; import com.cz.util.DbUtil; /** * * get access token * */ @Path("/token") public class TokenEndpoint { SqlSession sqlSession = DbUtil.getSessionFactory().openSession(true); AppMapper appDao = sqlSession.getMapper(AppMapper.class); RefreshTokenMapper refreshTokenDao = sqlSession .getMapper(RefreshTokenMapper.class); UserMapper dao = sqlSession.getMapper(UserMapper.class); AuthorityMapper authorityDao = sqlSession.getMapper(AuthorityMapper.class); // 登录页面 private static String loginPage; // 错误页面 private static String errorPage; static { Properties p = new Properties(); try { p.load(AuthzEndpoint.class.getClassLoader().getResourceAsStream( "config.properties")); loginPage = p.getProperty("loginPage"); errorPage = p.getProperty("errorPage"); } catch (Exception e) { e.printStackTrace(); } } public static final String INVALID_CLIENT_DESCRIPTION = "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)."; @SuppressWarnings({ "unchecked", "rawtypes" }) @POST @Consumes("application/x-www-form-urlencoded") @Produces("application/json") public Response authorize(@Context HttpServletRequest request) throws OAuthSystemException, URISyntaxException { OAuthTokenRequest oauthRequest = null; String scope = ""; OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator()); try { oauthRequest = new OAuthTokenRequest(request); /* * 当前登录的用户,模拟一个从session中获取的登录用户 * 该方法未实现,待模块与养老平台整合时,应调用养老平台方法判断用户是否已登录 */ String userId = "1"; if ("".equals(userId) || userId == null) { // 用户没有登录的话就跳转到登录页面 return Response.temporaryRedirect(new URI(loginPage)).build(); } App app = null; if (oauthRequest.getClientId() != null && !"".equals(oauthRequest.getClientId())) { app = appDao.selectByPrimaryKey(oauthRequest.getClientId()); } else { return Response.temporaryRedirect(new URI(errorPage + "?error=" + ServerErrorType.CLIENT_ID_IS_NULL)).build(); } // 校验clientid if (app == null || !app.getApp_key().toString().equals(oauthRequest.getClientId())) { if(oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.AUTHORIZATION_CODE.toString())){ return Response.temporaryRedirect(new URI(errorPage + "?error=" + ServerErrorType.UNKOWN_CLIENT_ID)).build(); }else{ OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_OK) .setError(OAuthError.TokenResponse.INVALID_CLIENT).setErrorDescription(INVALID_CLIENT_DESCRIPTION) .buildJSONMessage(); return Response.status(response.getResponseStatus()).entity(response.getBody()).build(); } } // 校验client_secret if (!app.getSecret_key().equals(oauthRequest.getClientSecret())) { if(oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.AUTHORIZATION_CODE.toString())){ return Response.temporaryRedirect(new URI(errorPage + "?error=" + ServerErrorType.UNKOWN_CLIENT_SECRET)).build(); }else{ OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_OK) .setError(OAuthError.TokenResponse.INVALID_CLIENT).setErrorDescription(INVALID_CLIENT_DESCRIPTION) .buildJSONMessage(); return Response.status(response.getResponseStatus()).entity(response.getBody()).build(); } } // 校验不同类型的授权方式 if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.AUTHORIZATION_CODE.toString())) { String cacheCode = null; if (CacheManager.getCacheInfo(userId + "_code").getValue() != null) { cacheCode = CacheManager.getCacheInfo(userId + "_code") .getValue().toString(); } else { // 用户没有登录的话就跳转到登录页面 return Response.temporaryRedirect(new URI(loginPage)).build(); } if (!cacheCode.equals(oauthRequest.getParam(OAuth.OAUTH_CODE))) { return Response.temporaryRedirect(new URI(errorPage+ "?error=" + ServerErrorType.INVALID_AUTHORIZATION_CODE)).build(); } if(CacheManager.getCacheInfo(userId+"_scope").getValue()!=null){ scope = CacheManager.getCacheInfo(userId+"_scope").getValue().toString(); } } else if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.PASSWORD.toString())) { User user = dao.getById(userId); if (!user.getPassword().equals(oauthRequest.getPassword())|| !user.getName().equals(oauthRequest.getUsername())) { OAuthResponse response = OAuthASResponse .errorResponse(HttpServletResponse.SC_OK) .setError(OAuthError.TokenResponse.INVALID_CLIENT) .setErrorDescription("Invalid username or password.") .buildJSONMessage(); return Response.status(response.getResponseStatus()).entity(response.getBody()).build(); } } else if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals( GrantType.CLIENT_CREDENTIALS.toString())) { // 客户端id以及secret已验证,更多验证规则在这里添加,没有其他验证则程序直接发放令牌 // OAuthResponse response = OAuthASResponse // .errorResponse(HttpServletResponse.SC_OK) // .setError(OAuthError.TokenResponse.INVALID_GRANT) // .setErrorDescription("invalid client") // .buildJSONMessage(); // return Response.status(response.getResponseStatus()).entity(response.getBody()).build(); }else if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals( GrantType.REFRESH_TOKEN.toString())) { // 刷新令牌未实现 } String accessToken = oauthIssuerImpl.accessToken(); String refreshToken = oauthIssuerImpl.refreshToken(); // 构建响应 OAuthResponse response = OAuthASResponse .tokenResponse(HttpServletResponse.SC_OK) .setAccessToken(accessToken).setRefreshToken(refreshToken) .setExpiresIn("3600") .buildJSONMessage(); // 判断是否已经授权----待调整是放在authz部分还是token部分 Map aQueryParam = new HashMap(); aQueryParam.put("appKey", oauthRequest.getClientId()); aQueryParam.put("userId", Integer.valueOf(userId)); if (authorityDao.findUnique(aQueryParam) == null) { Authority authority = new Authority(); authority.setApp_key(oauthRequest.getClientId()); authority.setUser_id(Integer.valueOf(userId)); authorityDao.insert(authority); } // String scope = ""; // if(CacheManager.getCacheInfo(userId+"_scope").getValue()!=null){ // scope = CacheManager.getCacheInfo(userId+"_scope").getValue().toString(); // } // 存储token,已授权则更新令牌,未授权则新增令牌 Map rQueryParam = new HashMap(); rQueryParam.put("appKey", oauthRequest.getClientId()); rQueryParam.put("userId", Integer.valueOf(userId)); if (refreshTokenDao.findUnique(rQueryParam) != null) { Map map = new HashMap(); map.put("accessToken", accessToken); map.put("appKey", oauthRequest.getClientId()); map.put("userId", Integer.valueOf(userId)); map.put("createTime", getDate()); map.put("scope", scope); map.put("authorizationTime", getDate()); refreshTokenDao.updateAccessToken(map); } else { RefreshToken rt = new RefreshToken(); rt.setApp_key(oauthRequest.getClientId()); rt.setUser_id(Integer.valueOf(userId)); rt.setAccess_token(accessToken); rt.setRefresh_token(refreshToken); rt.setCreate_time(getDate()); rt.setAuthorization_time(getDate()); rt.setExpire("3600"); rt.setScope(scope); rt.setAuthorization_time(getDate()); refreshTokenDao.insert(rt); } return Response.status(response.getResponseStatus()) .entity(response.getBody()).build(); } catch (OAuthProblemException e) { System.out.println(e.getDescription()); return Response.temporaryRedirect(new URI(errorPage + "?error="+ ServerErrorType.BAD_RQUEST)).build(); } } private String getDate() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(System.currentTimeMillis()); } } |
上面的代码,处理了客户端发来的申请令牌请求,并向客户端发放访问令牌,而oltu的客户端则通过如下代码来完成这个请求令牌和解析令牌的过程:
OAuthClient client = new OAuthClient(new URLConnectionClient()); OAuthAccessTokenResponse oauthResponse = null; oauthResponse = client.accessToken(request, OAuth.HttpMethod.POST); String token = oauthResponse.getRefreshToken(); |
如果你是第一次开发,oauth2.0的认证过程可能会让你觉得头疼,因为你首先需要对这个流程很熟悉,并且同时要看懂了oltu的代码才好理解这个开源的项目到底是怎么实现这个过程的,因此这里我不过多的粘贴代码,因为这并没有什么卵用,还是运行项目和追踪代码比较容易理解它的原理,下面是我实现的项目代码,代码写得比较简陋,不过对于跟我一样的菜鸟,还是能起到一定的帮助的~
服务端:https://git.oschina.net/honganlei/oauth-server.git
服务端授权登录页面:https://git.oschina.net/honganlei/OauthClient.git
第三方接入授权模块的例子:https://git.oschina.net/honganlei/OauthApp.git
Oltu在Jersey框架上实现oauth2.0授权模块的更多相关文章
- 使用DotNetOpenAuth搭建OAuth2.0授权框架
标题还是一如既往的难取. 我认为对于一个普遍问题,必有对应的一个简洁优美的解决方案.当然这也许只是我的一厢情愿,因为根据宇宙法则,所有事物总归趋于混沌,而OAuth协议就是混沌中的产物,不管是1.0. ...
- 使用微服务架构思想,设计部署OAuth2.0授权认证框架
1,授权认证与微服务架构 1.1,由不同团队合作引发的授权认证问题 去年的时候,公司开发一款新产品,但人手不够,将B/S系统的Web开发外包,外包团队使用Vue.js框架,调用我们的WebAPI,但是 ...
- API代理网关和OAuth2.0授权认证框架
API代理网关和OAuth2.0授权认证框架 https://www.cnblogs.com/bluedoctor/p/8967951.html 1,授权认证与微服务架构 1.1,由不同团队合作引发的 ...
- Spring Security实现OAuth2.0授权服务 - 基础版
一.OAuth2.0协议 1.OAuth2.0概述 OAuth2.0是一个关于授权的开放网络协议. 该协议在第三方应用与服务提供平台之间设置了一个授权层.第三方应用需要服务资源时,并不是直接使用用户帐 ...
- OAuth2.0授权
一.什么是OAuth2.0官方网站:http://oauth.net/ http://oauth.net/2/ 权威定义:OAuth is An open protocol to allow secu ...
- Microsoft.Owin.Security.OAuth搭建OAuth2.0授权服务端
Microsoft.Owin.Security.OAuth搭建OAuth2.0授权服务端 目录 前言 OAuth2.0简介 授权模式 (SimpleSSO示例) 使用Microsoft.Owin.Se ...
- 微信开发——OAuth2.0授权
微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使用这个的时候失败了或者无法理解其内容,希望我出个教程详细讲解一下,于是便有了这篇文章. 一. ...
- 新浪微博Oauth2.0授权认证及SDK、API的使用(Android)
---------------------------------------------------------------------------------------------- [版权申明 ...
- 工作笔记—新浪微博Oauth2.0授权 获取Access Token (java)
java发送新浪微博,一下博客从注册到发布第一条微博很详细 利用java语言在eclipse下实现在新浪微博开发平台发微博:http://blog.csdn.net/michellehsiao/art ...
随机推荐
- 【rlz01】完全数
Time Limit: 3 second Memory Limit: 2 MB 问题描述 所谓完全数,就是这个数除了它本身的约数之和也等于这个数,比如说6的约数有1.2.3,而1+2+3=6,所以6是 ...
- [Yarn] Use Yarn to Create an Alternative Import Name of an Installed Library
In this lesson we'll show how to use yarn to alias the names of same npm libraries but install diffe ...
- ZOJ FatMouse' Trade 贪心
得之我幸,不得,我命.仅此而已. 学姐说呀,希望下次看到你的时候依然潇洒如故.(笑~) 我就是这么潇洒~哈哈. 感觉大家比我还紧张~ 我很好的.真的 ------------------------- ...
- 【30.36%】【codeforces 740D】Alyona and a tree
time limit per test2 seconds memory limit per test256 megabytes inputstandard input outputstandard o ...
- Perl自动释放Licence启动Verdi
Perl自动释放Licence启动Verdi 在工作中,遇到verdi的License不够的情况,某些人占用了多个License,为及时获得一个可用的License,写了一个perl来kill运行时间 ...
- Linux 常用性能工具
Linux 常用性能工具简介. 一.wget 文件下载 使用wget下载单个文件:wget URL 下载并以不同的文件名保存:wget -O wordpress.zip URL wget限速下载:wg ...
- AJAX 相关笔记
AJAX (Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)). 平时工作中使用ajax的频率挺高的,这里整理了一些ajax相关的小知识,后续 ...
- 微信开发之八 页面获取周围beacon设备
原文链接:https://blog.csdn.net/qq_37936542/article/details/78912197 --注意:微信测试号不具备这个功能 步骤一:绑定域名 注意不要加ht ...
- thinkphp中如何实现无限级分类?
thinkphp中如何实现无限级分类? 一.总结 1.数据表设计+递归算法 二.php实现无限级分类实例总结 1.数据库数据如下: 2.任务需求:给一个id,求自己和所有父亲. 3.实现代码如下:th ...
- Struts2之配置使用
重要声明:此次学习struts2使用的版本号为:struts-2.3.15.3.假设是用的其它版本号出现的问题能够联系我. 一. 1.首先就是打开myeclipse创建project名为:struts ...