├── .gitignore ├── README.md ├── pom.xml ├── src └── main │ ├── java │ └── org │ │ └── lintx │ │ └── plugin │ │ └── webauth │ │ ├── Commands.java │ │ ├── Config.java │ │ ├── Listeners.java │ │ ├── Message.java │ │ ├── WebAuth.java │ │ ├── config │ │ └── DatabaseConfig.java │ │ ├── httpserver │ │ ├── Caches.java │ │ ├── InputModel.java │ │ ├── Messages.java │ │ ├── NettyHttpHandler.java │ │ ├── NettyHttpServer.java │ │ └── OutputModel.java │ │ ├── models │ │ ├── PlayerHistoryModel.java │ │ └── PlayerModel.java │ │ ├── sql │ │ ├── Model.java │ │ ├── MySql.java │ │ ├── SQLite.java │ │ └── SqlInterface.java │ │ └── utils │ │ ├── MojangApi.java │ │ └── Utils.java │ └── resources │ └── bungee.yml └── websource ├── package.json ├── src ├── html │ └── index.html └── static │ ├── css │ └── index.scss │ ├── js │ └── index.js │ └── vue │ ├── changePassword.vue │ ├── changePlayerName.vue │ ├── closeChangePlayerName.vue │ ├── closeRegister.vue │ ├── help.vue │ ├── index.vue │ ├── login.vue │ ├── me.vue │ ├── openChangePlayerName.vue │ ├── openRegister.vue │ └── register.vue └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | libs 3 | target 4 | WebAuth.iml 5 | websource/.idea 6 | websource/node_modules 7 | websource/package-lock.json 8 | src/main/resources/web -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### WebAuth 更好的离线登录插件 2 | 3 | #### 优点 4 | 1. 和正版登录共存。 5 | 2. 进入游戏前完成登录校验,有效防止压测假人、权限问题;玩家进入游戏后不用输入繁琐的指令,享受正版玩家的游戏体验。 6 | 3. 可以修改玩家名而玩家数据不丢失。 7 | 4. 关闭注册后可以实现已注册玩家自动“白名单”,未注册玩家无法进入游戏的效果。 8 | 5. 没有第三方服务器依赖。 9 | 10 | #### 安装方法 11 | 1. 下载本插件,并将插件复制到BungeeCord的plugins目录中。 12 | 2. 下载前置插件[ConfigureCore](https://github.com/lintx/ConfigureCore-for-Minecraft-plugins)并将它也复制到BungeeCord的plugins目录中。 13 | 3. 如果您使用SQLite作为数据库,那么还需要安装BungeeCord端的SQLite支持插件比如[SQLite for BungeeCord](https://www.spigotmc.org/resources/sqlite-for-bungeecord.57191/)(注意:安装这个插件服务端需要重启否则无法安装成功)。 14 | 4. 重启BungeeCord服务端或使用BungeePluginManager插件加载本插件(需要你的BungeeCord安装了这个插件)。 15 | 5. 修改配置文件,注意webPort需要修改到1024以上,且需要打开该端口的防火墙以使得外部网络可以访问这个端口。 16 | 6. 使用命令`/webauth reload`重新加载配置文件。 17 | 7. 完成。 18 | 19 | 注意: 20 | 1. 本插件需要安装到BungeeCord中。 21 | 2. 需要使用类似Spigot、Paper之类支持BungeeCord端口转发的服务端。 22 | 3. 如果子服是基于Spigot的服务端,需要将`spigot.yml`的`settings`中的`bungeecord`设置为`true`。 23 | 4. BungeeCord端的`config.yml`的`ip_forward`要设置为`true`。 24 | 25 | 26 | #### 配置文件 27 | ```yaml 28 | #当玩家因为没有注册而无法登录游戏时的提示{url}会被替换为tokenManageUrl字段的内容,下同(该设置仅在BungeeCord的online_mode设置为false时有效) 29 | notRegister: |- 30 | 你还没有注册,请打开网页 31 | {url} 32 | 注册后登录游戏 33 | #登录凭据无效时,无法进入游戏的提示 34 | tokenIsValid: |- 35 | 登录凭证无效,请检查您的凭证 36 | 如忘记凭证请打开网页 37 | {url} 38 | 重置凭证 39 | #登录凭据过期时,无法进入游戏的提示 40 | tokenIsExpired: |- 41 | 登录凭证已过期,请打开网页 42 | {url} 43 | 重置凭证 44 | #玩家名中含有不允许的字符时,无法进入游戏的提示 45 | nameIsValid: |- 46 | 玩家名中含有不允许的字符,请打开网页 47 | {url} 48 | 修改玩家名或联系管理员 49 | #插件web端网址,请自行修改为自己的网址 50 | tokenManageUrl: 'http://localhost:9000/' 51 | #数据库配置 52 | databaseConfig: 53 | type: SQLITE #SQLITE或MYSQL,表示是使用SQLite作为数据库还是使用MySQL数据库(注意使用MySQL时帐号需要有创建表权限) 54 | #type为SQLITE时以下3个设置不需要设置 55 | #MySQL连接地址,localhost:MySQL服务器地址,3306:MySQL端口,database:数据库名,其他内容一般不修改 56 | mysqlUri: jdbc:mysql://localhost:3306/database?autoReconnect=true&useSSL=false&characterEncoding=utf-8&useUnicode=true 57 | mysqlUser: root #MySQL用户名,注意需要有创建数据表权限 58 | mysqlPassword: root #MySQL用户密码 59 | webPort: 9000 #玩家登录系统的web端口,1024以上(1024以下需要root权限),不能使用已经被占用的端口 60 | openRegister: false #是否开放注册,开放注册后玩家可以自行在web端注册帐号并进入游戏 61 | openChangePlayername: false #是否允许修改玩家名,允许修改后玩家可以自行在web端修改名字 62 | userNameRegexp: ^[a-zA-Z0-9\u4e00-\u9fa5]+$ #玩家名允许的规则,正则表达式,默认允许大小写英文字母、数字、中文,如果只允许大小写字母和数字,可以修改为^[a-zA-Z0-9]+$,对正则表达式不熟悉的不推荐修改这个配置 63 | ``` 64 | 关于mssql数据库配置: 65 | mysql的uri配置为jdbc:mysql://`localhost`:`3306`/`database`?autoReconnect=true&useSSL=false&characterEncoding=utf-8&useUnicode=true这样的格式 66 | 其中,有3处需要修改,分别是`localhost`(数据地址,本机填localhost或127.0.0.1)、`3306`(数据库端口,mysql数据库默认端口为3306)和`database`(数据库名,请修改为自己的数据库名) 67 | 68 | 69 | 70 | #### 其他 71 | 1. 玩家登录时用户名或密码错误超过5次后10分钟内无法登录。 72 | 2. 玩家注册后10分钟内无法注册。 73 | 3. 玩家登录后10分钟内无需重新登录。 74 | 4. 如果需要修改web端界面的,可以自行修改,然后将修改后的文件放入插件目录下的web目录中即可,但是请遵循api规则。 75 | 5. 假如您服务器的ip地址为1.1.1.1,web端口设置为9000,那么默认情况下使用`http://1.1.1.1:9000`和`http://1.1.1.1:9000/bungeewebauth/`都可以访问,并且其他资源都在`http://1.1.1.1:9000/bungeewebauth/`下,您可以非常方便的将web端地址映射为您服务器网页的一个二级目录下或者二级域名下。 76 | 6. 如需使用https,或者隐藏端口,请使用nginx、apache等软件的转发功能。 77 | 7. web端登录时,用户名和密码均区分大小写。 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.lintx.plugin 8 | WebAuth 9 | 1.4 10 | 11 | 12 | 13 | 14 | org.apache.maven.plugins 15 | maven-compiler-plugin 16 | 3.1 17 | 18 | 1.8 19 | 1.8 20 | 21 | 22 | 23 | org.apache.maven.plugins 24 | maven-shade-plugin 25 | 3.1.0 26 | 27 | 28 | package 29 | 30 | shade 31 | 32 | 33 | 34 | 35 | org.bstats 36 | org.lintx.plugins.webauth.maven.bstats 37 | 38 | 39 | org.lintx.plugins.modules 40 | org.lintx.plugins.webauth.maven.modules 41 | 42 | 43 | true 44 | 45 | 46 | org.bstats 47 | org.lintx.plugins.modules 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | true 58 | src/main/resources 59 | 60 | 61 | 62 | 63 | 64 | CodeMC 65 | https://repo.codemc.io/repository/maven-public/ 66 | 67 | 68 | 69 | 70 | org.projectlombok 71 | lombok 72 | 1.16.16 73 | provided 74 | 75 | 76 | net.md-5.bungee 77 | BungeeCord 78 | 1.0 79 | system 80 | ${project.basedir}/libs/BungeeCord.jar 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.bstats 91 | bstats-bungeecord 92 | 1.5 93 | compile 94 | 95 | 96 | org.lintx.plugins.modules 97 | Configure 98 | 1.2.3-SNAPSHOT 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/Commands.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth; 2 | 3 | import net.md_5.bungee.api.CommandSender; 4 | import net.md_5.bungee.api.chat.ClickEvent; 5 | import net.md_5.bungee.api.chat.TextComponent; 6 | import net.md_5.bungee.api.plugin.Command; 7 | import org.lintx.plugin.webauth.httpserver.Caches; 8 | import org.lintx.plugin.webauth.httpserver.Messages; 9 | import org.lintx.plugin.webauth.models.PlayerModel; 10 | import org.lintx.plugin.webauth.utils.MojangApi; 11 | import org.lintx.plugin.webauth.utils.Utils; 12 | 13 | import java.io.*; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.UUID; 16 | import java.util.regex.Matcher; 17 | import java.util.regex.Pattern; 18 | 19 | public class Commands extends Command { 20 | private WebAuth auth; 21 | 22 | public Commands(WebAuth auth,String name, String permission, String... aliases) { 23 | super(name, permission, aliases); 24 | this.auth = auth; 25 | } 26 | 27 | @Override 28 | public void execute(CommandSender sender, String[] args) { 29 | if (!sender.hasPermission("webauth")){ 30 | sender.sendMessage(new TextComponent(Message.noPermission)); 31 | return; 32 | } 33 | if (args.length>0){ 34 | String child = args[0]; 35 | if (child.equalsIgnoreCase("player")){ 36 | if (args.length>=2){ 37 | String _id = args[1]; 38 | if (_id.equalsIgnoreCase("add")){ 39 | if (args.length>=4){ 40 | String username = args[2]; 41 | if (!auth.getModel().checkPlayerUsername(username)){ 42 | sender.sendMessage(new TextComponent(Message.playerNameRepeat)); 43 | return; 44 | } 45 | if (username.length()<4){ 46 | sender.sendMessage(new TextComponent(Message.playerNameShort)); 47 | return; 48 | } 49 | if (username.getBytes().length>16){ 50 | sender.sendMessage(new TextComponent(Message.playerNameLong)); 51 | return; 52 | } 53 | 54 | String password = args[3]; 55 | PlayerModel model = new PlayerModel(username,password); 56 | if (Config.getInstance().isCheckPlayerNameFromMojang()){ 57 | MojangApi.MojangAccount account = MojangApi.getMojangAccount(username); 58 | if (!account.checkName(username)){ 59 | if (args.length == 4 || (!args[4].equalsIgnoreCase("confirm") && !args[4].equalsIgnoreCase("ignore"))){ 60 | sender.sendMessage(new TextComponent(Message.addMojangPlayer.replaceAll("\\{name\\}",username).replaceAll("\\{uuid\\}",account.id))); 61 | return; 62 | } 63 | if (args[4].equalsIgnoreCase("confirm")){ 64 | model.setUuid(account.getUUID()); 65 | } 66 | } 67 | } 68 | 69 | if (auth.getModel().insertPlayer(model)){ 70 | model = auth.getModel().getPlayerWithPlayerName(username); 71 | sender.sendMessage(new TextComponent(Message.addPlayerSuccess)); 72 | sender.sendMessage(new TextComponent(Message.playerInfo(model))); 73 | }else { 74 | sender.sendMessage(new TextComponent(Message.addPlayerFail)); 75 | } 76 | return; 77 | } 78 | sender.sendMessage(new TextComponent(Message.commandHelp)); 79 | return; 80 | } 81 | PlayerModel model = null; 82 | Pattern pattern = Pattern.compile("^\\d+$"); 83 | Matcher isNum = pattern.matcher(_id); 84 | if (isNum.matches()){ 85 | int id = Integer.parseInt(_id); 86 | model = auth.getModel().getPlayerWithId(id); 87 | }else { 88 | model = auth.getModel().getPlayerWithPlayerName(_id); 89 | } 90 | if (model==null){ 91 | sender.sendMessage(new TextComponent(Message.playerNotFind)); 92 | return; 93 | } 94 | 95 | if (args.length>=4){ 96 | String action = args[2]; 97 | String val = args[3]; 98 | if (action.equalsIgnoreCase("password")){ 99 | model.updatePassword(val); 100 | sender.sendMessage(new TextComponent(Message.passwordUpdateSuccess)); 101 | return; 102 | }else if (action.equalsIgnoreCase("uuid")) { 103 | UUID uuid; 104 | try { 105 | uuid = UUID.fromString(val); 106 | }catch (IllegalArgumentException ignore){ 107 | sender.sendMessage(new TextComponent(Message.uuidIsValid)); 108 | return; 109 | } 110 | if (auth.getModel().getPlayerWithUUID(uuid)!=null){ 111 | sender.sendMessage(new TextComponent(Message.uuidIsRepeat)); 112 | return; 113 | } 114 | model.setUuid(uuid); 115 | auth.getModel().updatePlayer(model); 116 | model = auth.getModel().getPlayerWithId(model.getId()); 117 | sender.sendMessage(new TextComponent(Message.uuidUpdateSuccess)); 118 | sender.sendMessage(new TextComponent(Message.playerInfo(model))); 119 | return; 120 | }else if (action.equalsIgnoreCase("playername")){ 121 | if (!auth.getModel().checkPlayerUsername(val)){ 122 | sender.sendMessage(new TextComponent(Message.playerNameRepeat)); 123 | return; 124 | } 125 | if (val.length()<4){ 126 | sender.sendMessage(new TextComponent(Message.playerNameShort)); 127 | return; 128 | } 129 | if (val.getBytes().length>16){ 130 | sender.sendMessage(new TextComponent(Message.playerNameLong)); 131 | return; 132 | } 133 | model.setName(val); 134 | auth.getModel().updatePlayer(model); 135 | model = auth.getModel().getPlayerWithId(model.getId()); 136 | sender.sendMessage(new TextComponent(Message.playerNameUpdateSuccess)); 137 | sender.sendMessage(new TextComponent(Message.playerInfo(model))); 138 | return; 139 | }else if (action.equalsIgnoreCase("token")){ 140 | if (val.equalsIgnoreCase("refresh")){ 141 | int day = 7; 142 | if (args.length>=5 && pattern.matcher(args[4]).matches()){ 143 | day = Integer.parseInt(args[4]); 144 | } 145 | String token = Utils.newToken(); 146 | model.updateToken(token,day); 147 | auth.getModel().updatePlayer(model); 148 | String text = Message.tokenUpdateSuccess.replaceAll("\\{token\\}",token); 149 | text = text.replaceAll("\\{expire_time\\}",model.getToken_timeString()); 150 | TextComponent message = new TextComponent(text); 151 | message.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND,token)); 152 | sender.sendMessage(message); 153 | return; 154 | } 155 | } 156 | }else { 157 | sender.sendMessage(new TextComponent(Message.playerInfo(model))); 158 | return; 159 | } 160 | } 161 | }else if (child.equalsIgnoreCase("reload")){ 162 | auth.reload(); 163 | sender.sendMessage(new TextComponent(Message.reloadConfig)); 164 | return; 165 | }else if (child.equalsIgnoreCase("import")){ 166 | if (args.length>=2){ 167 | String filename = args[1]; 168 | auth.getProxy().getScheduler().runAsync(auth, () -> importPlayerData(sender,filename)); 169 | return; 170 | } 171 | } 172 | } 173 | sender.sendMessage(new TextComponent(Message.commandHelp)); 174 | } 175 | 176 | private void importPlayerData(CommandSender sender,String filename){ 177 | File file = new File(auth.getDataFolder(),filename); 178 | if (!file.exists()){ 179 | sender.sendMessage(new TextComponent(Message.fileNotExist)); 180 | return; 181 | } 182 | if (!file.isFile()){ 183 | sender.sendMessage(new TextComponent(Message.fileNotFile)); 184 | return; 185 | } 186 | sender.sendMessage(new TextComponent("检测到文件存在,开始处理数据,请稍后")); 187 | String logFilename = filename + ".log"; 188 | File logFile = new File(auth.getDataFolder(),logFilename); 189 | 190 | try (InputStream inputStream = new FileInputStream(file)) { 191 | Reader r = new InputStreamReader(inputStream, StandardCharsets.UTF_8); 192 | BufferedReader br = new BufferedReader(r); 193 | 194 | BufferedWriter writer = new BufferedWriter(new FileWriter(logFile)); 195 | String line; 196 | while ((line = br.readLine()) != null) { 197 | sender.sendMessage(new TextComponent(addUser(line, writer))); 198 | } 199 | br.close(); 200 | r.close(); 201 | writer.close(); 202 | } catch (IOException ignored) { 203 | 204 | } 205 | sender.sendMessage(new TextComponent("数据处理完毕,记录已经存储在" + logFilename + "中,请分发密码后提醒玩家及时修改密码。")); 206 | } 207 | 208 | private String addUser(String line,BufferedWriter writer){ 209 | String[] arr = line.split("\\s+"); 210 | String name = arr[0]; 211 | 212 | if (name.length()<4){ 213 | return addUserResultMessage(writer,line,false,"玩家名长度太短"); 214 | } 215 | if (name.getBytes().length>16){ 216 | return addUserResultMessage(writer,line,false,"玩家名长度太长"); 217 | } 218 | if (!auth.getModel().checkPlayerUsername(name)){ 219 | return addUserResultMessage(writer,line,false,"玩家名已经存在"); 220 | } 221 | 222 | UUID uuid; 223 | if (arr.length>=2){ 224 | try { 225 | uuid = UUID.fromString(arr[1]); 226 | }catch (IllegalArgumentException ignored){ 227 | return addUserResultMessage(writer,line,false,"UUID格式不正确"); 228 | } 229 | }else { 230 | uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8)); 231 | } 232 | if (auth.getModel().getPlayerWithUUID(uuid)!=null){ 233 | return addUserResultMessage(writer,line,false,"UUID重复"); 234 | } 235 | 236 | String password = Utils.newPassword(); 237 | PlayerModel model = new PlayerModel(name,password,uuid); 238 | boolean success = auth.getModel().insertPlayer(model); 239 | if (success){ 240 | return addUserResultMessage(writer,line, true,password); 241 | }else { 242 | return addUserResultMessage(writer,line, false,"插入到数据库失败"); 243 | } 244 | } 245 | 246 | private String addUserResultMessage(BufferedWriter writer,String line,boolean success,String extra){ 247 | String message = "数据:"; 248 | message += line; 249 | message += " ,状态:"; 250 | if (success){ 251 | message += "成功,密码:"; 252 | message += extra; 253 | }else { 254 | message += "失败,原因:"; 255 | message += extra; 256 | } 257 | try { 258 | writer.write(message + "\n"); 259 | } catch (IOException ignored) { 260 | 261 | } 262 | return message; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/Config.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth; 2 | 3 | import lombok.Getter; 4 | import org.lintx.plugin.webauth.config.DatabaseConfig; 5 | import org.lintx.plugins.modules.configure.Configure; 6 | import org.lintx.plugins.modules.configure.YamlConfig; 7 | 8 | import java.io.File; 9 | 10 | @YamlConfig 11 | public class Config { 12 | private static Config instance = new Config(); 13 | 14 | public static Config getInstance(){ 15 | if (instance==null) instance = new Config(); 16 | return instance; 17 | } 18 | 19 | public void load(WebAuth plugin){ 20 | Configure.bungeeLoad(plugin, this); 21 | File file = new File(plugin.getDataFolder(),"config.yml"); 22 | if (!file.exists()){ 23 | Configure.bungeeSave(plugin,this); 24 | } 25 | } 26 | 27 | @YamlConfig 28 | @Getter 29 | private String notRegister = "你还没有注册,请打开网页\n{url}\n注册后登录游戏"; 30 | 31 | @YamlConfig 32 | @Getter 33 | private String tokenIsValid = "登录凭证无效,请检查您的凭证\n如忘记凭证请打开网页\n{url}\n重置凭证"; 34 | 35 | @YamlConfig 36 | @Getter 37 | private String tokenIsExpired = "登录凭证已过期,请打开网页\n{url}\n重置凭证"; 38 | 39 | @YamlConfig 40 | @Getter 41 | private String nameIsValid = "玩家名中含有不允许的字符,请打开网页\n{url}\n修改玩家名或联系管理员"; 42 | 43 | @YamlConfig 44 | private String tokenManageUrl = ""; 45 | 46 | @YamlConfig 47 | @Getter 48 | private DatabaseConfig databaseConfig = new DatabaseConfig(); 49 | 50 | @YamlConfig 51 | @Getter 52 | private int webPort = 0; 53 | 54 | @YamlConfig 55 | @Getter 56 | private boolean openRegister = true; 57 | 58 | @YamlConfig 59 | @Getter 60 | private boolean openChangePlayername = false; 61 | 62 | @YamlConfig 63 | @Getter 64 | private String playerNameRegexp = "^[a-zA-Z0-9\\u4e00-\\u9fa5]+$"; 65 | 66 | @YamlConfig 67 | @Getter 68 | private boolean checkPlayerNameFromMojang = false; 69 | 70 | @YamlConfig 71 | @Getter 72 | private String httpProxyType = "http/socks"; 73 | 74 | @YamlConfig 75 | @Getter 76 | private String httpProxyAddress = ""; 77 | 78 | @YamlConfig 79 | @Getter 80 | private int httpProxyPort = 0; 81 | 82 | public String formatMessage(String message){ 83 | return message.replaceAll("\\{url\\}",tokenManageUrl); 84 | } 85 | 86 | public static void setInstance(Config instance) { 87 | Config.instance = instance; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/Listeners.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth; 2 | 3 | import net.md_5.bungee.api.chat.TextComponent; 4 | import net.md_5.bungee.api.connection.PendingConnection; 5 | import net.md_5.bungee.api.event.PreLoginEvent; 6 | import net.md_5.bungee.api.plugin.Listener; 7 | import net.md_5.bungee.connection.InitialHandler; 8 | import net.md_5.bungee.event.EventHandler; 9 | import org.lintx.plugin.webauth.models.PlayerModel; 10 | 11 | import java.util.regex.Pattern; 12 | 13 | public class Listeners implements Listener { 14 | private WebAuth auth; 15 | 16 | Listeners(WebAuth auth){ 17 | this.auth = auth; 18 | } 19 | 20 | @EventHandler(priority = 127) 21 | public void onLogin(PreLoginEvent event){ 22 | if (event.isCancelled()){ 23 | return; 24 | } 25 | PendingConnection connection = event.getConnection(); 26 | String name = connection.getName(); 27 | boolean isToken = name.length() == 16 && name.startsWith("=") && name.endsWith("="); 28 | if (!connection.isOnlineMode() || isToken){ 29 | if (event.getConnection() instanceof InitialHandler){ 30 | try { 31 | Config config = Config.getInstance(); 32 | PlayerModel model = auth.getModel().getPlayerWithToken(name); 33 | if (model==null){ 34 | if (isToken){ 35 | connection.disconnect(new TextComponent(config.formatMessage(config.getTokenIsValid()))); 36 | }else { 37 | connection.disconnect(new TextComponent(config.formatMessage(config.getNotRegister()))); 38 | } 39 | }else if (!model.tokenIsEffective()){ 40 | connection.disconnect(new TextComponent(config.formatMessage(config.getTokenIsExpired()))); 41 | }else if (!Pattern.matches(config.getPlayerNameRegexp(),model.getName())) { 42 | connection.disconnect(new TextComponent(config.formatMessage(config.getNameIsValid()))); 43 | }else { 44 | try { 45 | InitialHandler handler = (InitialHandler)event.getConnection(); 46 | handler.setOnlineMode(false); 47 | handler.getLoginRequest().setData(model.getName()); 48 | handler.setUniqueId(model.getUuid()); 49 | }catch (Error | Exception e){ 50 | e.printStackTrace(); 51 | connection.disconnect(new TextComponent(Message.notSupport)); 52 | } 53 | } 54 | }catch (Error | Exception e){ 55 | e.printStackTrace(); 56 | connection.disconnect(new TextComponent(Message.loginError)); 57 | } 58 | }else { 59 | connection.disconnect(new TextComponent(Message.notSupport)); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/Message.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth; 2 | 3 | import org.lintx.plugin.webauth.models.PlayerModel; 4 | 5 | public class Message { 6 | static final String commandHelp = "§b/webauth player §a查看玩家资料\n" + 7 | "§b/webauth player uuid §a 设置玩家UUID,\n" + 8 | " 设置UUID一般为迁移玩家数据用(比如给玩家设置其他登录插件登录后产生的离线id的UUID或正版玩家注册帐号后给玩家设置正版ID)\n" + 9 | " §c修改玩家UUID后,玩家资料会丢失,相当于一个新号(除非新UUID原来登录过游戏),请谨慎操作。\n" + 10 | "§b/webauth player password §a 设置玩家密码,设置密码后玩家登录凭据将无效。\n" + 11 | "§b/webauth player playername §a 修改玩家名。\n" + 12 | "§b/webauth player token refresh [day]§a 刷新玩家登录凭据,day为凭据有效期(天数),不填写day时有效期为7天。\n" + 13 | "§b/webauth player add §a 新增玩家,主要用于关闭注册时手动注册玩家\n" + 14 | "§b/webauth reload§a 重新加载配置。\n" + 15 | "§b/webauth import §a 从文本文件中导入玩家数据,文件格式必须为UTF-8,\n" + 16 | " 每行格式可以是§b玩家名 UUID§a或§b玩家名§a\n" + 17 | " 不指定UUID时,将按照离线用户的规则生成玩家UUID\n" + 18 | " 密码为随机密码,将同时显示在消息窗口和插件目录下的对应日志文件中,\n" + 19 | " 导入完毕后需要将密码分发给对应的老玩家,§c并提醒玩家尽快修改密码。"; 20 | static final String notSupport = "登录方式不受服务器核心版本不支持,请联系服主"; 21 | static final String loginError = "登录时发生了错误,无法登录"; 22 | static final String noPermission = "§c权限不足"; 23 | static final String playerNotFind = "§c没有找到对应的玩家"; 24 | static final String playerInfo = "§aid:§6{id}§a,用户名:§6{username}§a,当前玩家名:§6{name}§a,当前UUID:§6{uuid}"; 25 | static final String uuidIsValid = "§c无效的UUID格式"; 26 | static final String uuidIsRepeat = "§cUUID重复!"; 27 | static final String uuidUpdateSuccess = "§aUUID更新成功"; 28 | static final String playerNameUpdateSuccess = "§a玩家名更新成功"; 29 | static final String tokenUpdateSuccess = "§a玩家登录凭据更新成功,新的凭据为:§b{token}§a(点击以复制),有效期:§b{expire_time}"; 30 | static final String passwordUpdateSuccess = "§a密码更新成功"; 31 | static final String addPlayerFail = "§c新增玩家失败"; 32 | static final String addPlayerSuccess = "§a新增玩家成功"; 33 | static final String addMojangPlayer = "§a新增玩家{name}经查询是正版玩家,UUID为{uuid}。\n" + 34 | "§a新增并设置UUID为正版UUID请输入命令§b/webauth player add {name} 密码 confirm\n" + 35 | "§a新增并不设置UUID为正版UUID请输入命令§b/webauth player add {name} 密码 ignore"; 36 | static final String playerNameRepeat = "§c玩家用户名或玩家名重复"; 37 | static final String reloadConfig = "§a重新加载配置文件"; 38 | static final String playerNameShort = "§c玩家名太短(不能小于4字符)"; 39 | static final String playerNameLong = "§c玩家名太长(不能大于16字符)"; 40 | static final String fileNotExist = "§c文件不存在"; 41 | static final String fileNotFile = "§c目标不是一个文件"; 42 | 43 | static String playerInfo(PlayerModel model){ 44 | String info = playerInfo; 45 | info = info.replaceAll("\\{id\\}",""+model.getId()); 46 | info = info.replaceAll("\\{username\\}",model.getUsername()); 47 | info = info.replaceAll("\\{name\\}",model.getName()); 48 | info = info.replaceAll("\\{uuid\\}",model.getUuid().toString()); 49 | return info; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/WebAuth.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth; 2 | 3 | import net.md_5.bungee.api.plugin.Plugin; 4 | import net.md_5.bungee.api.scheduler.ScheduledTask; 5 | import org.bstats.bungeecord.Metrics; 6 | import org.lintx.plugin.webauth.config.DatabaseConfig; 7 | import org.lintx.plugin.webauth.httpserver.Caches; 8 | import org.lintx.plugin.webauth.httpserver.NettyHttpServer; 9 | import org.lintx.plugin.webauth.sql.Model; 10 | import org.lintx.plugin.webauth.sql.MySql; 11 | import org.lintx.plugin.webauth.sql.SQLite; 12 | 13 | import java.io.*; 14 | import java.util.Enumeration; 15 | import java.util.concurrent.TimeUnit; 16 | import java.util.zip.ZipEntry; 17 | import java.util.zip.ZipFile; 18 | 19 | public class WebAuth extends Plugin { 20 | public static WebAuth plugin; 21 | private SQLite sqLite; 22 | private MySql mySql; 23 | private Model model; 24 | private Config config; 25 | private NettyHttpServer httpServer; 26 | private ScheduledTask task; 27 | @Override 28 | public void onEnable() { 29 | autoFreedWeb(); 30 | plugin = this; 31 | config = Config.getInstance(); 32 | 33 | reload(); 34 | 35 | getProxy().getPluginManager().registerListener(this,new Listeners(this)); 36 | getProxy().getPluginManager().registerCommand(this,new Commands(this,"webauth",null,"auth","wa")); 37 | 38 | Metrics metrics = new Metrics(this); 39 | } 40 | 41 | @Override 42 | public void onDisable() { 43 | if (mySql!=null){ 44 | mySql.close(); 45 | } 46 | if (httpServer!=null){ 47 | httpServer.stop(); 48 | } 49 | } 50 | 51 | public Model getModel() { 52 | return model; 53 | } 54 | 55 | private void autoFreedWeb(){ 56 | String rootFolder = "web/"; 57 | File folder = this.getDataFolder(); 58 | folder.mkdir(); 59 | new File(folder,rootFolder).mkdir(); 60 | 61 | try (ZipFile zipFile = new ZipFile(getFile())) { 62 | Enumeration extends ZipEntry> entries = zipFile.entries(); 63 | while (entries.hasMoreElements()) { 64 | ZipEntry entry = entries.nextElement(); 65 | copyFile(zipFile, entry, rootFolder); 66 | } 67 | } catch (IOException ignored) { 68 | 69 | } 70 | } 71 | 72 | private void copyFile(ZipFile zipFile,ZipEntry entry,String rootFolder){ 73 | String name = entry.getName(); 74 | if(!name.startsWith(rootFolder)){ 75 | return; 76 | } 77 | 78 | File file = new File(getDataFolder(), name); 79 | if(entry.isDirectory()) { 80 | file.mkdirs(); 81 | }else { 82 | if (file.exists()){ 83 | return; 84 | } 85 | if (file.getParentFile().isDirectory()){ 86 | file.getParentFile().mkdir(); 87 | } 88 | FileOutputStream outputStream = null; 89 | InputStream inputStream = null; 90 | try { 91 | byte[] buffer = new byte[1024]; 92 | outputStream = new FileOutputStream(file); 93 | inputStream = zipFile.getInputStream(entry); 94 | int len; 95 | while ((len = inputStream.read(buffer)) >= 0) { 96 | outputStream.write(buffer, 0, len); 97 | } 98 | } catch (IOException ignored) { 99 | 100 | } finally { 101 | try { 102 | outputStream.close(); 103 | } catch (IOException ignored) { 104 | 105 | } 106 | try { 107 | inputStream.close(); 108 | } catch (IOException ignored) { 109 | 110 | } 111 | } 112 | } 113 | } 114 | 115 | void reload(){ 116 | int oldPort = config.getWebPort(); 117 | config.load(this); 118 | if (oldPort!=config.getWebPort() && config.getWebPort()>1024){ 119 | if (httpServer!=null){ 120 | httpServer.stop(); 121 | } 122 | if (task!=null){ 123 | task.cancel(); 124 | } 125 | httpServer = new NettyHttpServer(config.getWebPort(),this,new File(getDataFolder(),"web")); 126 | getProxy().getScheduler().runAsync(this, () -> httpServer.start()); 127 | task = getProxy().getScheduler().schedule(this, Caches::checkCaches,10L,10L, TimeUnit.SECONDS); 128 | } 129 | 130 | if (config.getDatabaseConfig().getType()== DatabaseConfig.DatabaseType.MYSQL){ 131 | if (mySql!=null){ 132 | mySql.close(); 133 | } 134 | mySql = new MySql(config.getDatabaseConfig().getMysqlUri(),config.getDatabaseConfig().getMysqlUser(),config.getDatabaseConfig().getMysqlPassword(),config.getDatabaseConfig().getTimeout()); 135 | model = new Model(mySql); 136 | }else { 137 | sqLite = new SQLite(getDataFolder(),"database"); 138 | model = new Model(sqLite); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/config/DatabaseConfig.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.config; 2 | 3 | import org.lintx.plugins.modules.configure.YamlConfig; 4 | 5 | public class DatabaseConfig { 6 | public enum DatabaseType{ 7 | MYSQL, 8 | SQLITE 9 | } 10 | 11 | @YamlConfig 12 | private DatabaseType type = DatabaseType.MYSQL; 13 | @YamlConfig 14 | private String mysqlUri = "jdbc:mysql://localhost:3306/database?autoReconnect=true&useSSL=false&characterEncoding=utf-8&useUnicode=true"; 15 | @YamlConfig 16 | private String mysqlUser = "root"; 17 | @YamlConfig 18 | private String mysqlPassword = "root"; 19 | @YamlConfig 20 | private boolean printError = false; 21 | @YamlConfig 22 | private long timeout = 60 * 60 * 5; 23 | 24 | public long getTimeout() { 25 | return timeout; 26 | } 27 | 28 | public void setTimeout(long timeout) { 29 | this.timeout = timeout; 30 | } 31 | 32 | public boolean isPrintError() { 33 | return printError; 34 | } 35 | 36 | public void setPrintError(boolean printError) { 37 | this.printError = printError; 38 | } 39 | 40 | public DatabaseType getType() { 41 | return type; 42 | } 43 | 44 | public void setType(DatabaseType type) { 45 | this.type = type; 46 | } 47 | 48 | public String getMysqlUri() { 49 | return mysqlUri; 50 | } 51 | 52 | public void setMysqlUri(String mysqlUri) { 53 | this.mysqlUri = mysqlUri; 54 | } 55 | 56 | public String getMysqlUser() { 57 | return mysqlUser; 58 | } 59 | 60 | public void setMysqlUser(String mysqlUser) { 61 | this.mysqlUser = mysqlUser; 62 | } 63 | 64 | public String getMysqlPassword() { 65 | return mysqlPassword; 66 | } 67 | 68 | public void setMysqlPassword(String mysqlPassword) { 69 | this.mysqlPassword = mysqlPassword; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/httpserver/Caches.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.httpserver; 2 | 3 | import org.lintx.plugin.webauth.models.PlayerModel; 4 | import org.lintx.plugin.webauth.utils.Utils; 5 | 6 | import java.time.Duration; 7 | import java.time.LocalDateTime; 8 | import java.util.HashMap; 9 | import java.util.Iterator; 10 | import java.util.Map; 11 | 12 | public class Caches { 13 | private static class LoginLog{ 14 | LocalDateTime lastTime; 15 | int count; 16 | } 17 | 18 | private static class LoginCache{ 19 | LocalDateTime time; 20 | PlayerModel model; 21 | } 22 | //本类功能:1.实现登录token的缓存,2.实现登录失败的缓存 23 | private static Map tokens = new HashMap<>(); 24 | private static Map loginLogMap = new HashMap<>(); 25 | private static Map registerLogMap = new HashMap<>(); 26 | 27 | static String login(PlayerModel model){ 28 | String token = Utils.sha1(Utils.newToken()); 29 | if (tokens.containsKey(token)){ 30 | return login(model); 31 | } 32 | LoginCache cache = new LoginCache(); 33 | cache.time = LocalDateTime.now(); 34 | cache.model = model; 35 | tokens.put(token, cache); 36 | return token; 37 | } 38 | 39 | static PlayerModel getLogin(String token){ 40 | if (!tokens.containsKey(token)){ 41 | return null; 42 | } 43 | return tokens.get(token).model; 44 | } 45 | 46 | static void refreshLogin(String token,PlayerModel model){ 47 | if (tokens.containsKey(token)){ 48 | LoginCache cache = tokens.get(token); 49 | cache.time = LocalDateTime.now(); 50 | cache.model = model; 51 | tokens.put(token,cache); 52 | } 53 | } 54 | 55 | static void loginOut(String token){ 56 | tokens.remove(token); 57 | } 58 | 59 | static void loginFail(String ip){ 60 | LoginLog log; 61 | if (!loginLogMap.containsKey(ip)){ 62 | log = new LoginLog(); 63 | loginLogMap.put(ip,log); 64 | }else { 65 | log = loginLogMap.get(ip); 66 | } 67 | log.count += 1; 68 | log.lastTime = LocalDateTime.now(); 69 | loginLogMap.put(ip,log); 70 | } 71 | 72 | static boolean canLogin(String ip){ 73 | if (!loginLogMap.containsKey(ip)){ 74 | return true; 75 | } 76 | LoginLog log = loginLogMap.get(ip); 77 | return log.count <= 5; 78 | } 79 | 80 | static void registerLog(String ip){ 81 | LoginLog log; 82 | if (!registerLogMap.containsKey(ip)){ 83 | log = new LoginLog(); 84 | registerLogMap.put(ip,log); 85 | }else { 86 | log = registerLogMap.get(ip); 87 | } 88 | log.count += 1; 89 | log.lastTime = LocalDateTime.now(); 90 | registerLogMap.put(ip,log); 91 | } 92 | 93 | static boolean canRegister(String ip){ 94 | if (!registerLogMap.containsKey(ip)){ 95 | return true; 96 | } 97 | LoginLog log = registerLogMap.get(ip); 98 | return log.count <= 0; 99 | } 100 | 101 | public static void checkCaches(){ 102 | LocalDateTime now = LocalDateTime.now(); 103 | Iterator> it = loginLogMap.entrySet().iterator(); 104 | while(it.hasNext()) { 105 | Map.Entry entry = it.next(); 106 | Duration duration = Duration.between(entry.getValue().lastTime,now); 107 | if (duration.getSeconds()>600){ 108 | it.remove(); 109 | } 110 | } 111 | 112 | Iterator> it1 = tokens.entrySet().iterator(); 113 | while(it1.hasNext()) { 114 | Map.Entry entry = it1.next(); 115 | Duration duration = Duration.between(entry.getValue().time,now); 116 | if (duration.getSeconds()>600){ 117 | it1.remove(); 118 | } 119 | } 120 | 121 | Iterator> it2 = registerLogMap.entrySet().iterator(); 122 | while(it2.hasNext()) { 123 | Map.Entry entry = it2.next(); 124 | Duration duration = Duration.between(entry.getValue().lastTime,now); 125 | if (duration.getSeconds()>600){ 126 | it2.remove(); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/httpserver/InputModel.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.httpserver; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class InputModel { 6 | @SerializedName("token") 7 | private String token="";//登录凭据 8 | @SerializedName("username") 9 | private String username=""; 10 | @SerializedName("password") 11 | private String password=""; 12 | @SerializedName("playerName") 13 | private String playerName=""; 14 | 15 | public String getToken() { 16 | return token; 17 | } 18 | 19 | public void setToken(String token) { 20 | this.token = token; 21 | } 22 | 23 | public String getUsername() { 24 | return username; 25 | } 26 | 27 | public void setUsername(String username) { 28 | this.username = username; 29 | } 30 | 31 | public String getPassword() { 32 | return password; 33 | } 34 | 35 | public void setPassword(String password) { 36 | this.password = password; 37 | } 38 | 39 | public String getPlayerName() { 40 | return playerName; 41 | } 42 | 43 | public void setPlayerName(String playerName) { 44 | this.playerName = playerName; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/httpserver/Messages.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.httpserver; 2 | 3 | public class Messages { 4 | static final String validRequest = "无效请求"; 5 | static final String usernameShort = "用户名太短(不能小于4字符)"; 6 | static final String usernameLong = "用户名太长(不能大于16字符)"; 7 | static final String usernameRepeat = "用户名重复"; 8 | static final String usernameInMojang = "存在同名正版帐号"; 9 | static final String playerNameRepeat = "玩家名重复"; 10 | static final String playerNameShort = "玩家名太短(不能小于4字符)"; 11 | static final String playerNameLong = "玩家名太长(不能大于16字符)"; 12 | static final String repeatRegister = "您已经注册过了,请勿重复注册"; 13 | static final String registerSuccess = "注册成功"; 14 | static final String registerFail = "注册失败,请重试"; 15 | static final String loginFail = "登录失败,用户名或密码错误"; 16 | static final String ipLoginFail = "失败次数过多,请等一会再试"; 17 | static final String loginSuccess = "登录成功"; 18 | static final String registerIsClose = "服务器已经关闭了注册功能"; 19 | static final String notLogin = "登录信息已失效,请重新登录"; 20 | static final String loginOut = "已经退出登录"; 21 | static final String changePlayerNameIsClose = "服务器已经关闭了修改玩家名功能"; 22 | static final String changePlayerNameSuccess = "修改玩家名成功"; 23 | static final String changePlayerNameFail = "修改玩家名失败,请重试"; 24 | static final String changePasswordSuccess = "修改密码成功"; 25 | static final String changePasswordFail = "修改密码失败,请重试"; 26 | static final String refreshTokenSuccess = "刷新登录凭据成功"; 27 | static final String refreshTokenFail = "刷新登录凭据失败,请重试"; 28 | static final String notMatchPlayerNameRegexp = "玩家名中含有不被允许的字符"; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/httpserver/NettyHttpHandler.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.httpserver; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.reflect.TypeToken; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.buffer.Unpooled; 7 | import io.netty.channel.*; 8 | import io.netty.handler.codec.http.*; 9 | import io.netty.util.AsciiString; 10 | import org.lintx.plugin.webauth.Config; 11 | import org.lintx.plugin.webauth.WebAuth; 12 | import org.lintx.plugin.webauth.models.PlayerModel; 13 | import org.lintx.plugin.webauth.utils.MojangApi; 14 | import org.lintx.plugin.webauth.utils.Utils; 15 | 16 | import java.io.*; 17 | import java.net.*; 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.Locale; 20 | import java.util.regex.Pattern; 21 | 22 | public class NettyHttpHandler extends SimpleChannelInboundHandler { 23 | private AsciiString htmlType = AsciiString.cached("text/html"); 24 | private AsciiString jsonType = HttpHeaderValues.APPLICATION_JSON; 25 | private AsciiString jsType = AsciiString.cached("text/javascript"); 26 | private AsciiString cssType = AsciiString.cached("text/css"); 27 | private AsciiString jpegType = AsciiString.cached("image/jpeg"); 28 | private final WebAuth plugin; 29 | private final File rootFolder; 30 | private final String rootPath = "/bungeewebauth"; 31 | private final String apiPath = "/api"; 32 | private final String resourcesPath = "/static"; 33 | private final Gson gson = new Gson(); 34 | 35 | NettyHttpHandler(WebAuth plugin,File rootFolder){ 36 | this.plugin = plugin; 37 | this.rootFolder = rootFolder; 38 | } 39 | 40 | @Override 41 | protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { 42 | try { 43 | URI uri = new URI(request.uri()); 44 | String path = uri.getPath(); 45 | if (path.equals("/")){ 46 | writeIndex(ctx); 47 | return; 48 | } 49 | if (path.startsWith(rootPath)){ 50 | path = path.substring(rootPath.length()); 51 | if (path.equals("/")){ 52 | writeIndex(ctx); 53 | return; 54 | } 55 | if (path.startsWith(apiPath)){ 56 | if (request.method()!=HttpMethod.POST){ 57 | write404(ctx); 58 | return; 59 | } 60 | path = path.substring(apiPath.length()); 61 | InputModel inputModel = null; 62 | try { 63 | ByteBuf buf = request.content(); 64 | String body = buf.toString(StandardCharsets.UTF_8); 65 | inputModel = gson.fromJson(body,new TypeToken(){}.getType()); 66 | }catch (Exception ignored){ 67 | } 68 | 69 | String ip = getIP(ctx,request); 70 | 71 | if (!Caches.canLogin(ip)){ 72 | writeError(ctx,Messages.ipLoginFail); 73 | return; 74 | } 75 | PlayerModel model; 76 | if (path.equals("/index")){ 77 | //返回用户id、token过期时间、玩家名 78 | OutputModel outputModel = new OutputModel(); 79 | outputModel.setCode(1); 80 | outputModel.setOpenRegister(Config.getInstance().isOpenRegister()); 81 | outputModel.setOpenChangePlayerName(Config.getInstance().isOpenChangePlayername()); 82 | if (inputModel!=null){ 83 | model = Caches.getLogin(inputModel.getToken()); 84 | if (model!=null){ 85 | outputModel.setUserId(model.getId()); 86 | outputModel.setPlayerName(model.getName()); 87 | outputModel.setUserTokenTime(model.getToken_timeString()); 88 | 89 | if (!model.tokenIsEffective()){ 90 | refreshPlayerToken(ctx,model,outputModel); 91 | model = WebAuth.plugin.getModel().getPlayerWithId(model.getId()); 92 | Caches.refreshLogin(inputModel.getToken(),model); 93 | }else { 94 | writeModel(ctx,outputModel); 95 | } 96 | return; 97 | } 98 | } 99 | writeModel(ctx,outputModel); 100 | return; 101 | } 102 | 103 | if (inputModel==null){ 104 | writeError(ctx,Messages.validRequest); 105 | return; 106 | } 107 | if (path.equals("/register")){ 108 | //注册,需要判断是否开启注册 109 | if (!Config.getInstance().isOpenRegister()){ 110 | writeError(ctx,Messages.registerIsClose); 111 | return; 112 | } 113 | if (!Caches.canRegister(ip)){ 114 | writeError(ctx,Messages.repeatRegister); 115 | return; 116 | } 117 | if (inputModel.getUsername().length()<4){ 118 | writeError(ctx,Messages.usernameShort); 119 | return; 120 | } 121 | if (inputModel.getUsername().getBytes().length>16){ 122 | writeError(ctx,Messages.usernameLong); 123 | return; 124 | } 125 | if (!WebAuth.plugin.getModel().checkPlayerUsername(inputModel.getUsername())){ 126 | writeError(ctx,Messages.usernameRepeat); 127 | Caches.loginFail(ip); 128 | return; 129 | } 130 | if (Config.getInstance().isCheckPlayerNameFromMojang()){ 131 | MojangApi.MojangAccount account = MojangApi.getMojangAccount(inputModel.getUsername()); 132 | if (!account.checkName(inputModel.getUsername())){ 133 | writeError(ctx,Messages.usernameInMojang); 134 | Caches.loginFail(ip); 135 | return; 136 | } 137 | } 138 | model = new PlayerModel(inputModel.getUsername(),inputModel.getPassword()); 139 | if (WebAuth.plugin.getModel().insertPlayer(model)){ 140 | writeSuccess(ctx,Messages.registerSuccess); 141 | Caches.registerLog(ip); 142 | return; 143 | }else { 144 | writeError(ctx,Messages.registerFail); 145 | return; 146 | } 147 | }else if (path.equals("/login")){ 148 | //登录,取得用户名密码后登录,返回登录token 149 | model = WebAuth.plugin.getModel().getPlayerWithUsername(inputModel.getUsername()); 150 | if (model==null || !model.checkPassword(inputModel.getPassword())){ 151 | Caches.loginFail(ip); 152 | writeError(ctx,Messages.loginFail); 153 | }else { 154 | String token = Caches.login(model); 155 | OutputModel outputModel = new OutputModel(); 156 | outputModel.setToken(token); 157 | outputModel.setCode(1); 158 | outputModel.setMessage(Messages.loginSuccess); 159 | outputModel.setPlayerName(model.getName()); 160 | outputModel.setUserId(model.getId()); 161 | if (!model.tokenIsEffective()){ 162 | refreshPlayerToken(ctx,model,outputModel); 163 | model = WebAuth.plugin.getModel().getPlayerWithId(model.getId()); 164 | Caches.refreshLogin(token,model); 165 | }else { 166 | outputModel.setUserTokenTime(model.getToken_timeString()); 167 | writeModel(ctx,outputModel); 168 | } 169 | return; 170 | } 171 | }else{ 172 | //这里都是需要先登录的,所以需要先检验登录状态 173 | model = Caches.getLogin(inputModel.getToken()); 174 | if (model==null || model.getId()==0){ 175 | writeError(ctx,Messages.notLogin); 176 | Caches.loginFail(ip); 177 | return; 178 | } 179 | Caches.refreshLogin(inputModel.getToken(),model); 180 | if (path.equals("/changeplayername")){ 181 | //接收新用户名,并修改 182 | if (!Config.getInstance().isOpenChangePlayername()){ 183 | writeError(ctx,Messages.changePlayerNameIsClose); 184 | return; 185 | } 186 | if (inputModel.getPlayerName().getBytes().length<4){ 187 | writeError(ctx,Messages.playerNameShort); 188 | return; 189 | } 190 | if (inputModel.getPlayerName().getBytes().length>16){ 191 | writeError(ctx,Messages.playerNameLong); 192 | return; 193 | } 194 | if (!Pattern.matches(Config.getInstance().getPlayerNameRegexp(),inputModel.getPlayerName())){ 195 | writeError(ctx,Messages.notMatchPlayerNameRegexp); 196 | return; 197 | } 198 | if (!WebAuth.plugin.getModel().checkPlayerName(inputModel.getPlayerName())){ 199 | writeError(ctx,Messages.playerNameRepeat); 200 | return; 201 | } 202 | model.setName(inputModel.getPlayerName()); 203 | if (WebAuth.plugin.getModel().updatePlayer(model)){ 204 | writeSuccess(ctx,Messages.changePlayerNameSuccess); 205 | model = WebAuth.plugin.getModel().getPlayerWithId(model.getId()); 206 | Caches.refreshLogin(inputModel.getToken(),model); 207 | return; 208 | }else { 209 | writeError(ctx,Messages.changePlayerNameFail); 210 | return; 211 | } 212 | }else if (path.equals("/changepassword")){ 213 | //接收新密码,并修改 214 | model.updatePassword(inputModel.getPassword()); 215 | if (WebAuth.plugin.getModel().updatePlayer(model)){ 216 | writeSuccess(ctx,Messages.changePasswordSuccess); 217 | model = WebAuth.plugin.getModel().getPlayerWithId(model.getId()); 218 | Caches.refreshLogin(inputModel.getToken(),model); 219 | return; 220 | }else { 221 | writeError(ctx,Messages.changePasswordFail); 222 | return; 223 | } 224 | }else if (path.equals("/refresh")){ 225 | //刷新token,返回新的token和过期时间 226 | OutputModel outputModel = new OutputModel(); 227 | String playerToken = Utils.newToken(); 228 | model.updateToken(playerToken); 229 | if (WebAuth.plugin.getModel().updatePlayer(model)){ 230 | outputModel.setUserToken(playerToken); 231 | outputModel.setUserTokenTime(model.getToken_timeString()); 232 | outputModel.setCode(1); 233 | outputModel.setMessage(Messages.refreshTokenSuccess); 234 | model = WebAuth.plugin.getModel().getPlayerWithId(model.getId()); 235 | Caches.refreshLogin(inputModel.getToken(),model); 236 | }else { 237 | outputModel.setCode(0); 238 | outputModel.setMessage(Messages.refreshTokenFail); 239 | } 240 | writeModel(ctx,outputModel); 241 | }else if (path.equals("/loginout")){ 242 | Caches.loginOut(inputModel.getToken()); 243 | writeSuccess(ctx,Messages.loginOut); 244 | return; 245 | } 246 | } 247 | }else if (path.startsWith(resourcesPath)){ 248 | if (request.method()!=HttpMethod.GET){ 249 | write404(ctx); 250 | return; 251 | } 252 | writeFile(ctx,path); 253 | return; 254 | } 255 | } 256 | write404(ctx); 257 | } 258 | catch (Exception e) { 259 | e.printStackTrace(); 260 | write404(ctx); 261 | } 262 | } 263 | 264 | @Override 265 | public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 266 | super.channelReadComplete(ctx); 267 | ctx.flush(); 268 | } 269 | 270 | @Override 271 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 272 | if (null != cause) cause.printStackTrace(); 273 | if (null != ctx) ctx.close(); 274 | } 275 | 276 | private void writeIndex(ChannelHandlerContext ctx){ 277 | writeFile(ctx,"/index.html"); 278 | } 279 | 280 | private void writeFile(ChannelHandlerContext ctx,String path){ 281 | try { 282 | File file = new File(rootFolder,path); 283 | String canonical = file.getCanonicalPath(); 284 | if (canonical.startsWith(rootFolder.getCanonicalPath()) && file.exists() && file.isFile()){ 285 | AsciiString mime; 286 | String cs = canonical.toLowerCase(Locale.ROOT); 287 | if (cs.endsWith(".jpg") || cs.endsWith(".jpeg")){ 288 | mime = jpegType; 289 | }else if (cs.endsWith(".html")){ 290 | mime = htmlType; 291 | }else if (cs.endsWith(".js")){ 292 | mime = jsType; 293 | }else if (cs.endsWith(".css")){ 294 | mime = cssType; 295 | }else { 296 | write404(ctx); 297 | return; 298 | } 299 | 300 | final RandomAccessFile raf = new RandomAccessFile(file,"r"); 301 | long fileLength = raf.length(); 302 | HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK); 303 | HttpHeaders heads = response.headers(); 304 | heads.add(HttpHeaderNames.CONTENT_TYPE, mime + "; charset=UTF-8"); 305 | heads.add(HttpHeaderNames.CONTENT_LENGTH, fileLength); 306 | heads.add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 307 | ctx.write(response); 308 | 309 | ChannelFuture sendFileFuture = ctx.write(new DefaultFileRegion(raf.getChannel(),0,fileLength),ctx.newProgressivePromise()); 310 | 311 | ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); 312 | sendFileFuture.addListener(new ChannelProgressiveFutureListener() { 313 | @Override 314 | public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long l, long l1) throws Exception { 315 | 316 | } 317 | 318 | @Override 319 | public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) throws Exception { 320 | raf.close(); 321 | } 322 | }); 323 | }else { 324 | write404(ctx); 325 | } 326 | } catch (IOException ignored) { 327 | write404(ctx); 328 | } 329 | } 330 | 331 | private void refreshPlayerToken(ChannelHandlerContext ctx,PlayerModel model, OutputModel outputModel){ 332 | String playerToken = Utils.newToken(); 333 | model.updateToken(playerToken); 334 | if (plugin.getModel().updatePlayer(model)){ 335 | outputModel.setUserToken(playerToken); 336 | outputModel.setUserTokenTime(model.getToken_timeString()); 337 | } 338 | writeModel(ctx,outputModel); 339 | } 340 | 341 | private void write404(ChannelHandlerContext ctx){ 342 | DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK); 343 | String string = "File Not Found"; 344 | response.content().writeBytes(Unpooled.wrappedBuffer(string.getBytes())); 345 | write(ctx,response,htmlType); 346 | } 347 | 348 | private void writeModel(ChannelHandlerContext ctx,OutputModel model){ 349 | DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK); 350 | String string = gson.toJson(model); 351 | response.content().writeBytes(Unpooled.wrappedBuffer(string.getBytes(StandardCharsets.UTF_8))); 352 | write(ctx,response,jsonType); 353 | } 354 | 355 | private void writeError(ChannelHandlerContext ctx,String message){ 356 | OutputModel model = new OutputModel(); 357 | model.setMessage(message); 358 | writeModel(ctx,model); 359 | } 360 | 361 | private void writeSuccess(ChannelHandlerContext ctx,String message){ 362 | OutputModel model = new OutputModel(); 363 | model.setCode(1); 364 | model.setMessage(message); 365 | writeModel(ctx,model); 366 | } 367 | 368 | private void write(ChannelHandlerContext ctx,DefaultFullHttpResponse response,AsciiString type){ 369 | HttpHeaders heads = response.headers(); 370 | heads.add(HttpHeaderNames.CONTENT_TYPE, type + "; charset=UTF-8"); 371 | heads.add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); 372 | heads.add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 373 | ctx.write(response); 374 | } 375 | 376 | private String getIP(ChannelHandlerContext ctx, FullHttpRequest request){ 377 | String ip = request.headers().get("X-Forwarded-For"); 378 | if (ip==null){ 379 | InetAddress address = ((InetSocketAddress)ctx.channel().remoteAddress()).getAddress(); 380 | ip = address.getHostAddress(); 381 | }else { 382 | if (ip.contains(",")){ 383 | ip = ip.substring(0,ip.indexOf(",")); 384 | } 385 | } 386 | return ip; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/httpserver/NettyHttpServer.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.httpserver; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.channel.ChannelInitializer; 5 | import io.netty.channel.ChannelOption; 6 | import io.netty.channel.nio.NioEventLoopGroup; 7 | import io.netty.channel.socket.SocketChannel; 8 | import io.netty.channel.socket.nio.NioServerSocketChannel; 9 | import io.netty.handler.codec.http.HttpObjectAggregator; 10 | import io.netty.handler.codec.http.HttpRequestDecoder; 11 | import io.netty.handler.codec.http.HttpResponseEncoder; 12 | import org.lintx.plugin.webauth.WebAuth; 13 | 14 | import java.io.File; 15 | 16 | public class NettyHttpServer { 17 | private final int port; 18 | private ServerBootstrap bootstrap; 19 | private NioEventLoopGroup group; 20 | private final WebAuth plugin; 21 | private final File rootFolder; 22 | 23 | public NettyHttpServer(int port,WebAuth plugin,File rootFolder) { 24 | this.port = port; 25 | this.plugin = plugin; 26 | this.rootFolder = rootFolder; 27 | } 28 | 29 | public void start(){ 30 | bootstrap = new ServerBootstrap(); 31 | this.group = new NioEventLoopGroup(); 32 | bootstrap.group(group) 33 | .channel(NioServerSocketChannel.class) 34 | .childHandler(new ChannelInitializer() { 35 | @Override 36 | protected void initChannel(SocketChannel channel) throws Exception { 37 | channel.pipeline() 38 | .addLast("decoder",new HttpRequestDecoder()) 39 | .addLast("encoder",new HttpResponseEncoder()) 40 | .addLast("aggregator",new HttpObjectAggregator(512*1024)) 41 | .addLast("handler",new NettyHttpHandler(plugin,rootFolder)); 42 | } 43 | }) 44 | .option(ChannelOption.SO_BACKLOG,128) 45 | .childOption(ChannelOption.SO_KEEPALIVE,Boolean.TRUE); 46 | try { 47 | bootstrap.bind(port).sync(); 48 | } catch (InterruptedException ignored) { 49 | 50 | } 51 | } 52 | 53 | public void stop(){ 54 | try { 55 | group.shutdownGracefully(); 56 | }catch (Exception | Error ignored){} 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/httpserver/OutputModel.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.httpserver; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class OutputModel { 6 | @SerializedName("code") 7 | private int code=0;//0失败,1成功 8 | @SerializedName("message") 9 | 10 | private String message="";//code=0时,代表失败原因,或者部分仅提示的返回消息的原因 11 | @SerializedName("token") 12 | private String token="";//登录成功后返回的token,需要带入到除了注册和登录之外的其他请求中 13 | @SerializedName("userToken") 14 | private String userToken="";//游戏登录token 15 | @SerializedName("userTokenTime") 16 | private String userTokenTime="";//游戏登录token到期时间 17 | @SerializedName("userId") 18 | private int userId=0; 19 | @SerializedName("playerName") 20 | private String playerName=""; 21 | @SerializedName("openRegister") 22 | private boolean openRegister = false; 23 | @SerializedName("openChangePlayerName") 24 | private boolean openChangePlayerName = false; 25 | 26 | public int getCode() { 27 | return code; 28 | } 29 | 30 | public void setCode(int code) { 31 | this.code = code; 32 | } 33 | 34 | public String getMessage() { 35 | return message; 36 | } 37 | 38 | public void setMessage(String message) { 39 | this.message = message; 40 | } 41 | 42 | public String getToken() { 43 | return token; 44 | } 45 | 46 | public void setToken(String token) { 47 | this.token = token; 48 | } 49 | 50 | public String getUserToken() { 51 | return userToken; 52 | } 53 | 54 | public void setUserToken(String userToken) { 55 | this.userToken = userToken; 56 | } 57 | 58 | public String getUserTokenTime() { 59 | return userTokenTime; 60 | } 61 | 62 | public void setUserTokenTime(String userTokenTime) { 63 | this.userTokenTime = userTokenTime; 64 | } 65 | 66 | public int getUserId() { 67 | return userId; 68 | } 69 | 70 | public void setUserId(int userId) { 71 | this.userId = userId; 72 | } 73 | 74 | public String getPlayerName() { 75 | return playerName; 76 | } 77 | 78 | public void setPlayerName(String playerName) { 79 | this.playerName = playerName; 80 | } 81 | 82 | public boolean isOpenRegister() { 83 | return openRegister; 84 | } 85 | 86 | public void setOpenRegister(boolean openRegister) { 87 | this.openRegister = openRegister; 88 | } 89 | 90 | public boolean isOpenChangePlayerName() { 91 | return openChangePlayerName; 92 | } 93 | 94 | public void setOpenChangePlayerName(boolean openChangePlayerName) { 95 | this.openChangePlayerName = openChangePlayerName; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/models/PlayerHistoryModel.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.models; 2 | 3 | public class PlayerHistoryModel { 4 | private int player_id; 5 | private String name; 6 | 7 | public int getPlayer_id() { 8 | return player_id; 9 | } 10 | 11 | public void setPlayer_id(int player_id) { 12 | this.player_id = player_id; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public void setName(String name) { 20 | this.name = name; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/models/PlayerModel.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.models; 2 | 3 | import org.lintx.plugin.webauth.utils.Utils; 4 | 5 | import java.time.Duration; 6 | import java.time.LocalDateTime; 7 | import java.util.UUID; 8 | 9 | public class PlayerModel { 10 | private int id; 11 | private String username; 12 | private String password; 13 | private String slat; 14 | private String name; 15 | private UUID uuid; 16 | private String token; 17 | private LocalDateTime token_time; 18 | 19 | public PlayerModel(){ 20 | 21 | } 22 | 23 | public PlayerModel(String username,String password){ 24 | this(username,password,Utils.newUUID()); 25 | } 26 | 27 | public PlayerModel(String username,String password,UUID uuid){ 28 | setSlat(Utils.sha1(Utils.newToken())); 29 | setUsername(username); 30 | setName(username); 31 | updatePassword(password); 32 | setUuid(uuid); 33 | setToken(""); 34 | setToken_time(LocalDateTime.now()); 35 | } 36 | 37 | public boolean checkPassword(String password){ 38 | String string = password + getSlat(); 39 | String sign = Utils.sha1(string); 40 | return sign.equals(getPassword()); 41 | } 42 | 43 | public boolean tokenIsEffective(){ 44 | if (token.equals("")) return false; 45 | return token_time.isAfter(LocalDateTime.now()); 46 | } 47 | 48 | public int getId() { 49 | return id; 50 | } 51 | 52 | public void setId(int id) { 53 | this.id = id; 54 | } 55 | 56 | public String getUsername() { 57 | return username; 58 | } 59 | 60 | public void setUsername(String username) { 61 | this.username = username; 62 | } 63 | 64 | public String getPassword() { 65 | return password; 66 | } 67 | 68 | public void setPassword(String password) { 69 | this.password = password; 70 | } 71 | 72 | public void updatePassword(String password){ 73 | String string = password + getSlat(); 74 | String sign = Utils.sha1(string); 75 | setPassword(sign); 76 | setToken(""); 77 | } 78 | 79 | public String getSlat() { 80 | return slat; 81 | } 82 | 83 | public void setSlat(String slat) { 84 | this.slat = slat; 85 | } 86 | 87 | public String getName() { 88 | return name; 89 | } 90 | 91 | public void setName(String name) { 92 | this.name = name; 93 | } 94 | 95 | public UUID getUuid() { 96 | return uuid; 97 | } 98 | 99 | public void setUuid(UUID uuid) { 100 | this.uuid = uuid; 101 | } 102 | 103 | public String getToken() { 104 | return token; 105 | } 106 | 107 | public void setToken(String token) { 108 | this.token = token; 109 | } 110 | 111 | public void updateToken(String token){ 112 | updateToken(token,7); 113 | } 114 | 115 | public void updateToken(String token,int day){ 116 | setToken(Utils.sha1(token)); 117 | setToken_time(LocalDateTime.now().plusDays(day)); 118 | } 119 | 120 | public LocalDateTime getToken_time() { 121 | return token_time; 122 | } 123 | 124 | public void setToken_time(LocalDateTime token_time) { 125 | this.token_time = token_time; 126 | } 127 | 128 | public void setToken_timeString(String timeString){ 129 | setToken_time(Utils.str2DateTime(timeString)); 130 | } 131 | 132 | public String getToken_timeString(){ 133 | return Utils.dateTime2String(this.token_time); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/sql/Model.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.sql; 2 | 3 | import org.lintx.plugin.webauth.Config; 4 | import org.lintx.plugin.webauth.models.PlayerModel; 5 | import org.lintx.plugin.webauth.utils.Utils; 6 | 7 | import java.sql.Connection; 8 | import java.sql.PreparedStatement; 9 | import java.sql.ResultSet; 10 | import java.sql.SQLException; 11 | import java.util.UUID; 12 | 13 | public class Model { 14 | private final SqlInterface sql; 15 | public Model(SqlInterface sqlInterface){ 16 | this.sql = sqlInterface; 17 | } 18 | 19 | private PlayerModel playerModelWithResultSet(ResultSet rs){ 20 | try { 21 | PlayerModel model = new PlayerModel(); 22 | model.setId(rs.getInt("id")); 23 | model.setUsername(rs.getString("username")); 24 | model.setPassword(rs.getString("password")); 25 | model.setSlat(rs.getString("slat")); 26 | model.setName(rs.getString("player_name")); 27 | model.setUuid(UUID.fromString(rs.getString("uuid"))); 28 | model.setToken(rs.getString("token")); 29 | model.setToken_timeString(rs.getString("token_time")); 30 | return model; 31 | } catch (SQLException e) { 32 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 33 | e.printStackTrace(); 34 | } 35 | } 36 | return null; 37 | } 38 | 39 | public PlayerModel getPlayerWithId(int id){ 40 | Connection conn = null; 41 | PreparedStatement ps = null; 42 | ResultSet rs = null; 43 | try { 44 | conn = sql.getSQLConnection(); 45 | ps = conn.prepareStatement("SELECT * FROM webauth_player WHERE id=? LIMIT 1"); 46 | ps.setInt(1,id); 47 | 48 | rs = ps.executeQuery(); 49 | while(rs.next()){ 50 | return playerModelWithResultSet(rs); 51 | } 52 | } catch (SQLException e) { 53 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 54 | e.printStackTrace(); 55 | } 56 | } finally { 57 | release(conn,ps,rs); 58 | } 59 | return null; 60 | } 61 | 62 | public PlayerModel getPlayerWithUUID(UUID uuid){ 63 | Connection conn = null; 64 | PreparedStatement ps = null; 65 | ResultSet rs = null; 66 | try { 67 | conn = sql.getSQLConnection(); 68 | ps = conn.prepareStatement("SELECT * FROM webauth_player WHERE uuid=? LIMIT 1"); 69 | ps.setString(1,uuid.toString()); 70 | 71 | rs = ps.executeQuery(); 72 | while(rs.next()){ 73 | return playerModelWithResultSet(rs); 74 | } 75 | } catch (SQLException e) { 76 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 77 | e.printStackTrace(); 78 | } 79 | } finally { 80 | release(conn,ps,rs); 81 | } 82 | return null; 83 | } 84 | 85 | public PlayerModel getPlayerWithToken(String token){ 86 | Connection conn = null; 87 | PreparedStatement ps = null; 88 | ResultSet rs = null; 89 | try { 90 | conn = sql.getSQLConnection(); 91 | ps = conn.prepareStatement("SELECT * FROM webauth_player WHERE token=? LIMIT 1"); 92 | ps.setString(1, Utils.sha1(token)); 93 | 94 | rs = ps.executeQuery(); 95 | while(rs.next()){ 96 | return playerModelWithResultSet(rs); 97 | } 98 | } catch (SQLException e) { 99 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 100 | e.printStackTrace(); 101 | } 102 | } finally { 103 | release(conn,ps,rs); 104 | } 105 | return null; 106 | } 107 | 108 | public PlayerModel getPlayerWithUsername(String username){ 109 | Connection conn = null; 110 | PreparedStatement ps = null; 111 | ResultSet rs = null; 112 | try { 113 | conn = sql.getSQLConnection(); 114 | ps = conn.prepareStatement("SELECT * FROM webauth_player WHERE username=? LIMIT 1"); 115 | ps.setString(1,username); 116 | 117 | rs = ps.executeQuery(); 118 | while(rs.next()){ 119 | return playerModelWithResultSet(rs); 120 | } 121 | } catch (SQLException e) { 122 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 123 | e.printStackTrace(); 124 | } 125 | } finally { 126 | release(conn,ps,rs); 127 | } 128 | return null; 129 | } 130 | 131 | public PlayerModel getPlayerWithPlayerName(String name){ 132 | Connection conn = null; 133 | PreparedStatement ps = null; 134 | ResultSet rs = null; 135 | try { 136 | conn = sql.getSQLConnection(); 137 | ps = conn.prepareStatement("SELECT * FROM webauth_player WHERE player_name=? LIMIT 1"); 138 | ps.setString(1,name); 139 | 140 | rs = ps.executeQuery(); 141 | while(rs.next()){ 142 | return playerModelWithResultSet(rs); 143 | } 144 | } catch (SQLException e) { 145 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 146 | e.printStackTrace(); 147 | } 148 | } finally { 149 | release(conn,ps,rs); 150 | } 151 | return null; 152 | } 153 | 154 | public boolean updatePlayer(PlayerModel model){ 155 | Connection conn = null; 156 | PreparedStatement ps = null; 157 | try { 158 | conn = sql.getSQLConnection(); 159 | ps = conn.prepareStatement("update webauth_player set password=?,uuid=?,player_name=?,token=?,token_time=? where id=?"); 160 | ps.setString(1,model.getPassword()); 161 | ps.setString(2,model.getUuid().toString()); 162 | ps.setString(3,model.getName()); 163 | ps.setString(4,model.getToken()); 164 | ps.setString(5,model.getToken_timeString()); 165 | ps.setInt(6,model.getId()); 166 | 167 | int r = ps.executeUpdate(); 168 | return r>0; 169 | } catch (SQLException e) { 170 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 171 | e.printStackTrace(); 172 | } 173 | } finally { 174 | release(conn,ps,null); 175 | } 176 | return false; 177 | } 178 | 179 | public boolean checkPlayerUsername(String username){ 180 | PlayerModel model = getPlayerWithUsername(username); 181 | if (model!=null) return false; 182 | return checkPlayerName(username); 183 | } 184 | 185 | public boolean checkPlayerName(String name){ 186 | PlayerModel model = getPlayerWithPlayerName(name); 187 | return model==null; 188 | } 189 | 190 | public boolean insertPlayer(PlayerModel model){ 191 | if (getPlayerWithUUID(model.getUuid())!=null){ 192 | return false; 193 | } 194 | Connection conn = null; 195 | PreparedStatement ps = null; 196 | try { 197 | conn = sql.getSQLConnection(); 198 | ps = conn.prepareStatement("insert into webauth_player (username,password,slat,uuid,player_name,token,token_time) values (?,?,?,?,?,?,?)"); 199 | ps.setString(1,model.getUsername()); 200 | ps.setString(2,model.getPassword()); 201 | ps.setString(3,model.getSlat()); 202 | ps.setString(4,model.getUuid().toString()); 203 | ps.setString(5,model.getName()); 204 | ps.setString(6,model.getToken()); 205 | ps.setString(7,model.getToken_timeString()); 206 | 207 | int r = ps.executeUpdate(); 208 | return r>0; 209 | } catch (SQLException e) { 210 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 211 | e.printStackTrace(); 212 | } 213 | } finally { 214 | release(conn,ps,null); 215 | } 216 | return false; 217 | } 218 | 219 | private void release(Connection conn, PreparedStatement ps, ResultSet rs) { 220 | if (rs != null) { 221 | try { 222 | rs.close(); 223 | } catch (Exception e) { 224 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 225 | e.printStackTrace(); 226 | } 227 | } 228 | } 229 | if (ps != null) { 230 | try { 231 | ps.close(); 232 | } catch (Exception e) { 233 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 234 | e.printStackTrace(); 235 | } 236 | } 237 | } 238 | if (conn != null) { 239 | try { 240 | conn.close(); 241 | } catch (Exception e) { 242 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 243 | e.printStackTrace(); 244 | } 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/sql/MySql.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.sql; 2 | 3 | import org.lintx.plugin.webauth.Config; 4 | 5 | import java.lang.reflect.Proxy; 6 | import java.sql.*; 7 | import java.time.Duration; 8 | import java.time.LocalDateTime; 9 | import java.util.*; 10 | 11 | public class MySql implements SqlInterface { 12 | private static String db_url; 13 | private static String username; 14 | private static String password; 15 | 16 | // private static LinkedList linkedlist = new LinkedList<>(); 17 | private static LinkedList pools = new LinkedList<>(); 18 | //最小连接数量 19 | private static int jdbcConnectionInitSize = 10; 20 | 21 | //当前最大连接数量=max*jdbcConnectionInitSize 22 | private static int max = 1; 23 | 24 | private final long timeout; 25 | 26 | public MySql(String uri,String username,String password,long timeout){ 27 | MySql.db_url = uri; 28 | MySql.username = username; 29 | MySql.password = password; 30 | this.timeout = timeout; 31 | load(); 32 | } 33 | 34 | public void close(){ 35 | linkedListClear(); 36 | } 37 | 38 | private void linkedListClear(){ 39 | try { 40 | final ConnectionPool pool = pools.removeFirst(); 41 | if (pool!=null) { 42 | pool.close = true; 43 | pool.connection.close(); 44 | linkedListClear(); 45 | } 46 | } catch (Exception e) { 47 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 48 | e.printStackTrace(); 49 | } 50 | } 51 | } 52 | 53 | public Connection getSQLConnection() { 54 | if (pools.size() == 0 && max <= 5 * jdbcConnectionInitSize) { 55 | try { 56 | Class.forName("com.mysql.jdbc.Driver"); 57 | } catch (ClassNotFoundException e) { 58 | e.printStackTrace(); 59 | } 60 | try { 61 | for (int i = 0; i < jdbcConnectionInitSize; i++) { 62 | Connection conn = DriverManager.getConnection(db_url, username, password); 63 | ConnectionPool pool = new ConnectionPool(conn,timeout); 64 | pool.connection = (Connection) Proxy.newProxyInstance( 65 | pool.connection.getClass().getClassLoader(), 66 | pool.connection.getClass().getInterfaces(), (proxy, method, args) -> { 67 | if (!method.getName().equalsIgnoreCase("close") || pool.close) { 68 | if (method.getName().equalsIgnoreCase("close") && pool.close){ 69 | max--; 70 | } 71 | return method.invoke(pool.connection, args); 72 | } else { 73 | pool.update(); 74 | pools.add(pool); 75 | return null; 76 | } 77 | } 78 | ); 79 | 80 | pools.add(pool); 81 | max++; 82 | } 83 | } 84 | catch (Exception e){ 85 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 86 | e.printStackTrace(); 87 | } 88 | } 89 | } 90 | if (pools.size() > 0) { 91 | final ConnectionPool pool = pools.removeFirst(); 92 | if (!pool.check()){ 93 | pool.close = true; 94 | try { 95 | pool.connection.close(); 96 | } catch (SQLException e) { 97 | e.printStackTrace(); 98 | } 99 | return getSQLConnection(); 100 | } 101 | 102 | return pool.connection; 103 | } 104 | return null; 105 | } 106 | 107 | 108 | 109 | private void load() { 110 | Connection connection = getSQLConnection(); 111 | if (connection==null){ 112 | return; 113 | } 114 | try { 115 | Statement s = connection.createStatement(); 116 | String SQLiteCreateTable1 = "CREATE TABLE IF NOT EXISTS `webauth_player` (" + 117 | "`id` int PRIMARY KEY NOT NULL AUTO_INCREMENT," + 118 | "`username` varchar(64) NOT NULL," + 119 | "`password` varchar(64) NOT NULL," + 120 | "`slat` varchar(64) NOT NULL," + 121 | "`player_name` varchar(16) NOT NULL," + 122 | "`uuid` varchar(64) NOT NULL," + 123 | "`token` varchar(64) NOT NULL," + 124 | "`token_time` varchar(20) NOT NULL" + 125 | ");"; 126 | s.executeUpdate(SQLiteCreateTable1); 127 | // String SQLiteCreateTable2 = "CREATE TABLE IF NOT EXISTS `player_history` (" + 128 | // "`player_id` INTEGER," + 129 | // "`name` varchar(64) NOT NULL" + 130 | // ");"; 131 | // s.executeUpdate(SQLiteCreateTable2); 132 | s.close(); 133 | } catch (SQLException e) { 134 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 135 | e.printStackTrace(); 136 | } 137 | } 138 | try { 139 | connection.close(); 140 | } catch (SQLException e) { 141 | if (Config.getInstance().getDatabaseConfig().isPrintError()){ 142 | e.printStackTrace(); 143 | } 144 | } 145 | } 146 | 147 | static class ConnectionPool{ 148 | Connection connection; 149 | LocalDateTime lastTime; 150 | final long timeout; 151 | boolean close = false; 152 | ConnectionPool(Connection connection,long timeout){ 153 | this.connection = connection; 154 | this.timeout = timeout; 155 | update(); 156 | } 157 | 158 | void update(){ 159 | lastTime = LocalDateTime.now(); 160 | } 161 | 162 | boolean check(){ 163 | Duration duration = Duration.between(LocalDateTime.now(),lastTime); 164 | return duration.getSeconds() > timeout; 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/sql/SQLite.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.sql; 2 | 3 | import java.io.File; 4 | import java.sql.*; 5 | import java.util.*; 6 | 7 | public class SQLite implements SqlInterface { 8 | private Connection connection; 9 | private String dbname; 10 | private File folder; 11 | public SQLite(File folder, String dbname){ 12 | this.folder = folder; 13 | this.dbname = dbname; 14 | load(); 15 | } 16 | 17 | public Connection getSQLConnection() { 18 | try { 19 | if(connection!=null&&!connection.isClosed()){ 20 | return connection; 21 | } 22 | 23 | File dataFolder = new File(folder, dbname+".db"); 24 | 25 | Class.forName("org.sqlite.JDBC"); 26 | Properties properties = new Properties(); 27 | properties.setProperty("characterEncoding", "UTF-8"); 28 | properties.setProperty("encoding", "\"UTF-8\""); 29 | connection = DriverManager.getConnection("jdbc:sqlite:" + dataFolder.toString()); 30 | return connection; 31 | } catch (SQLException | ClassNotFoundException ex) { 32 | ex.printStackTrace(); 33 | } 34 | return null; 35 | } 36 | 37 | private void load() { 38 | connection = getSQLConnection(); 39 | if (connection==null){ 40 | return; 41 | } 42 | try { 43 | Statement s = connection.createStatement(); 44 | String SQLiteCreateTable1 = "CREATE TABLE IF NOT EXISTS \"webauth_player\" (" + 45 | "\"id\" INTEGER PRIMARY KEY," + 46 | "\"username\" varchar(64) NOT NULL," + 47 | "\"password\" varchar(64) NOT NULL," + 48 | "\"slat\" varchar(64) NOT NULL," + 49 | "\"player_name\" varchar(16) NOT NULL," + 50 | "\"uuid\" varchar(64) NOT NULL," + 51 | "\"token\" varchar(64) NOT NULL," + 52 | "\"token_time\" varchar(20) NOT NULL" + 53 | ");"; 54 | s.executeUpdate(SQLiteCreateTable1); 55 | // String SQLiteCreateTable2 = "CREATE TABLE IF NOT EXISTS \"player_history\" (" + 56 | // "\"player_id\" INTEGER," + 57 | // "\"name\" varchar(64) NOT NULL" + 58 | // ");"; 59 | // s.executeUpdate(SQLiteCreateTable2); 60 | s.close(); 61 | } catch (SQLException e) { 62 | e.printStackTrace(); 63 | } 64 | try { 65 | connection.close(); 66 | } catch (SQLException ignored) { 67 | 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/sql/SqlInterface.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.sql; 2 | 3 | import java.sql.Connection; 4 | 5 | public interface SqlInterface { 6 | Connection getSQLConnection(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/utils/MojangApi.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.utils; 2 | 3 | import com.google.gson.Gson; 4 | import org.lintx.plugin.webauth.Config; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | import java.io.Reader; 9 | import java.net.HttpURLConnection; 10 | import java.net.InetSocketAddress; 11 | import java.net.Proxy; 12 | import java.net.URL; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.UUID; 15 | 16 | public class MojangApi { 17 | public static MojangAccount getMojangAccount(String name){ 18 | try { 19 | String url = "https://api.mojang.com/users/profiles/minecraft/" + name; 20 | URL obj = new URL(url); 21 | Config config = Config.getInstance(); 22 | HttpURLConnection conn; 23 | if (!config.getHttpProxyAddress().isEmpty() && config.getHttpProxyPort()>0){ 24 | Proxy.Type proxyType = Proxy.Type.HTTP; 25 | if (config.getHttpProxyType().equalsIgnoreCase("socks")){ 26 | proxyType = Proxy.Type.SOCKS; 27 | } 28 | Proxy proxy = new Proxy(proxyType,new InetSocketAddress(config.getHttpProxyAddress(),config.getHttpProxyPort())); 29 | conn = (HttpURLConnection) obj.openConnection(proxy); 30 | }else { 31 | conn = (HttpURLConnection) obj.openConnection(); 32 | } 33 | conn.setRequestMethod("GET"); 34 | 35 | if (conn.getInputStream()==null){ 36 | return new MojangAccount(); 37 | } 38 | 39 | try(Reader reader = new InputStreamReader(conn.getInputStream(),StandardCharsets.UTF_8)) { 40 | return new Gson().fromJson(reader,MojangAccount.class); 41 | } catch (IOException ignored) { 42 | 43 | } 44 | }catch (Exception ignore){ 45 | 46 | } 47 | return new MojangAccount(); 48 | } 49 | 50 | public static class MojangAccount{ 51 | public String id = ""; 52 | public String name = ""; 53 | 54 | public boolean checkName(String name){ 55 | return !name.equalsIgnoreCase(this.name); 56 | } 57 | 58 | public UUID getUUID(){ 59 | String str = this.name; 60 | if (str==null || str.isEmpty()) return null; 61 | if (str.length()==32){ 62 | str = str.substring(0,8) + "-" + str.substring(8,12) + "-" + str.substring(12,16) + "-" + str.substring(16,20) + "-" + str.substring(20,32); 63 | } 64 | try { 65 | return UUID.fromString(str); 66 | }catch (Exception ignore){ 67 | return null; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/lintx/plugin/webauth/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package org.lintx.plugin.webauth.utils; 2 | 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | import java.time.LocalDateTime; 6 | import java.time.format.DateTimeFormatter; 7 | import java.util.UUID; 8 | 9 | public class Utils { 10 | private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); 11 | private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 12 | 13 | public static UUID newUUID(){ 14 | UUID uuid = UUID.randomUUID(); 15 | long most = uuid.getMostSignificantBits(); 16 | most = most << 32 >>> 32; 17 | return new UUID(most,uuid.getLeastSignificantBits()); 18 | } 19 | 20 | public static String newToken(){ 21 | //14*4=56,4位字节会换成一位字符串 22 | UUID uuid = UUID.randomUUID(); 23 | long hi = 1L << 56; 24 | long val = uuid.getLeastSignificantBits(); 25 | String str = Long.toHexString(hi | (val & (hi - 1))).substring(1); 26 | return "=" + str + "="; 27 | } 28 | 29 | public static String newPassword(){ 30 | //14*4=56,4位字节会换成一位字符串 31 | UUID uuid = UUID.randomUUID(); 32 | long hi = 1L << 40; 33 | long val = uuid.getLeastSignificantBits(); 34 | return Long.toHexString(hi | (val & (hi - 1))).substring(1); 35 | } 36 | 37 | public static String sha1(String string){ 38 | try { 39 | MessageDigest messageDigest = MessageDigest.getInstance("SHA1"); 40 | messageDigest.update(string.getBytes()); 41 | byte[] bytes = messageDigest.digest(); 42 | 43 | char[] hexChars = new char[bytes.length * 2]; 44 | for (int j = 0; j < bytes.length; j++) { 45 | int v = bytes[j] & 0xFF; 46 | hexChars[j * 2] = HEX_ARRAY[v >>> 4]; 47 | hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; 48 | } 49 | return new String(hexChars); 50 | } catch (NoSuchAlgorithmException ignored) { 51 | 52 | } 53 | return ""; 54 | } 55 | 56 | public static LocalDateTime str2DateTime(String str){ 57 | try { 58 | return LocalDateTime.parse(str,dateTimeFormatter); 59 | } 60 | catch (Exception e){ 61 | return null; 62 | } 63 | } 64 | 65 | public static String dateTime2String(LocalDateTime date){ 66 | return dateTimeFormatter.format(date); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/resources/bungee.yml: -------------------------------------------------------------------------------- 1 | main: org.lintx.plugin.webauth.WebAuth 2 | name: ${artifactId} 3 | version: ${version} 4 | author: LinTx -------------------------------------------------------------------------------- /websource/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-auth-web", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build_devlopment": "webpack --env.development", 7 | "build_production": "webpack --env.production" 8 | }, 9 | "dependencies": { 10 | "bootstrap": "^4.3.1", 11 | "jquery": "^3.4.1", 12 | "particles.js": "^2.0.0", 13 | "popper.js": "^1.15.0", 14 | "vue": "^2.6.10", 15 | "vue-router": "^3.1.2", 16 | "vue-toast-notification": "0.0.2" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.4.4", 20 | "@babel/preset-env": "^7.4.4", 21 | "autoprefixer": "^9.5.1", 22 | "babel-loader": "^8.0.5", 23 | "css-loader": "^2.1.1", 24 | "cssnano": "^4.1.10", 25 | "file-loader": "^3.0.1", 26 | "html-webpack-plugin": "^4.0.0-beta.5", 27 | "html-withimg-loader": "^0.1.16", 28 | "mini-css-extract-plugin": "^0.6.0", 29 | "node-sass": "^4.12.0", 30 | "optimize-css-assets-webpack-plugin": "^5.0.1", 31 | "postcss-import": "^12.0.1", 32 | "postcss-loader": "^3.0.0", 33 | "sass-loader": "^7.1.0", 34 | "style-loader": "^0.23.1", 35 | "url-loader": "^1.1.2", 36 | "vue-loader": "^15.7.1", 37 | "vue-template-compiler": "^2.6.10", 38 | "webpack": "^4.30.0", 39 | "webpack-cleanup-plugin": "^0.5.1", 40 | "webpack-cli": "^3.3.1", 41 | "webpack-plugin-hash-output": "^3.2.1" 42 | }, 43 | "author": "LinTx", 44 | "license": "ISC", 45 | "private": true 46 | } 47 | -------------------------------------------------------------------------------- /websource/src/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | WebAuth 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /websource/src/static/css/index.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; 2 | @import "~vue-toast-notification"; 3 | 4 | :root { 5 | --input-padding-x: .75rem; 6 | --input-padding-y: .75rem; 7 | } 8 | 9 | html, 10 | body { 11 | height: 100%; 12 | background-color: #333; 13 | } 14 | 15 | body { 16 | display: -ms-flexbox; 17 | display: -webkit-box; 18 | display: flex; 19 | -ms-flex-align: center; 20 | -ms-flex-pack: center; 21 | -webkit-box-align: center; 22 | align-items: center; 23 | -webkit-box-pack: center; 24 | justify-content: center; 25 | color: #fff; 26 | text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); 27 | box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5); 28 | } 29 | 30 | 31 | .form-signin { 32 | width: 100%; 33 | max-width: 420px; 34 | padding: 15px; 35 | margin: 0 auto; 36 | } 37 | 38 | .form-label-group { 39 | position: relative; 40 | text-align: left; 41 | //margin-bottom: 1rem; 42 | .form-control{ 43 | height: auto; 44 | } 45 | } 46 | 47 | .form-label-group > input, 48 | .form-label-group > label { 49 | padding: var(--input-padding-y) var(--input-padding-x); 50 | } 51 | 52 | .form-label-group > label { 53 | position: absolute; 54 | top: 0; 55 | left: 0; 56 | display: block; 57 | width: 100%; 58 | margin-bottom: 0; /* Override default `` margin */ 59 | line-height: 1.5; 60 | color: #495057; 61 | border: 1px solid transparent; 62 | border-radius: .25rem; 63 | transition: all .1s ease-in-out; 64 | } 65 | 66 | .form-label-group input::-webkit-input-placeholder { 67 | color: transparent; 68 | } 69 | 70 | .form-label-group input:-ms-input-placeholder { 71 | color: transparent; 72 | } 73 | 74 | .form-label-group input::-ms-input-placeholder { 75 | color: transparent; 76 | } 77 | 78 | .form-label-group input::-moz-placeholder { 79 | color: transparent; 80 | } 81 | 82 | .form-label-group input::placeholder { 83 | color: transparent; 84 | } 85 | 86 | .form-label-group input:not(:placeholder-shown) { 87 | padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3)); 88 | padding-bottom: calc(var(--input-padding-y) / 3); 89 | } 90 | 91 | .form-label-group input:not(:placeholder-shown) ~ label { 92 | padding-top: calc(var(--input-padding-y) / 3); 93 | padding-bottom: calc(var(--input-padding-y) / 3); 94 | font-size: 12px; 95 | color: #777; 96 | } 97 | 98 | form .form-label-group:not(:last-child){ 99 | margin-bottom: -2px; 100 | input{ 101 | border-bottom-right-radius: 0; 102 | border-bottom-left-radius: 0; 103 | } 104 | } 105 | 106 | form .form-label-group:not(:first-child){ 107 | input{ 108 | border-top-right-radius: 0; 109 | border-top-left-radius: 0; 110 | } 111 | } 112 | 113 | form .form-label-group input.form-control:focus{ 114 | border-color: #ccc; 115 | box-shadow: none; 116 | } 117 | 118 | 119 | 120 | /* 121 | * Globals 122 | */ 123 | 124 | /* Links */ 125 | a, 126 | a:focus, 127 | a:hover { 128 | color: #fff; 129 | } 130 | 131 | 132 | .cover-container { 133 | max-width: 42em; 134 | } 135 | 136 | 137 | /* 138 | * Header 139 | */ 140 | .masthead { 141 | margin-bottom: 2rem; 142 | min-width: 320px; 143 | } 144 | 145 | .masthead-brand { 146 | margin-bottom: 0; 147 | } 148 | 149 | .nav-masthead .nav-link { 150 | padding: .25rem 0; 151 | font-weight: 700; 152 | color: rgba(255, 255, 255, .5); 153 | background-color: transparent; 154 | border-bottom: .25rem solid transparent; 155 | } 156 | 157 | .nav-masthead .nav-link:hover{ 158 | border-bottom-color: rgba(255, 255, 255, .25); 159 | } 160 | 161 | .nav-masthead .nav-link + .nav-link { 162 | margin-left: 1rem; 163 | } 164 | 165 | .nav-masthead .active { 166 | color: #fff; 167 | border-bottom-color: #fff; 168 | } 169 | 170 | @media (min-width: 48em) { 171 | .masthead-brand { 172 | float: left; 173 | } 174 | .nav-masthead { 175 | float: right; 176 | } 177 | } 178 | 179 | 180 | /* 181 | * Cover 182 | */ 183 | .cover { 184 | padding: 0 1.5rem; 185 | } 186 | .cover .btn-lg { 187 | padding: .75rem 1.25rem; 188 | font-weight: 700; 189 | } 190 | 191 | 192 | /* 193 | * Footer 194 | */ 195 | .mastfoot { 196 | color: rgba(255, 255, 255, .5); 197 | } -------------------------------------------------------------------------------- /websource/src/static/js/index.js: -------------------------------------------------------------------------------- 1 | import "bootstrap" 2 | import Vue from "vue/dist/vue" 3 | import "../css/index.scss" 4 | import Session from "./libs/session"; 5 | import VueRouter from "vue-router"; 6 | import VueToast from "vue-toast-notification"; 7 | import loginPage from "../vue/login.vue"; 8 | import indexPage from "../vue/index.vue"; 9 | import registerPage from "../vue/register.vue"; 10 | import helpPage from "../vue/help.vue"; 11 | import mePage from "../vue/me.vue"; 12 | import changePasswordPage from "../vue/changePassword.vue"; 13 | import changePlayerNamePage from "../vue/changePlayerName.vue"; 14 | 15 | Vue.use(VueRouter); 16 | Vue.use(VueToast,{ 17 | position:"top" 18 | }); 19 | 20 | const routes = [ 21 | {path:"/",name:"index",component:mePage}, 22 | {path:"/login",name:"login",component:loginPage}, 23 | {path:"/register",name:"register",component:registerPage}, 24 | {path:"/help",name:"help",component:helpPage}, 25 | {path:"/changePassword",name:"changePassword",component:changePasswordPage}, 26 | {path:"/changeUserName",name:"changeUserName",component:changePlayerNamePage} 27 | ]; 28 | 29 | const router = new VueRouter({ 30 | mode:'hash', 31 | routes: routes 32 | }); 33 | 34 | var baseData = null; 35 | 36 | router.beforeEach((to,from,next)=>{ 37 | let hasRouter = false; 38 | routes.forEach((r)=>{ 39 | if (r.path===to.path){ 40 | hasRouter = true; 41 | } 42 | }); 43 | if (!hasRouter){ 44 | next("/"); 45 | return; 46 | } 47 | if (baseData===null){ 48 | Session.getBaseData((data)=>{ 49 | baseData = data; 50 | routerBefore(to,from,next); 51 | }); 52 | }else { 53 | routerBefore(to,from,next); 54 | } 55 | }); 56 | 57 | function routerBefore(to,from,next){ 58 | if (to.path!=="/login" && to.path!=="/register" && to.path!=="/help"){ 59 | Session.getBaseData((data)=>{ 60 | if (Session.getPlayerData().userId===0){ 61 | next("/login"); 62 | }else { 63 | next(); 64 | } 65 | }); 66 | return; 67 | } 68 | if (to.path==="/login" || to.path==="/register"){ 69 | if (Session.getPlayerData().userId!==0){ 70 | next("/"); 71 | return; 72 | } 73 | } 74 | next(); 75 | } 76 | 77 | let app = new Vue({ 78 | el:"#app", 79 | router:router, 80 | render:c=>c(indexPage) 81 | }); 82 | -------------------------------------------------------------------------------- /websource/src/static/vue/changePassword.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 修改密码 5 | 6 | 7 | 8 | 9 | 10 | 新密码 11 | 12 | 13 | 14 | 15 | 重复密码 16 | 17 | 18 | 19 | 修改密码 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /websource/src/static/vue/changePlayerName.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /websource/src/static/vue/closeChangePlayerName.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 出错了 4 | 服务器禁止修改玩家名,请联系服主或管理员 5 | 返回首页 6 | 7 | -------------------------------------------------------------------------------- /websource/src/static/vue/closeRegister.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 出错了 4 | 服务器禁止注册新用户,请联系服主或管理员 5 | 已有帐号,去登录 6 | 7 | -------------------------------------------------------------------------------- /websource/src/static/vue/help.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 帮助 4 | 5 | 6 | 本系统是WebAuth插件的用户管理系统。 7 | 如果您已经注册,可以 点击这里 来登录。 8 | 如果您尚未注册,且插件配置允许注册,您应该先 注册 一个帐号,否则请联系服主或管理员来申请帐号。 9 | 使用了本插件的服务器,非正版玩家,需要使用“登录凭据”来登录游戏,您第一次登录后就可以看见您的“登录凭据”,下次登录时“登录凭据”将无法查看,除非您“重置登录凭据”使得旧的凭据失效。 10 | 使用“登录凭据”登录游戏服务器的方法:直接将“登录凭据”作为离线用户名登录游戏即可(请注意,“登录凭据”的有效时间为7天,7天后这个“登录凭据”将无法登录游戏,届时您需要在该系统重置“登录凭据”) 11 | 注册帐号时,注册的帐号就是您的玩家名,如果服务器允许,您可以在之后自由修改玩家名且一般不会造成玩家数据丢失,部分服务器使用中文玩家名可能会发生未知错误。 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /websource/src/static/vue/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebAuth 6 | 7 | 主页 8 | 帮助 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /websource/src/static/vue/login.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 登录 5 | 6 | 7 | 8 | 9 | 10 | 帐号 11 | 12 | 13 | 14 | 15 | 密码 16 | 17 | 18 | 19 | 立即登录 20 | 没有帐号,去注册 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /websource/src/static/vue/me.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 我的资料 4 | 5 | ID 6 | {{userId}} 7 | 8 | 9 | 玩家名 10 | {{playerName}}修改 11 | 12 | 13 | 登录凭据 14 | {{userToken}}刷新 15 | 16 | 17 | 过期时间 18 | {{userTokenTime}} 19 | 20 | 21 | 修改密码 22 | 退出登录 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /websource/src/static/vue/openChangePlayerName.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 修改玩家名 5 | 6 | 7 | 8 | 9 | 10 | 玩家名 11 | 12 | 13 | 14 | 修改玩家名 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /websource/src/static/vue/openRegister.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 注册 5 | 6 | 7 | 8 | 9 | 10 | 帐号/玩家名 11 | 12 | 13 | 14 | 15 | 密码 16 | 17 | 18 | 19 | 20 | 重复密码 21 | 22 | 23 | 24 | 立即注册 25 | 已有帐号,去登录 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /websource/src/static/vue/register.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /websource/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const WebpackCleanupPlugin = require('webpack-cleanup-plugin'); 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 8 | const WebPack = require('webpack'); 9 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 10 | 11 | //会编译的html文件后缀 12 | const htmlExtensions = ['.html','.htm','.tpl']; 13 | 14 | const host = { 15 | dev:'/bungeewebauth/', 16 | dis:'/bungeewebauth/' 17 | }; 18 | 19 | //目录配置 20 | const paths = { 21 | root:{ 22 | //开发环境输出根目录 23 | dev:path.resolve(__dirname, '../../../minecraft.node.js/bungeecord-server/plugins/WebAuth/web'), 24 | //生产环境输出根目录 25 | dis:path.resolve(__dirname, '../src/main/resources/web'), 26 | //源码文件根目录 27 | src:path.resolve(__dirname, './src') 28 | }, 29 | outpath:{ 30 | //输出的js文件目录 31 | js:'static/js/', 32 | //输出的图片文件目录 33 | img:'static/images/', 34 | //输出的字体文件目录 35 | font:'static/fonts/', 36 | //输出的css文件目录 37 | css:'static/css/', 38 | //输出的模版文件目录 39 | html:'' 40 | }, 41 | srcpath:{ 42 | //js源码文件所在目录 43 | js:'static/js/', 44 | //模版源码文件所在目录 45 | html:'html' 46 | } 47 | }; 48 | 49 | module.exports = function (env, argv) { 50 | let mode = 'development'; 51 | let development = true; 52 | if (typeof env === 'object' && env.hasOwnProperty('production') && env.production === true) { 53 | mode = 'production'; 54 | development = false; 55 | } 56 | let publicPath = development?host.dev:host.dis; 57 | let webpackConfig = { 58 | // mode: 'none',//"production" | "development" | "none" 59 | mode: mode, 60 | entry: {},//具体内容由后面编写的脚本填充 61 | output: { 62 | //输出文件根目录,绝对路径 63 | path: development?paths.root.dev:paths.root.dis, 64 | 65 | //输出文件名 66 | filename: paths.outpath.js+'[name].js', 67 | 68 | //sourcemap输出文件名 69 | //[file]:生成后的js文件名(包括路径),[filebase]:生成后的js文件名(不包括路径) 70 | sourceMapFilename: '[file].map', 71 | 72 | //chunk文件输出文件名 73 | chunkFilename:paths.outpath.js+'common.[id].js', 74 | 75 | // publicPath: 相对目录,可以使用cdn地址(开发环境不设置等) 76 | publicPath:publicPath 77 | }, 78 | //优化选项 79 | optimization:{ 80 | splitChunks:{ 81 | cacheGroups:{ 82 | //公共模块配置 83 | commons:{ 84 | //对导入方式的设置,'async':只管动态导入的,'initial':只管静态导入的,'all':所有的 85 | //静态导入:import 'xxx',动态导入:import('xxx') 86 | chunks: 'all', 87 | //有2个chunk使用才切分到公共文件中 88 | minChunks:2, 89 | //最大并发数,简单理解为一个entry及包含的文件最多被拆分成多少个chunk 90 | maxInitialRequests:5, 91 | //0以上的大小就会切分chunk 92 | minSize:0, 93 | // name:'[chunkhash]'//影响文件名、sourcemap文件名、其他地方引用的chunk的name 94 | // filename:paths.outpath.js+'common.js' 95 | }, 96 | vendor:{ 97 | test:/[\\/]node_modules[\\/]/i, 98 | chunks:'all', 99 | priority:10, 100 | enforce:true 101 | } 102 | } 103 | } 104 | }, 105 | module: { 106 | rules: [ 107 | { 108 | //对ES6语法进行编译 109 | test: /\.js$/i, 110 | exclude:/[\\/]node_modules[\\/]/i, 111 | loader:'babel-loader', 112 | options:{ 113 | presets:['@babel/preset-env'] 114 | } 115 | }, 116 | { 117 | //css文件的处理 118 | test: /\.(css|scss)$/i, 119 | use : [ 120 | //使用插件将css文件提取为单独的文件 121 | MiniCssExtractPlugin.loader, 122 | 123 | //css解析需要的配置 124 | 'css-loader', 125 | 126 | // 处理CSS压缩、@import等需要的配置 127 | { 128 | loader: 'postcss-loader', 129 | options: { 130 | plugins: [ 131 | require('postcss-import')(), 132 | //自动添加fixer(比如--webkit-等) 133 | require('autoprefixer')({ 134 | browsers: ['last 30 versions', "> 2%", "Firefox >= 10", "ie 6-11"] 135 | }) 136 | ] 137 | } 138 | }, 139 | 140 | //处理sass 141 | 'sass-loader', 142 | ] 143 | }, 144 | { 145 | //css中图片的处理 146 | test: /\.(png|svg|jpg|gif)$/i, 147 | use:[ 148 | { 149 | //使用urlloader将图片自动转换成base64/文件 150 | loader:'url-loader', 151 | options:{ 152 | //文件名 153 | name:paths.outpath.img+'[name].[ext]', 154 | 155 | //单独的publicpath 156 | publicPath:publicPath, 157 | // outputPath:path.resolve(__dirname, development?'./output/dev':'./output/dis'), 158 | 159 | //base64/文件的文件大小界限(K) 160 | limit:200 161 | } 162 | } 163 | ] 164 | }, 165 | { 166 | test:/\.(woff|woff2|eot|ttf|otf|svg)$/i, 167 | use:[ 168 | { 169 | loader:'file-loader', 170 | options:{ 171 | name:paths.outpath.font+'[name].[ext]', 172 | publicPath:publicPath, 173 | limit: 0 174 | } 175 | } 176 | ] 177 | }, 178 | { 179 | //处理html文件中的资源文件,比如图片,提取后匹配上面的图片test,然后由urlloader处理 180 | test:/\.(html)$/i, 181 | use:['html-withimg-loader'] 182 | }, 183 | { 184 | test: /.vue$/, 185 | loader: 'vue-loader' 186 | } 187 | ] 188 | }, 189 | plugins: [ 190 | //css单独打包插件 191 | new MiniCssExtractPlugin({filename:paths.outpath.css+'[id].css'}), 192 | 193 | //打包前用于清空 output 目录 194 | new WebpackCleanupPlugin(), 195 | 196 | //全局使用jq 197 | new WebPack.ProvidePlugin({ 198 | $: "jquery", 199 | jQuery: "jquery" 200 | }), 201 | 202 | new VueLoaderPlugin() 203 | ], 204 | // devtool 更详细的资料:https://segmentfault.com/a/1190000008315937 205 | devtool: 'source-map' 206 | }; 207 | if (!development) { 208 | //生产模式配置CSS压缩 209 | webpackConfig.plugins.push(new OptimizeCSSAssetsPlugin({ 210 | assetNameRegExp: /\.css$/g, 211 | cssProcessor: require('cssnano'), 212 | // cssProcessorOptions: cssnanoOptions, 213 | cssProcessorPluginOptions: { 214 | preset: ['default', { 215 | discardComments: { 216 | removeAll: true, 217 | }, 218 | normalizeUnicode: false 219 | }] 220 | }, 221 | canPrint: true 222 | })); 223 | } 224 | 225 | //批量遍历寻找html文件,然后寻找对应的js文件,并添加到webpack config中 226 | let addhtml = (root,sub)=>{ 227 | let p = path.resolve(root,paths.srcpath.html,sub); 228 | 229 | //读取子目录文件列表 230 | let f = fs.readdirSync(p); 231 | f.forEach(file=>{ 232 | let child = path.resolve(p,file); //文件/文件夹绝对路径 233 | let stats = fs.statSync(child); //文件/文件夹状态 234 | if (stats.isDirectory()) { 235 | //是目录,遍历子目录 236 | addhtml(root,sub + path.sep + file); 237 | } 238 | else { 239 | let extension = path.extname(file); 240 | 241 | //只处理指定后缀的文件 242 | if (htmlExtensions.indexOf(extension.toLocaleString())===-1) { 243 | return; 244 | } 245 | 246 | //获取不带后缀的文件名 247 | let name = file.substring(0,file.lastIndexOf(extension)); 248 | 249 | //组装对应的js文件的绝对路径 250 | let js = path.resolve(root,paths.srcpath.js,sub,name+'.js'); 251 | 252 | //对应的js文件存在,则添加进entry 253 | if (fs.existsSync(js) && fs.statSync(js).isFile()) { 254 | webpackConfig.entry[name] = js; 255 | } 256 | 257 | //添加htmlwebpackplugin配置 258 | webpackConfig.plugins.push(new HtmlWebpackPlugin({ 259 | //输出文件名 260 | filename: paths.outpath.html + sub + path.sep + name + extension, 261 | 262 | //模板文件(源文件) 263 | template: child, 264 | 265 | //插入的内容在哪里(true|'head'|'body'|false) 266 | inject: true, 267 | 268 | //是否闭合link标签(xhtml标准) 269 | xhtml: true, 270 | 271 | //在插入的js、css标签后面加上hash 272 | hash: true, 273 | 274 | //这个html文件中插入的chunks,3.x版本的插件不能处理common chunk,4.x版本可以自动处理 275 | chunks: [name] 276 | })); 277 | } 278 | }) 279 | }; 280 | addhtml(paths.root.src,'./'); 281 | 282 | return webpackConfig; 283 | }; --------------------------------------------------------------------------------
服务器禁止修改玩家名,请联系服主或管理员
服务器禁止注册新用户,请联系服主或管理员