落雨 cas 单点登录

希望能给以后来研究cas的兄弟留下一点思路,也算是研究了两天的成果,外国人的代码写的很晦涩,翻译下来也没有时间继续跟进,所以有错误的还请大家跟帖和我讨论,qq 394263788

edu.yale.its.tp.cas.client.filter源码分析:

[java] view plaincopy
  1. /*  Copyright (c) 2000-2004 Yale University. All rights reserved.
  2. *  See full notice at end.
  3. */
  4. package edu.yale.its.tp.cas.client.filter;
  5. import java.io.*;
  6. import java.net.*;
  7. import java.util.*;
  8. import javax.servlet.*;
  9. import javax.servlet.http.*;
  10. import edu.yale.its.tp.cas.client.*;
  11. import org.apache.commons.logging.Log;
  12. import org.apache.commons.logging.LogFactory;
  13. /*
  14. *
  15. * @author Shawn Bayern
  16. * @author Drew Mazurek
  17. * @author andrew.petro@yale.edu
  18. */
  19. public class CASFilter implements Filter {
  20. private static Log log = LogFactory.getLog(CASFilter.class);
  21. // Filter initialization parameters
  22. //必须参数
  23. /**
  24. * loginUrl:指定 CAS 提供登录页面的 URL
  25. */
  26. public final static String LOGIN_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.loginUrl";
  27. /**
  28. * validateUrl:指定 CAS 提供 service ticket 或 proxy ticket 验证服务的 URL
  29. */
  30. public final static String VALIDATE_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.validateUrl";
  31. /**
  32. * serviceUrl:本web项目的URL,该参数指定过后将覆盖 serverName 参数,成为登录成功过后重定向的目的地址
  33. */
  34. public final static String SERVICE_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.serviceUrl";
  35. /**
  36. * serverName:全主机端口号,指定客户端的域名和端口,是指客户端应用所在机器而不是 CAS Server 所在机器,该参数或 serviceUrl 至少有一个必须指定
  37. */
  38. public final static String SERVERNAME_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.serverName";
  39. //可选参数
  40. /**
  41. * renew:如果指定为 true,那么受保护的资源每次被访问时均要求用户重新进行验证,而不管之前是否已经通过
  42. */
  43. public final static String RENEW_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.renew";
  44. /**
  45. * authorizedProxy:用于允许当前应用从代理处获取 proxy tickets,该参数接受以空格分隔开的多个 proxy URLs,但实际使用只需要一个成功即可。当指定该参数过后,需要修改 validateUrl 到 proxyValidate,
  46. */
  47. public final static String AUTHORIZED_PROXY_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.authorizedProxy";
  48. /**
  49. * proxyCallbackUrl:用于当前应用需要作为其他服务的代理(proxy)时获取 Proxy Granting Ticket 的地址
  50. */
  51. public final static String PROXY_CALLBACK_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.proxyCallbackUrl";
  52. /**
  53. * wrapRequest:如果指定为 true,那么 CASFilter 将重新包装 HttpRequest,并且使 getRemoteUser() 方法返回当前登录用户的用户名
  54. */
  55. public final static String WRAP_REQUESTS_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.wrapRequest";
  56. /**
  57. * gateway:这个参数很奇葩,一开始没读懂是干嘛的。。官方解释是一旦发生过CAS重定向,过滤器将不会自动重新设置登录的用户。然后你可以提供一个明确的CAS登录链接(HTTPS:/ / CAS服务器/ CAS /登录?服务= HTTP:/ /应用程序)或建立映射到不同的路径的过滤器的两个实例。一个实例将gateway实现。当你需要登录的用户,直接转到其他过滤器。
  58. * 是的你没有想错,这一句话着实让人不知道是要说明什么,于是万能的百度上有且仅有一个前辈说出来了这个参数其实是和renew互斥的,renew就是说无论如何都得重新验证此用户,不管你session中有没有上下文信息。而gateway则是只要检测到session中有sso上下文,就不再重新认证
  59. */
  60. public final static String GATEWAY_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.gateway";
  61. public final static String CAS_FILTER_USER = "edu.yale.its.tp.cas.client.filter.user";
  62. public final static String CAS_FILTER_RECEIPT = "edu.yale.its.tp.cas.client.filter.receipt";
  63. private static final String CAS_FILTER_GATEWAYED = "edu.yale.its.tp.cas.client.filter.didGateway";
  64. // *********************************************************************
  65. // Configuration state
  66. private String casLogin;
  67. private String casValidate;
  68. private String casServiceUrl;
  69. private String casServerName;
  70. private String casProxyCallbackUrl;
  71. private boolean casRenew;
  72. private boolean wrapRequest;
  73. private boolean casGateway = false;
  74. /**
  75. * 对proxyticketreceptor URL授权代理在过滤器的路径的服务列表
  76. */
  77. private List authorizedProxies = new ArrayList();
  78. // *********************************************************************
  79. // Initialization
  80. public void init(FilterConfig config) throws ServletException {
  81. //拿到参数
  82. casLogin = config.getInitParameter(LOGIN_INIT_PARAM);
  83. casValidate = config.getInitParameter(VALIDATE_INIT_PARAM);
  84. casServiceUrl = config.getInitParameter(SERVICE_INIT_PARAM);
  85. String casAuthorizedProxy = config.getInitParameter(AUTHORIZED_PROXY_INIT_PARAM);
  86. casRenew = Boolean.valueOf(config.getInitParameter(RENEW_INIT_PARAM)).booleanValue();
  87. casServerName = config.getInitParameter(SERVERNAME_INIT_PARAM);
  88. casProxyCallbackUrl = config.getInitParameter(PROXY_CALLBACK_INIT_PARAM);
  89. wrapRequest = Boolean.valueOf(config.getInitParameter(WRAP_REQUESTS_INIT_PARAM)).booleanValue();
  90. casGateway = Boolean.valueOf(config.getInitParameter(GATEWAY_INIT_PARAM)).booleanValue();
  91. if (casGateway && Boolean.valueOf(casRenew).booleanValue()) {
  92. //这俩参数不能一起设置为true
  93. throw new ServletException("gateway and renew cannot both be true in filter configuration");
  94. }
  95. if (casServerName != null && casServiceUrl != null) {
  96. //这俩参数也不能一起设置
  97. throw new ServletException("serverName and serviceUrl cannot both be set: choose one.");
  98. }
  99. if (casServerName == null && casServiceUrl == null) {
  100. //这俩参数也不能一起为null
  101. throw new ServletException("one of serverName or serviceUrl must be set.");
  102. }
  103. if (casServiceUrl != null) {
  104. //检测uri前缀
  105. if (!(casServiceUrl.startsWith("https://") || (casServiceUrl.startsWith("http://")))) {
  106. throw new ServletException("service URL must start with http:// or https://; its current value is [" + casServiceUrl + "]");
  107. }
  108. }
  109. if (casValidate == null) {
  110. //cas验证用户的网址不能为空
  111. throw new ServletException("validateUrl parameter must be set.");
  112. }
  113. if (!casValidate.startsWith("https://")) {
  114. //如果cas认证网址不是以https开头,就报错。。如果你是用http请求,可以屏蔽掉这个判断语句
  115. throw new ServletException("validateUrl must start with https://, its current value is [" + casValidate + "]");
  116. }
  117. //代理是否为空
  118. if (casAuthorizedProxy != null) {
  119. // parse and remember authorized proxies
  120. StringTokenizer casProxies = new StringTokenizer(casAuthorizedProxy);
  121. while (casProxies.hasMoreTokens()) {
  122. //授权的标记
  123. String anAuthorizedProxy = casProxies.nextToken();
  124. //https前缀检测
  125. if (!anAuthorizedProxy.startsWith("https://")) {
  126. throw new ServletException("CASFilter initialization parameter for authorized proxies " + "must be a whitespace delimited list of authorized proxies.  " + "Authorized proxies must be secure (https) addresses.  This one wasn't: [" + anAuthorizedProxy + "]");
  127. }
  128. //将所有授权的代理添加到list中(唉,着实不知道是干什么的,也许几年后回来读读应该能知道答案,2013年4月22日14:56:37)
  129. this.authorizedProxies.add(anAuthorizedProxy);
  130. }
  131. }
  132. if (log.isDebugEnabled()) {
  133. log.debug(("CASFilter initialized as: [" + toString() + "]"));
  134. }
  135. }
  136. // *********************************************************************
  137. // Filter processing
  138. // 过滤器处理
  139. public void doFilter(ServletRequest request, ServletResponse response, FilterChain fc) throws ServletException, IOException {
  140. //核心思想:首先检查session中有无凭证receipt,如果有,那么就要去下个过滤器链进行处理,如果无,则获取传参ticket,如果有ticket,就经过getAuthenticatedUser()方法去拿到receipt凭证,如果无(这中间会有一些对renew或者gateway的处理),就立即进入cas服务端进行登录
  141. if (log.isTraceEnabled()) {
  142. log.trace("entering doFilter()");
  143. }
  144. // make sure we've got an HTTP request
  145. if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
  146. log.error("doFilter() called on a request or response that was not an HttpServletRequest or response.");
  147. throw new ServletException("CASFilter protects only HTTP resources");
  148. }
  149. // Is this a request for the proxy callback listener? If so, pass
  150. // it through
  151. if (casProxyCallbackUrl != null && casProxyCallbackUrl.endsWith(((HttpServletRequest) request).getRequestURI()) && request.getParameter("pgtId") != null && request.getParameter("pgtIou") != null) {
  152. log.trace("passing through what we hope is CAS's request for proxy ticket receptor.");
  153. fc.doFilter(request, response);
  154. return;
  155. }
  156. // Wrap the request if desired
  157. if (wrapRequest) {
  158. log.trace("Wrapping request with CASFilterRequestWrapper.");
  159. request = new CASFilterRequestWrapper((HttpServletRequest) request);
  160. }
  161. // 1.从当前web应用中拿到session
  162. HttpSession session = ((HttpServletRequest) request).getSession();
  163. // if our attribute's already present and valid, pass through the filter chain
  164. // 1.1.如果存在一个票据(令牌,凭证),就要跳到下一个过滤器链(去验证此票据的真实性,因为此票据的真实性是未知的)
  165. CASReceipt receipt = (CASReceipt) session.getAttribute(CAS_FILTER_RECEIPT);
  166. if (receipt != null && isReceiptAcceptable(receipt)) {
  167. log.trace("CAS_FILTER_RECEIPT attribute was present and acceptable - passing  request through filter..");
  168. fc.doFilter(request, response);
  169. return;
  170. }
  171. // otherwise, we need to authenticate via CAS
  172. // 1.2.如果receipt(令牌)不存在就先拿到ticket,我们要去cas验证用户进行登录
  173. String ticket = request.getParameter("ticket");
  174. // no ticket? abort request processing and redirect
  175. //如果ticket为空
  176. if (ticket == null || ticket.equals("")) {
  177. log.trace("CAS ticket was not present on request.");
  178. // 4.1判断是否经过网关参数(didGateway这个参数否已经经过网关的一个标记参数,表示不再进行认证)
  179. // did we go through the gateway already?
  180. boolean didGateway = Boolean.valueOf((String) session.getAttribute(CAS_FILTER_GATEWAYED)).booleanValue();
  181. // 4.1.1没有casLogin的配置信息下的异常处理
  182. if (casLogin == null) {
  183. // TODO: casLogin should probably be ensured to not be null at filter initialization. -awp9
  184. log.fatal("casLogin was not set, so filter cannot redirect request for authentication.");
  185. throw new ServletException("When CASFilter protects pages that do not receive a 'ticket' " + "parameter, it needs a edu.yale.its.tp.cas.client.filter.loginUrl " + "filter parameter");
  186. }
  187. // 4.2如果网关标记为false,设置CAS_FILTER_GATEWAYED属性为true,并跳转到cas服务端进行验证
  188. if (!didGateway) {
  189. log.trace("Did not previously gateway.  Setting session attribute to true.");
  190. session.setAttribute(CAS_FILTER_GATEWAYED, "true");
  191. redirectToCAS((HttpServletRequest) request, (HttpServletResponse) response);
  192. // abort chain
  193. return;
  194. } else {
  195. log.trace("Previously gatewayed.");
  196. // 4.3 如果有网关参数(之前已经通过了网关),就不再进行验证,从而进入下一个过滤器处理即可。
  197. // if we should be logged in, make sure validation succeeded
  198. if (casGateway || session.getAttribute(CAS_FILTER_USER) != null) {
  199. //已经通过了验证和授权。。
  200. log.trace("casGateway was true and CAS_FILTER_USER set: passing request along filter chain.");
  201. // continue processing the request 交给下一个过滤器
  202. fc.doFilter(request, response);
  203. return;
  204. } else {
  205. // 其他情况下,跳往cas服务端
  206. // unknown state... redirect to CAS
  207. //将经过网关的参数didGateway设置为true
  208. session.setAttribute(CAS_FILTER_GATEWAYED, "true");
  209. redirectToCAS((HttpServletRequest) request, (HttpServletResponse) response);
  210. // abort chain
  211. return;
  212. }
  213. }
  214. }
  215. try {
  216. // ticket存在,就经过getAuthenticatedUser()方法去拿到receipt,初步判断此方法是为根据request中的ticket参数组装了一个数据发送给了cas服务端进行判断此ticket是否是正确的合法的(它可能是使用代理类进行的实现)
  217. receipt = getAuthenticatedUser((HttpServletRequest) request);
  218. } catch (CASAuthenticationException e) {
  219. log.error(e);
  220. throw new ServletException(e);
  221. }
  222. if (!isReceiptAcceptable(receipt)) {
  223. //检测授权不被认可,就是非法的。
  224. throw new ServletException("Authentication was technically successful but rejected as a matter of policy. [" + receipt + "]");
  225. }
  226. //既然拿到了凭证,就去拿到session中是否有相关信息,并写入CASFilter.CAS_FILTER_RECEIPT
  227. // Store the authenticated user in the session
  228. if (session != null) { // probably unnecessary
  229. //将username(用户名)信息放入session中
  230. session.setAttribute(CAS_FILTER_USER, receipt.getUserName());
  231. //放入票据
  232. session.setAttribute(CASFilter.CAS_FILTER_RECEIPT, receipt);
  233. // don't store extra unnecessary session state
  234. //不要储存额外的不必要的会话状态
  235. session.removeAttribute(CAS_FILTER_GATEWAYED);
  236. }
  237. if (log.isTraceEnabled()) {
  238. log.trace("validated ticket to get authenticated receipt [" + receipt + "], now passing request along filter chain.");
  239. }
  240. // continue processing the request
  241. //进入下一个过滤器进行处理
  242. fc.doFilter(request, response);
  243. log.trace("returning from doFilter()");
  244. }
  245. /**
  246. * Is this receipt acceptable as evidence of authentication by credentials that would have been acceptable to this path? Current implementation checks whether from renew and whether proxy was authorized.
  247. *
  248. * @param receipt 票据
  249. * @return true if acceptable, false otherwise
  250. */
  251. private boolean isReceiptAcceptable(CASReceipt receipt) {
  252. if (receipt == null)
  253. throw new IllegalArgumentException("Cannot evaluate a null receipt.");
  254. if (this.casRenew && !receipt.isPrimaryAuthentication()) {
  255. return false;
  256. }
  257. if (receipt.isProxied()) {
  258. if (!this.authorizedProxies.contains(receipt.getProxyingService())) {
  259. return false;
  260. }
  261. }
  262. return true;
  263. }
  264. // *********************************************************************
  265. // Utility methods
  266. /**
  267. * Converts a ticket parameter to a CASReceipt, taking into account an optionally configured trusted proxy in the tier immediately in front of us.
  268. *
  269. * @throws ServletException -
  270. *             when unable to get service for request
  271. * @throws CASAuthenticationException -
  272. *             on authentication failure
  273. */
  274. private CASReceipt getAuthenticatedUser(HttpServletRequest request) throws ServletException, CASAuthenticationException {
  275. log.trace("entering getAuthenticatedUser()");
  276. ProxyTicketValidator pv = null;
  277. pv = new ProxyTicketValidator();
  278. pv.setCasValidateUrl(casValidate);
  279. pv.setServiceTicket(request.getParameter("ticket"));
  280. pv.setService(getService(request));
  281. pv.setRenew(Boolean.valueOf(casRenew).booleanValue());
  282. if (casProxyCallbackUrl != null) {
  283. pv.setProxyCallbackUrl(casProxyCallbackUrl);
  284. }
  285. if (log.isDebugEnabled()) {
  286. log.debug("about to validate ProxyTicketValidator: [" + pv + "]");
  287. }
  288. return CASReceipt.getReceipt(pv);
  289. }
  290. /**
  291. * Returns either the configured service or figures it out for the current request. The returned service is URL-encoded.
  292. */
  293. private String getService(HttpServletRequest request) throws ServletException {
  294. log.trace("entering getService()");
  295. String serviceString;
  296. // ensure we have a server name or service name
  297. if (casServerName == null && casServiceUrl == null)
  298. throw new ServletException("need one of the following configuration " + "parameters: edu.yale.its.tp.cas.client.filter.serviceUrl or " + "edu.yale.its.tp.cas.client.filter.serverName");
  299. // use the given string if it's provided
  300. if (casServiceUrl != null)
  301. serviceString = URLEncoder.encode(casServiceUrl);
  302. else
  303. // otherwise, return our best guess at the service
  304. serviceString = Util.getService(request, casServerName);
  305. if (log.isTraceEnabled()) {
  306. log.trace("returning from getService() with service [" + serviceString + "]");
  307. }
  308. return serviceString;
  309. }
  310. /**
  311. * Redirects the user to CAS, determining the service from the request.
  312. */
  313. private void redirectToCAS(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
  314. if (log.isTraceEnabled()) {
  315. log.trace("entering redirectToCAS()");
  316. }
  317. String casLoginString = casLogin + "?service=" + getService((HttpServletRequest) request) + ((casRenew) ? "&renew=true" : "") + (casGateway ? "&gateway=true" : "");
  318. if (log.isDebugEnabled()) {
  319. log.debug("Redirecting browser to [" + casLoginString + ")");
  320. }
  321. ((HttpServletResponse) response).sendRedirect(casLoginString);
  322. if (log.isTraceEnabled()) {
  323. log.trace("returning from redirectToCAS()");
  324. }
  325. }
  326. public String toString() {
  327. StringBuffer sb = new StringBuffer();
  328. sb.append("[CASFilter:");
  329. sb.append(" casGateway=");
  330. sb.append(this.casGateway);
  331. sb.append(" wrapRequest=");
  332. sb.append(this.wrapRequest);
  333. sb.append(" casAuthorizedProxies=[");
  334. sb.append(this.authorizedProxies);
  335. sb.append("]");
  336. if (this.casLogin != null) {
  337. sb.append(" casLogin=[");
  338. sb.append(this.casLogin);
  339. sb.append("]");
  340. } else {
  341. sb.append(" casLogin=NULL!!!!!");
  342. }
  343. if (this.casProxyCallbackUrl != null) {
  344. sb.append(" casProxyCallbackUrl=[");
  345. sb.append(casProxyCallbackUrl);
  346. sb.append("]");
  347. }
  348. if (this.casRenew) {
  349. sb.append(" casRenew=true");
  350. }
  351. if (this.casServerName != null) {
  352. sb.append(" casServerName=[");
  353. sb.append(casServerName);
  354. sb.append("]");
  355. }
  356. if (this.casServiceUrl != null) {
  357. sb.append(" casServiceUrl=[");
  358. sb.append(casServiceUrl);
  359. sb.append("]");
  360. }
  361. if (this.casValidate != null) {
  362. sb.append(" casValidate=[");
  363. sb.append(casValidate);
  364. sb.append("]");
  365. } else {
  366. sb.append(" casValidate=NULL!!!");
  367. }
  368. return sb.toString();
  369. }
  370. /* (non-Javadoc)
  371. * @see javax.servlet.Filter#destroy()
  372. */
  373. public void destroy() {
  374. // TODO Auto-generated method stub
  375. }
  376. }

注释都已经写在代码块里了

SSO单点登录系列1:cas客户端源码分析cas-client-java-2.1.1.jar相关推荐

  1. SSO单点登录系统搭建(附源码)

  2. 通用权限管理系统组件 中集成多个子系统的单点登录(网站入口方式)附源码

    通用权限管理系统组件 (GPM - General Permissions Manager) 中集成多个子系统的单点登录(网站入口方式)附源码 上文中实现了直接连接数据库的方式,通过配置文件,自定义的 ...

  3. TeamTalk客户端源码分析七

    TeamTalk客户端源码分析七 一,CBaseSocket类 二,select模型 三,样例分析:登录功能 上篇文章我们分析了network模块中的引用计数,智能锁,异步回调机制以及数据的序列化和反 ...

  4. 【深入浅出MyBatis系列十一】缓存源码分析

    为什么80%的码农都做不了架构师?>>>    #0 系列目录# 深入浅出MyBatis系列 [深入浅出MyBatis系列一]MyBatis入门 [深入浅出MyBatis系列二]配置 ...

  5. grpc-go客户端源码分析

    grpc-go客户端源码分析 代码讲解基于v1.37.0版本. 和grpc-go服务端源码分析一样,我们先看一段示例代码, const (address = "localhost:50051 ...

  6. Dubbo系列(二)源码分析之SPI机制

    Dubbo系列(二)源码分析之SPI机制 在阅读Dubbo源码时,常常看到 ExtensionLoader.getExtensionLoader(*.class).getAdaptiveExtensi ...

  7. 人人网官方Android客户端源码分析(1)

    ContentProvider是不同应用程序之间进行数据交换的标准API,ContentProvider以某种Uri的形式对外提供数据,允许其他应用访问或修改数据;其他应用程序使用ContentRes ...

  8. mosquitto客户端对象“struct mosquitto *mosq”管理下篇(mosquitto2.0.15客户端源码分析之四)

    文章目录 前言 5 设置网络参数 5.1 客户端连接服务器使用的端口号 `mosq->port` 5.2 指定绑定的网络地址 `mosq->bind_address` 5.3 客户端连接服 ...

  9. Eoe客户端源码分析---SlidingMenu的使用

    Eoe客户端源码分析及代码注释 使用滑动菜单SlidingMenu,单击滑动菜单的不同选项,可以通过ViewPager和PagerIndicator显示对应的数据内容. 0  BaseSlidingF ...

  10. SSO单点登录教程(四)自己动手写SSO单点登录服务端和客户端

    作者:蓝雄威,叩丁狼教育高级讲师.原创文章,转载请注明出处. 一.前言 我们自己动手写单点登录的服务端目的是为了加深对单点登录的理解.如果你们公司想实现单点登录/单点注销功能,推荐使用开源的单点登录框 ...

最新文章

  1. 如何迅速成为Java高手[Tomjava原创]
  2. (转)Kafka 消费者 Java 实现
  3. 关于配置Webapck的 exclude 不过滤 node_modules Babel却没有处理转换node_modules的源码
  4. mysql8安装步骤及排坑
  5. C语言C++中memset()函数使用和注意事项
  6. 华为n3计算机在哪里,在华为nova3i中连接电脑的两种方法介绍
  7. mac太烫会坏吗?Mac太烫怎么办?冷静下来,看完你就知道了
  8. vs2017远程编译linux教程,Visual Studio 2017 远程编译调试 Linux 上已存在的通过 Samba 共享的 CMake 工程...
  9. 号外号外!兹有第一届区块链技术及应用峰会(BTA)·中国首轮议题抢鲜看
  10. SLAM14讲学习笔记(一) 李群李代数基础
  11. MinIO客户端(mc命令)实现数据迁移
  12. 2022建筑电工(建筑特殊工种)考试题目模拟考试平台操作
  13. 读书笔记:《软件架构师应该知道的97件事》
  14. 如何用python绘制灰度直方图_用python简单处理图片(5):图像直方图
  15. oracle数据库事务日志已满,SQL Server中已满事务日志原因的检测(上)
  16. Informatica PowerCenter 简介(三)
  17. 为什么下水井盖是圆的
  18. RedHat RHEL7.2系统的详细步骤(图文)
  19. MySQL查询某个列中相同值的数量统计
  20. 深入浅出MySQL复制

热门文章

  1. 【tool】c/s和b/s的区别及实例说明
  2. 使用电脑时,眼睛离电脑多远才合适
  3. WordPress Cart66 Lite插件跨站请求伪造漏洞
  4. ubuntu 16.04: 配置ssh, vnc, ftp远程
  5. 在Mac上将WebP图像批量转换为JPG的方法
  6. 想满足一点小小的欲望,怎么就这么难……咦?这儿有戏!
  7. iOS UITableView设置UITableViewStyleGrouped模式下section间多余间距的处理
  8. Python基础之集合set
  9. vue路由vue-router的使用
  10. springmvc集成shiro例子