java jaas_基于Java JAAS表单的身份验证

java jaas

使用JAAS实现登录模块是一个高级主题,而且大多数开发人员也很少有机会参与这种开发。 但是JAAS登录模块的基本实现不是那么难实现,这是因为我打算将其发布。
在这里,我正在解释如何实现tomcat管理的身份验证模块。 此实现与容器无关。 我们可以将其与稍有更改的任何容器一起使用。
第一步,我们需要创建一个实现javax.security.auth.spi.LoginModule接口的登录模块类。 该接口提供了必须由我们的登录模块类实现的5种方法。 这些是initialize(),login(),commit(),abort(),logout()
initialize()方法传递了四个参数。
“主题”是什么,我们需要进行身份验证。 主题可以代表单个登录用户的相关信息。 它可以表示“用户名”,“密码”等身份。此外,它还可以表示分配给用户的角色。 所有这些身份都应表示为java.security.Principal。 因此,我们应该通过实现java.security.Principal创建单独的类来区分这些实体。 在本教程中,我为用户名,密码和角色创建了单独的类,分别为JAASUserPrincipal,JAASPasswordPrincipal和JAASRolePrincipal。 主题的getPrincipals()方法返回一组与主题关联的java.security.Principal。 为了区别这些,为每个标识创建单独的类很重要。
CallbackHandler ”用于与用户通信。 通过登录模块对用户进行身份验证时,登录模块将调用CallbackHandler实例的handle()方法以获取用户名和密码。 我们还不想担心CallbackHandler实例。 因为容器设法提供所需的callbakcs。 Tomcat为此提供了JAASCallbackHandler 。 但是,如果我们要显式调用身份验证,则需要通过实现javax.security.auth.callback.CallbackHandler来创建自己的回调处理程序类。 我将在本教程的结尾进行解释。
initialize()方法的下一个重要参数是'options'。 这些选项我们在' jass.config '文件中声明。 通过初始化登录模块,提供了在“ jass.config”文件中声明的选项的映射。 稍后我将解释本教程的“ jaas.config”文件。
接下来,我将显示我们的主体类的完整源代码。

JAASUserPrincipal.java

package com.rainyday.server.login;import java.io.Serializable;import java.security.Principal;/*** @author semika**/public class JAASUserPrincipal implements Principal, Serializable {private String name;/*** @param name*/public JAASUserPrincipal(String name) {if (name == null) {throw new NullPointerException("NULL user name");}this.name = name;}@Overridepublic String getName() {return name;}@Overridepublic String toString() {return "UserPrincipal [name=" + name + "]";}@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + ((name == null) ? 0 : name.hashCode());return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;JAASUserPrincipal other = (JAASUserPrincipal) obj;if (name == null) {if (other.name != null)return false;} else if (!name.equals(other.name))return false;return true;}
}

JAASRolePrincipal.java

package com.rainyday.server.login;import java.io.Serializable;import java.security.Principal;/*** @author semika**/public class JAASRolePrincipal implements Principal, Serializable {private String name;/*** @param name*/public JAASRolePrincipal(String name) {if (name == null) {throw new NullPointerException("NULL role name");}this.name = name;}@Overridepublic String getName() {return name;}@Overridepublic String toString() {return "JASSRolePrincipal [name=" + name + "]";}@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + ((name == null) ? 0 : name.hashCode());return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;JAASRolePrincipal other = (JAASRolePrincipal) obj;if (name == null) {if (other.name != null)return false;} else if (!name.equals(other.name))return false;return true;}
}

JAASPasswordPrincipal.java

package com.rainyday.server.login;import java.io.Serializable;import java.security.Principal;/*** @author semika**/public class JAASPasswordPrincipal implements Principal, Serializable {private String name;/*** @param name*/public JAASPasswordPrincipal(String name) {if (name == null) {throw new NullPointerException("NULL password.");}this.name = name;}@Overridepublic String getName() {return name;}@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + ((name == null) ? 0 : name.hashCode());return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;JAASPasswordPrincipal other = (JAASPasswordPrincipal) obj;if (name == null) {if (other.name != null)return false;} else if (!name.equals(other.name))return false;return true;}@Overridepublic String toString() {return "JAASPasswordPrincipal [name=" + name + "]";}}
以上三个类是完全相似的。 但是,我们需要为每个主体创建单独的类以区分它们。
登录模块的login()方法执行身份验证。 这将验证用户输入的数据库用户名和密码。 现在,您可能有一个问题,用户输入的登录详细信息如何进入login()方法。 如前所述,回调处理程序将登录标识带入login()方法。 从login()方法中,login模块通过将所需的回调传递到其中来调用回调处理程序的handle()方法。 然后handle()方法使用所需的信息填充这些回调,并使它们可用于login()方法。
成功验证后,登录模块将调用commit()方法。 可以使用相关的主体填充主题。 例如,我们可以从数据库中检索用户分配的角色,并将这些角色附加到主题中。
接下来,我将解释运行此登录模块所需的配置。
我们应该创建“ jass.config”文件,并将其放置在“ $ CATALINA_HOME / conf ”下。
本教程的“ jass.config”文件如下。
rainyDay {com.rainyday.server.login.JAASLoginModule requireddbDriver="com.mysql.jdbc.Driver"dbURL="jdbc:mysql://localhost/rainyday"dbUser="root"dbPassword="abc123"userQuery="select username from secu_user where secu_user.username=? and secu_user.password=?"roleQuery="select secu_user_role.rolename from secu_user, secu_user_role "+ "where secu_user.username=secu_user_role.username and secu_user.username=?"debug=true;
};

“ jass.config”文件应具有类似的格式。 除了登录模块声明之外,您还可以根据需要声明选项。 登录模块可以使用initialize()方法参数中的“ options”映射来使用这些选项。

另外,我们应该通过添加JAVA_OPTS环境变量的路径来告诉tomcat,“ jaas.config”文件的位置。 我将其添加到$ CATALINA_HOME / bin下的'catalina.sh'文件中,如下所示。

JAVA_OPTS =” $ JAVA_OPTS -Djava.security.auth.login.config == .. / conf / jaas.config”

接下来,您需要声明JAASRealm配置。 您可以在$ CATALINA_HOME / conf下的server.xml文件中添加一个新的“ Realm”条目。 在我们的教程中,“领域”条目如下。

<Realm className="org.apache.catalina.realm.JAASRealm"appName="rainyDay"userClassNames="com.rainyday.server.login.JASSUserPrincipal,com.rainyday.server.login.JAASPasswordPrincipal"roleClassNames="com.rainyday.server.login.JASSRolePrincipal"/>

有关apache tomcat的领域配置,您可以查看此文档 。 jaas登录模块的完整源代码。

JAASLoginModule.java

package com.rainyday.server.login;import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;import org.apache.log4j.Logger;/*** @author semika**/
public class JAASLoginModule implements LoginModule { private static Logger LOGGER = Logger.getLogger(JAASLoginModule.class); // initial stateprivate Subject subject;private CallbackHandler callbackHandler;private Map sharedState;private Map options;// configurable optionprivate boolean debug = false;// the authentication statusprivate boolean succeeded = false;private boolean commitSucceeded = false;//user credentialsprivate String username = null;private char[] password = null;//user principleprivate JAASUserPrincipal userPrincipal = null;private JAASPasswordPrincipal passwordPrincipal = null;public JAASLoginModule() {super();}@Overridepublic void initialize(Subject subject, CallbackHandler callbackHandler,Map<string, ?=""> sharedState, Map<string, ?=""> options) {this.subject = subject;this.callbackHandler = callbackHandler;this.sharedState = sharedState;this.options = options;debug = "true".equalsIgnoreCase((String)options.get("debug")); }@Overridepublic boolean login() throws LoginException {if (callbackHandler == null){throw new LoginException("Error: no CallbackHandler available " +"to garner authentication information from the user");}Callback[] callbacks = new Callback[2];callbacks[0] = new NameCallback("username");callbacks[1] = new PasswordCallback("password: ", false);try {callbackHandler.handle(callbacks);username = ((NameCallback)callbacks[0]).getName();password = ((PasswordCallback)callbacks[1]).getPassword();if (debug) {LOGGER.debug("Username :" + username);LOGGER.debug("Password : " + password);}if (username == null || password == null) {LOGGER.error("Callback handler does not return login data properly");throw new LoginException("Callback handler does not return login data properly"); }if (isValidUser()) { //validate user.succeeded = true;return true;} } catch (IOException e) { e.printStackTrace();} catch (UnsupportedCallbackException e) {e.printStackTrace();}return false;}@Overridepublic boolean commit() throws LoginException {if (succeeded == false) {return false;} else { userPrincipal = new JAASUserPrincipal(username);if (!subject.getPrincipals().contains(userPrincipal)) {subject.getPrincipals().add(userPrincipal);LOGGER.debug("User principal added:" + userPrincipal);}passwordPrincipal = new JAASPasswordPrincipal(new String(password)); if (!subject.getPrincipals().contains(passwordPrincipal)) {subject.getPrincipals().add(passwordPrincipal);LOGGER.debug("Password principal added: " + passwordPrincipal);}//populate subject with roles.List<string> roles = getRoles();for (String role: roles) {JAASRolePrincipal rolePrincipal = new JAASRolePrincipal(role);if (!subject.getPrincipals().contains(rolePrincipal)) {subject.getPrincipals().add(rolePrincipal); LOGGER.debug("Role principal added: " + rolePrincipal);}}commitSucceeded = true;LOGGER.info("Login subject were successfully populated with principals and roles"); return true;}}@Overridepublic boolean abort() throws LoginException {if (succeeded == false) {return false;} else if (succeeded == true && commitSucceeded == false) {succeeded = false;username = null;if (password != null) {password = null;}userPrincipal = null;    } else {logout();}return true;}@Overridepublic boolean logout() throws LoginException {subject.getPrincipals().remove(userPrincipal);succeeded = false;succeeded = commitSucceeded;username = null;if (password != null) {for (int i = 0; i < password.length; i++){password[i] = ' ';password = null;}}userPrincipal = null;return true;}private boolean isValidUser() throws LoginException {String sql = (String)options.get("userQuery");Connection con = null;ResultSet rs = null;PreparedStatement stmt = null;try {con = getConnection();stmt = con.prepareStatement(sql);stmt.setString(1, username);stmt.setString(2, new String(password));rs = stmt.executeQuery();if (rs.next()) { //User exist with the given user name and password.return true;}} catch (Exception e) {LOGGER.error("Error when loading user from the database " + e);e.printStackTrace();} finally {try {rs.close();} catch (SQLException e) {LOGGER.error("Error when closing result set." + e);}try {stmt.close();} catch (SQLException e) {LOGGER.error("Error when closing statement." + e);}try {con.close();} catch (SQLException e) {LOGGER.error("Error when closing connection." + e);}}return false;}/*** Returns list of roles assigned to authenticated user.* @return*/private List<string> getRoles() { Connection con = null;ResultSet rs = null;PreparedStatement stmt = null;List<string> roleList = new ArrayList<string>(); try {con = getConnection();String sql = (String)options.get("roleQuery");stmt = con.prepareStatement(sql);stmt.setString(1, username);rs = stmt.executeQuery();if (rs.next()) { roleList.add(rs.getString("rolename")); }} catch (Exception e) {LOGGER.error("Error when loading user from the database " + e);e.printStackTrace();} finally {try {rs.close();} catch (SQLException e) {LOGGER.error("Error when closing result set." + e);}try {stmt.close();} catch (SQLException e) {LOGGER.error("Error when closing statement." + e);}try {con.close();} catch (SQLException e) {LOGGER.error("Error when closing connection." + e);}}return roleList;}/*** Returns JDBC connection* @return* @throws LoginException*/private Connection getConnection() throws LoginException {String dBUser = (String)options.get("dbUser");String dBPassword = (String)options.get("dbPassword");String dBUrl = (String)options.get("dbURL");String dBDriver = (String)options.get("dbDriver");Connection con = null;try {//loading driverClass.forName (dBDriver).newInstance();con = DriverManager.getConnection (dBUrl, dBUser, dBPassword);} catch (Exception e) {LOGGER.error("Error when creating database connection" + e);e.printStackTrace();} finally {}return con;}
}
如果login()或commit()方法执行中出现问题,将调用login模块的abort()方法。 在这种情况下,如果身份验证过程失败,我们不能说身份验证过程已成功完成,并且可以在abort()方法内完成所需的清理操作。
我们可以利用在登录模块的initialize()方法中初始化的“选项”映射来获取在“ jass.config”文件中声明的配置信息。 您可以想出一种好的技术来获取JDBC连接对象。 在本教程中,我并没有专心于此,而只是想向您展示机制,即如何完成事情。
至此,我们已经完成了基本JAAS身份验证模块所需的工作。 接下来,我们应该在web.xml文件中配置安全约束。
<login-config><auth-method>FORM</auth-method><realm-name>rainyDay</realm-name><form-login-config><form-login-page>/login.jsp</form-login-page><form-error-page>/error.jsp</form-error-page></form-login-config>
</login-config>
<security-role><role-name>*</role-name>
</security-role>
<security-constraint><web-resource-collection><web-resource-name>Rainy day</web-resource-name><url-pattern>/</url-pattern><http-method>POST</http-method><http-method>GET</http-method></web-resource-collection><auth-constraint><role-name>*</role-name></auth-constraint>
</security-constraint>

由于上述安全限制,如果在没有身份验证的情况下对应用程序受保护区域中的特定资源提出了一些请求,则该请求将被重定向到“登录”页面。 接下来,我将向您展示简单HTML表单,该表单将在表单提交后调用我们的登录模块。

<form id="loginForm" name="loginForm" method="post" action="j_security_check">User Name : <input id="username" type="text" name="j_username" class="textbox"></input>Password : <input id="password" type="password" name="j_password" class="textbox"></input><input name="login" type="submit" value="LOGIN" id="submit" class="button blue"></form>
到此,我们完成了。这是JAAS的非常基本的实现。 这种JAAS模块的优点是,我们只需更改一次配置即可更改为其他登录模块实现,而无需对现有代码进行任何修改。 而且这是与容器无关的。 如果要使用jBoss服务器而不是“ jass.config”来部署它,则可以使用jboss conf文件夹中的“ login-config.xml”文件。 正如我答应过的,这里就是如何显式调用这种登录模块。 在某些情况下,我们需要以实用的方式对特定用户进行身份验证,但仍然应该使用已实现的登录模块。 在这种情况下,最大的问题是向我们的登录模块提供用户身份(用户名,密码等)。 在上述情况下,我们使用了一个“ CallbackHandler”类,该类是apache catalina提供的“ JAASCallbackHander”。 但是,在这里我们不能使用和我们必须实现我们自己的回调处理程序类。

JAASCallbackHandler.java

package com.rainyday.server.login;import java.io.IOException;import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;import org.apache.log4j.Logger;/*** @author semika**/public class JAASCallbackHandler implements CallbackHandler {private static final Logger LOGGER = Logger.getLogger(JAASCallbackHandler.class);private String username = null;private String password = null;/*** @param username* @param password*/public JAASCallbackHandler(String username, String password) {this.username = username;this.password = password;}@Overridepublic void handle(Callback[] callbacks) throws IOException,UnsupportedCallbackException {LOGGER.info("Callback Handler invoked ");for (int i = 0; i < callbacks.length; i++) {if (callbacks[i] instanceof NameCallback) {NameCallback nameCallback = (NameCallback) callbacks[i];nameCallback.setName(username);} else if (callbacks[i] instanceof PasswordCallback) {PasswordCallback passwordCallback = (PasswordCallback) callbacks[i];passwordCallback.setPassword(password.toCharArray());} else {throw new UnsupportedCallbackException(callbacks[i], "The submitted Callback is unsupported");}}}
}

接下来,我们必须创建一个' LoginContext'实例来显式调用身份验证。

LoginContext lc = null;try {lc = new LoginContext("rainyDay", new JAASCallbackHandler(username, password));lc.login();//get the subject.Subject subject = lc.getSubject();//get principalssubject.getPrincipals();LOGGER.info("established new logincontext");} catch (LoginException e) {LOGGER.error("Authentication failed " + e);}
如果我们最终无一例外地执行了以上代码,则表明身份验证已成功。 如果遇到异常,则认证失败。
这就是本教程中的全部内容。 http://docs.oracle.com/javaee/1.4/tutorial/doc/Security5.html是我了解登录身份验证的很好的教程。

参考:来自我们的JCG合作伙伴 Semika loku kaluge的基于Java表单的身份验证 ,位于Code Box博客上。


翻译自: https://www.javacodegeeks.com/2012/06/java-jaas-form-based-authentication.html

java jaas

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/354653.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在ubuntu上如何将多张图片或PDF合到一个PDF上

在日常工作中我们可能需要将不同的PDF或图像合并为一个PDF上&#xff0c;这种情况并不少见。 这时候我们需要学习使用一个非常强大的命令行工具ImageMagick。通过apt命令下载安装ImageMagick在Ubuntu 20.04版本中已经预装了ImageMagick&#xff0c;因为有许多软件包使用此工具作…

使用Java 8在地图上流式传输

在本文中&#xff0c;我将向您展示如何在标准Java映射上有效地实现Speedment Open Source流&#xff0c;并将Stream接口扩展为MapStream&#xff01; 即使在复杂的情况下&#xff0c;此添加将使保持流的具体性和可读性变得更加容易。 希望这将允许您继续流式传输而不会过早收集…

日期差值 1096

问题描述 链接&#xff1a;https://www.nowcoder.com/questionTerminal/ccb7383c76fc48d2bbc27a2a6319631c 有两个日期&#xff0c;求两个日期之间的天数&#xff0c;如果两个日期是连续的我们规定他们之间的天数为两天 输入描述: 有多组数据&#xff0c;每组数据有两行&#x…

如何使用python给PDF文件加水印

Python作为编程界最火的语言&#xff0c;能做的事几乎你能想到的它都能干&#xff0c;就连抢茅台都可以&#xff0c;还有什么不行&#xff1f;&#xff01;Python作为脚本编程语言&#xff0c;可以做很多事情。使用Python&#xff0c;你可以轻松地给pdf加上水印。 你可以使用名…

搭建一个redis高可用系统

一、单个实例 当系统中只有一台redis运行时&#xff0c;一旦该redis挂了&#xff0c;会导致整个系统无法运行。 单个实例二、备份 由于单台redis出现单点故障&#xff0c;就会导致整个系统不可用&#xff0c;所以想到的办法自然就是备份&#xff08;一般工业界认为比较安全的备…

建设者还是二传手?

不用说&#xff0c;每个对象都需要先创建才能使用。 无论我们是在谈论域&#xff0c;框架&#xff0c;库还是任何其他类型的类&#xff0c;都没有关系。 当您的代码是面向对象的时&#xff0c;这些类仅是对象的定义。 在创建对象之前&#xff0c;不能使用它们。 在谈论对象的初…

SSH连接远程服务器,本地known_hosts文件记录了什么

今天工作时&#xff0c;使用ssh命令远程连接公司的本地服务器时&#xff0c;突然出现以下错误bash-3.2$ ssh argus192.168.200.8 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdroppin…

“全人类的知识宝藏”维基百科迎来了20岁的生日!

维基百科从一个伟大的想法开始&#xff0c;与无数的像你像我一样的阅读者&#xff0c;创作者&#xff0c;捐赠者和粉丝经历了互联网的20年&#xff0c;今天让我们一起为这个属于所有互联网人的成果庆祝一次生日。值此20周年特地为它做了一个主页&#xff1a;https://wikimediaf…

转:智能音箱市场深度报告:怎么大家都在抢这个两亿小蛋糕?

原文链接&#xff1a;http://www.sohu.com/a/199335366_115978 智能音箱是今年最热的智能硬件项目之一。目前&#xff0c;智能音箱已经有了比较成熟的技术方案和模式思路&#xff0c;但消费市场似乎依然秉持着比较谨慎的态度。智能音箱市场上的主流产品都有什么思路&#xff1f…

Tailwindcss尤大神都fork了,是未来的趋势?

最近Tailwindcss频繁出现在我的视野里&#xff0c;从单词拼写中看&#xff0c;多多少少与css有点关系。近几年是JS框架大行其道&#xff0c;CSS方面少有新的框架出现。昨天突然看到尤大神在Github上的动态&#xff0c;fork了该项目&#xff0c;看来马上要火的节奏啊&#xff01…

JUnit 5 –架构

现在我们知道如何设置JUnit 5并使用它编写一些测试 &#xff0c;下面让我们看一下。 在本文中&#xff0c;我们将讨论JUnit 5架构以及采用这种方式的原因。 总览 这篇文章是有关JUnit 5的系列文章的一部分&#xff1a; 设定 基本 建筑 条件 注射 … JUnit 4 忽略Hamcre…

前端程序员书桌上不可缺少的CSS书籍

作为前端&#xff0c;CSS不仅要会&#xff0c;而且要精通&#xff0c;随着各种浏览器规范参差不齐和网页交互多元化的趋势越来越复杂&#xff0c;前端程序员必须要将CSS基础知识打牢。由于现在的框架越来越多&#xff0c;导致很大一部分程序员的工作只是拿着现成的组件布局&…

nodejs 进阶:图片缩小

demo 效果&#xff1a; 代码&#xff1a; /*** Created by ZXW on 2017/10/30.*/ var fs require(fs); var gm require(gm);gm(./不饿.jpg).resize(50, 50,"!").write(./不饿1.jpg, function (err) {if (!err) console.log(done);});2017-10-30 22:10:46转载于:ht…

可能是最先出来的关于介绍使用Vue3的一本书

Vue3 release版本已发布有几个月了&#xff0c;不少公司都已经开始使用vue3开发项目了&#xff0c;市场上的主流的框架如&#xff1a;Vant&#xff0c;Element UI&#xff0c;Taro也都发布了支持Vue3的版本。Vue3很多的开发优势自不必再说&#xff0c;学习上手vue3已经成为每个…

JPA陷阱/错误

根据我在帮助团队和进行培训方面的经验&#xff0c;这是我遇到的一些陷阱/错误&#xff0c;这些陷阱/错误在使用JPA的基于Java的系统中引起了一些问题。 需要一个公共的无参数构造函数 始终使用双向关联/关系 使用OneToMany收集可能庞大的集合 需要一个公共的无参数构造函数…

CSGL

glShadeModel void glShadeModel(GLenum mode) GL_FLAT/【GL_SMOOTH】 着色技术选择 glClearDepth GL.glClearDepth(depth); glClearDepth&#xff1a;设置深度缓存的清除值 参数 depth 指定清除深度缓存时使用的深度值。 说明 本函数指定用glClear清除深度缓存时所使用的深度值…

强大的Canvas开源库Fabric.js简介与开发指南

什么是Fabric.js&#xff1f;Fabric.js 是一个强大且简单的Javascript HTML5 Canvas库。官网地址&#xff1a;http://fabricjs.com/为什么要使用Fabric.js&#xff1f;Canvas提供一个好的画布能力, 但是Api不够友好。绘制简单图形其实还可以, 不过做一些复杂的图形绘制, 编写一…

模拟qq斗地主-准备发牌抢地主都是农民下一轮准备

为什么要搞这样一个项目&#xff1f;&#xff1f; 1&#xff0c;满足自己的java网络多线程编程的欲望&#xff01;因为之前一直都是搞web开发&#xff0c;服务器和客户端数据交流人家web服务器早就给你搞好了&#xff0c;比如tomcat,jetty...等等&#xff0c;其实之前脑子里就有…

rube3xxx_Rube GoldbergSpring整合

rube3xxxSpring Integration为集成系统所涉及的一些复杂性提供了非常好的抽象-Spring Integration从Integration的角度完美地满足了Facade的定义-简化了对复杂基础系统的访问。 为了说明这一点&#xff0c;请考虑一个简单的系统&#xff0c;该系统仅接收一条消息&#xff0c;然…

纯CSS实现React Logo图形,内含详细解析

以上是将要实现的效果&#xff0c;Javascript框架React的Logo图形&#xff0c;首先我们来拆解下&#xff0c;它包括三个交叉的椭圆和中间一个圆点&#xff0c;所以我们Html元素可以用以下代码实现&#xff1a;<div class"main"><div class"ellipse ell…