1.项目介绍本项目基础架构为:SpringBoot+Thymleaf+Mybatis整合SpringBoot版本:2.3.52.创建项目(打开IDEA,选择File->New->Project,在弹出的对话框中选择SpringInitializr)3.设置项目基础信息4.选择项目基础依赖5.设置项目保存路径6.创建完成等待项目依赖下载完成,则项目框架到此全部搭建完成7.pom.xml最终项目的pom.xml:<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.5.RELEASE</version><relativePath/><!--lookupparentfromrepository--></parent><groupId>cn.coralcloud</groupId><artifactId>ims</artifactId><version>0.0.1-SNAPSHOT</version><name>ims</name><description>DemoprojectforSpringBoot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.3</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
Linux离线安装Apache-2.41.系统环境信息系统版本:Linux2.6.32-696.el6.x86_64操作系统:Centos6.92.前置准备Apache-2.4编译安装依赖apr、apr-util、pcre,所以安装前需要先下载好四个离线安装包,安装包下载地址:apr-1.5.2apr-util-1.5.4pcre-8.42httpd-2.4.343.编译安装aprcd/home/softwaretar-zxvfapr-1.5.2.tar.gzcdapr-1.5.2./configure--prefix=/usr/local/apr-1.5.2make&&makeinstall4.编译安装apr-utilcd/home/softwaretar-zxvfapr-util-1.5.4.tar.gzcdapr-util-1.5.4./configure--prefix=/usr/local/apr-util-1.5.4--with-apr=/usr/local/apr-1.5.2make&&makeinstall5.编译安装pcrecd/home/softwaretar-zxvfpcre-8.42.tar.gzcdpcre-8.42./configure--prefix=/usr/local/pcre-8.42make&&makeinstall6.编译安装httpdcd/home/softwaretar-zxvfhttpd-2.4.34.tar.gzcdhttpd-2.4.34./configure--prefix=/usr/local/httpd-2.4.34--with-apr=/usr/local/apr-1.5.2--with-apr-util=/usr/local/apr-util-1.5.4--with-pcre=/usr/local/pcre-8.42make&&makeinstall7.配置httpd.confapache编译安装完成后,配置文件地址在/usr/local/apache-2.4.34/conf/httpd.conf修改启动端口为8080Listen80818.启动Apache创建软链接:ln-s/usr/local/apache-2.4.34/bin/apachectl/usr/bin/apachectl启动:/usr/bin/apachectl
下载与ES对应的版本:官网地址解压修改$KIBANA_HOME/config/kibana.yml文件启动Kibana:sudo$KIBANA_HOME/bin/kibana浏览器输入host:5601能正常打开页面即可KAAE插件安装KAAE为Kibana的插件,主要用来监控和报警,用户可以根据需求配置相应的监控条件,达到某个条件会发出报警消息,同时KAAE也提供有报告Report功能,能够将查询到的结果生成图表发送到指定邮箱。安装:$KIBANA_HOME/bin/kibana-pulgininstallhttps://github.com/sirensolutions/sentinl/releases/download/tag-6.2.3-3/sentinl-v6.2.4.zip配置kibana.yml文件,在最后加上:重启Kibana后,浏览器输入:http://ip:5601出现以下界面说明插件安装成功可以在页面上按照需求配置监听报警
1.在使用mybatis的动态sql时,有时候遇到根据条件判断添加where后面的筛选条件的情况,会出现多余的AND或者OR:2.使用where关键字:2.1当第一个参数为空时,拼接后的sql为:select*fromtdwhereandphone=.......;2.2当所有的参数都为空时,拼接后的sql为:select*fromtdwhere.....,显然这样的sql不是完整的sql,执行时会报错.3.使用where标签时:3.1当第一个参数为空时,拼接后的sql为:select*fromtdwherephone=......(若语句的开头为AND或者OR时,where元素会将他们去除).3.2当所有的参数都为空时,拼接后的sql为:select*fromtd.(where元素只会在至少有一个子元素的条件返回SQL子句的情况下才去插入“WHERE”子句)。
ELK日志分析系统介绍1.系统概述本系统为业务日志分析监控系统,使用ELK+Beats实现对系统业务日志的收集、存储、分析,业务系统运行期间将相关日志输出到一个指定的文件夹/文件内,使用FileBeat组件实现对日志文件夹/文件的监听,可以直接将新增的数据发往设定好的Logstash中过滤,或直接发往ElasticSearch分类存储,当系统运行出现问题时,运维人员可以使用Kibana对存储在ES中的日志数据根据相关字段搜索查找,Kibana也支持对数据进行相应的可视化图表展现。2.系统实现描述2.1采集-filebeat对于日志数据收集使用Filebeat部署在业务服务器后台监听日志文件的方式。Filebeat运行环境没有任何依赖,后台运行占用内存资源极低,相比于Logstash可以忽略不计,不影响服务器正常的业务。Filebeat可以运行在MacOS、Windows、Linux等系统下。Filebeat监听指定的文件(可以使用通配符),一旦文件中有新的一行内容追加则会将这条数据发往配置好的output路径。Filebeat的output支持logstash、ElastciSearch、file、console等,一般的不需要复杂过滤的可以直接发往ES存储,多节点日志采集也可以经过Logstash汇总过滤后,再存储进ES。在Filebaet运行过程中,每个Prospector的状态信息都会保存在内存里。当Filebeat进行了重启后,会从注册表文件里恢复重启之前的状态信息,让FIlebeat继续从之前已知的位置开始进行数据读取。2.2解析-Logstash对于数据解析主要包括:(1)对汇总多节点后的日志进行区分(2)将不规则格式数据转换为规则数据(3)将不符合格式要求的数据过滤去除因为Filebeat只支持简单的数据解析,对于日志的解析过滤整体可以使用Logstash。Logstash内置许多解析格式:grok、date、ip、json...,支持对不规则的数据字符串进行规则化输出,也能够在数据传输过程中添加或删除某些指定字段。将采集到的日志数据经过logstash过滤转换后发往ES建立索引存储。因为logstash占用内存资源较大(默认1G),为不影响业务尽量不部署在业务服务器上。2.3存储-ElasticSearch日志数据存储使用ElasticSearch,由logstash将过滤完成后的规则化数据存入ES指定索引中。ES有自动发现功能,初期使用ElasticSearch的单节点集群模式,后续想要添加节点只需指定elasticsearch集群名称保持一致,就能自动加入集群,ES就会按照配置将索引分片到新加入的节点上。2.4展现-Kibana(1)ELK中Kibana专门为ES中的数据提供可视化展现的,支持搜索、汇总计算,图表展现等。(2)ElasticSearch也提供有RESTAPI,支持调用接口的方式访问操作索引数据。(3)使用插件进行数据异常监控报警功能实现,如系统日志出现异常报错则可配置发送邮件通知相关人员。(4)Kibana也支持对系统日志进行可视化监控展现,包括CPU、内存、硬盘等。3.系统可用性测试系统运行过程中,logstash宕机:Filebeat会记录发送不成功的数据,并尝试连接logstash,成功连接后会再次将数据发往logstash,下图为再次发送成功后的日志。系统运行过程中,Filebeat宕机:在Filebaet运行过程中,每个Prospector的状态信息都会保存在内存里。当Filebeat进行了宕机重启后,会从注册表文件里恢复重启之前的状态信息,让FIlebeat继续从之前已读取的位置开始往后进行数据读取。系统运行过程中,ES集群宕机::::hljs-centerLogstash日志::::::hljs-centerFilebeat日志:::logstash没有数据存储功能,ES集群宕机,logstash数据无法发送,Filebeat会记录未成功发送的数据,同时logstash定时尝试连接ES,直到连接成功,数据会再次发送。
1.建基础包/文件夹2.application.propertis编写application.propertis基础配置和数据库连接3.index.ftl注:在index.ftl页面可以输入感叹号!,然后按tab键一键生成HTML代码4.IndexController.java5.启动服务启动服务,后再浏览器输入http://localhost:8080,是否能成功访问:
1.说明本文主要介绍基于SpringSecurity的用户权限控制的简单实现。1.1环境版本SpringBoot:2.0.7SpringSecurity:5.0.10JDK:1.82.项目配置2.1引入maven包<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.7.RELEASE</version><relativePath/><!--lookupparentfromrepository--></parent><groupId>cn.coralcloud</groupId><artifactId>security</artifactId><version>0.0.1-SNAPSHOT</version><name>security</name><description>DemoSecurityprojectforSpringBoot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.1</version></dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId><version>2.9.9</version></dependency><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.5</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.60</version></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>22.0</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>2.2application.ymlserver.port:9091spring.application.name:spring-webspring.http.encoding.charset:utf8spring:session:store-type:redisredis:host:localhostport:6379password:123456datasource:driver-class-name:com.mysql.jdbc.Driverurl:jdbc:mysql://127.0.0.1:3306/security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername:rootpassword:123456servlet:multipart:max-file-size:1024MBmax-request-size:1024MBmybatis:mapper-locations:classpath*:mapper/*Mapper.xmltype-aliases-package:cn.coralcloud.security.model2.3数据库初始化脚本/*NavicatMySQLDataTransferSourceServer:localhostSourceServerVersion:50644SourceHost:localhost:3306SourceDatabase:securityTargetServerType:MYSQLTargetServerVersion:50644FileEncoding:65001Date:2019-12-0216:28:44*/SETFOREIGN_KEY_CHECKS=0;--------------------------------Tablestructureforrole------------------------------DROPTABLEIFEXISTS`role`;CREATETABLE`role`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`name`varchar(50)NOTNULL,`nickname`varchar(50)NOTNULL,`system`bit(1)NOTNULLDEFAULTb'0',`description`varchar(500)DEFAULTNULL,`permission`mediumtext,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=2DEFAULTCHARSET=utf8mb4;--------------------------------Tablestructureforuser------------------------------DROPTABLEIFEXISTS`user`;CREATETABLE`user`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`username`varchar(50)NOTNULL,`password`varchar(300)NOTNULL,`role`varchar(500)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=3DEFAULTCHARSET=utf8mb4;3.系统初始化文件3.1users.json[{"username":"admin","password":"ea48576f30be1669971699c09ad05c94","role":"ROLE_ADMINISTRATOR"}]3.2roles.json[{"name":"ROLE_ADMINISTRATOR","nickname":"管理员","description":"系统超级管理员,不允许用户更改","system":true,"permissions":[{"resourceId":"user","resourceName":"用户管理","privileges":{"list":"查看用户列表","add":"新增用户","update":"修改用户信息","delete":"删除用户"}},{"resourceId":"permission","resourceName":"权限","privileges":{"read":"查看权限","write":"新增权限","update":"更新权限","delete":"删除权限"}}]}]4.数据持久化DAO层4.1UserDao.javapackagecn.coralcloud.security.dao;importcn.coralcloud.security.model.User;importorg.apache.ibatis.annotations.Mapper;importorg.springframework.stereotype.Component;importjava.util.List;/***@authorgeff*@nameUserDao*@description*@date2019-11-2910:31*/@Component@MapperpublicinterfaceUserDao{/***根据用户名查找*@date2019/11/2915:24*@authorgeff*@paramusernameusername*@returncn.coralcloud.security.model.User*/UserfindByUsername(Stringusername);/***创建用户*@date2019/11/2915:24*@authorgeff*@paramuseruser*/voidsave(Useruser);/***获取用户列表*@date2019/11/2915:24*@authorgeff*@param*@returnjava.util.List<cn.coralcloud.security.model.User>*/List<User>list();}4.2RoleDao.javapackagecn.coralcloud.security.dao;importcn.coralcloud.security.model.Role;importorg.apache.ibatis.annotations.Mapper;importorg.springframework.stereotype.Component;/***@authorgeff*@nameRoleDao*@description*@date2019-11-2910:31*/@Component@MapperpublicinterfaceRoleDao{/***根据名称查找*@date2019/11/2915:23*@authorgeff*@paramnamename*@returncn.coralcloud.security.model.Role*/RolefindByName(Stringname);/***保存数据*@date2019/11/2915:23*@authorgeff*@paramrolerole*/voidsave(Rolerole);}4.3UserMapper.xml<?xmlversion="1.0"encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTDMapper3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="cn.coralcloud.security.dao.UserDao"><resultMapid="userMapper"type="cn.coralcloud.security.model.User"><idproperty="id"column="id"/></resultMap><selectid="findByUsername"parameterType="String"resultMap="userMapper">select*from`user`where`username`=#{username}</select><selectid="list"resultType="cn.coralcloud.security.model.User">select*from`user`</select><insertid="save"parameterType="cn.coralcloud.security.model.User"useGeneratedKeys="true"keyProperty="id">insertinto`user`(username,password,role)values(#{username},#{password},#{role})</insert></mapper>4.4RoleMapper.xml<?xmlversion="1.0"encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTDMapper3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="cn.coralcloud.security.dao.RoleDao"><resultMapid="roleMapper"type="cn.coralcloud.security.model.Role"><idproperty="id"column="id"/></resultMap><selectid="findByName"parameterType="String"resultMap="roleMapper">select*fromrolewhere`name`=#{name}</select><insertid="save"parameterType="cn.coralcloud.security.model.Role"useGeneratedKeys="true"keyProperty="id">insertintorole(name,nickname,description,system,permission)values(#{name},#{nickname},#{description},#{system},#{permission})</insert></mapper>5.处理用户权限认证逻辑5.1SpringSecurity配置要在项目中使用@PreAuthorize等注解实现方法级别权限控制,则需要在项目启动类上添加注解@EnableGlobalMethodSecurity(prePostEnabled=true),本文项目启动类:packagecn.coralcloud.security;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.boot.builder.SpringApplicationBuilder;importorg.springframework.boot.web.servlet.support.SpringBootServletInitializer;importorg.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;/***@authorgeff*/@SpringBootApplication@EnableGlobalMethodSecurity(prePostEnabled=true)publicclassSecurityApplicationextendsSpringBootServletInitializer{@OverrideprotectedSpringApplicationBuilderconfigure(SpringApplicationBuilderapplicationBuilder){returnapplicationBuilder.sources(SecurityApplication.class);}publicstaticvoidmain(String[]args){SpringApplication.run(SecurityApplication.class,args);}}5.2用户对象SpringSecurity自带的有UserDetails接口主要保存用户对象数据,所以我们的用户对象需要实现UserDetails接口packagecn.coralcloud.security.model;importcom.fasterxml.jackson.annotation.JsonIgnore;importlombok.Data;importlombok.NoArgsConstructor;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.UserDetails;importjava.io.Serializable;importjava.util.*;/***@authorgeff*/@Data@NoArgsConstructorpublicclassUserimplementsUserDetails,Serializable{privateLongid;/***用户登录名*/privateStringusername;/***用户登录密码,用户的密码不应该暴露给客户端*/@JsonIgnoreprivateStringpassword;/***用户在系统中的角色列表,将根据角色对用户操作权限进行限制*/privateStringrole;privateList<Role>roles;@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){Collection<GrantedAuthority>authorities=newArrayList<>();if(roles!=null){for(Rolerole:roles){if(role==null){continue;}for(Permissionpermission:role.getPermissions()){for(Stringprivilege:permission.getPrivileges().keySet()){authorities.add(newSimpleGrantedAuthority(String.format("%s-%s",permission.getResourceId(),privilege)));}}}}returnauthorities;}@OverridepublicbooleanisAccountNonExpired(){returntrue;}@OverridepublicbooleanisAccountNonLocked(){returntrue;}@OverridepublicbooleanisCredentialsNonExpired(){returntrue;}@OverridepublicbooleanisEnabled(){returntrue;}}在用户对象的publicCollection<?extendsGrantedAuthority>getAuthorities()方法中,需要根据用户当前角色生成当前用户权限列表,本文权限机制使用resourceId-privilege方式5.3角色对象Role.javapackagecn.coralcloud.security.model;importcom.alibaba.fastjson.JSON;importlombok.Data;importorg.springframework.util.StringUtils;importjava.io.Serializable;importjava.util.List;/***@authorgeff*@nameRole*@description*@date2019-11-2910:08*/@DatapublicclassRoleimplementsSerializable{privateLongid;/***角色名,用于权限校验*/privateStringname;/***角色中文名,用于显示*/privateStringnickname;/***角色描述信息*/privateStringdescription;/***是否为内置*/privateBooleansystem;/***角色可进行的操作列表*/privateList<Permission>permissions;privateStringpermission;/***SpringSecurity4.0以上版本角色都默认以'ROLE_'开头*@paramnamename*/publicvoidsetName(Stringname){if(!name.contains("ROLE_")){this.name="ROLE_"+name;}else{this.name=name;}}publicList<Permission>getPermissions(){if(permissions==null){if(!StringUtils.isEmpty(permission)){this.permissions=JSON.parseArray(permission,Permission.class);}}returnpermissions;}publicStringgetPermission(){if(StringUtils.isEmpty(permission)){this.permission=JSON.toJSONString(permissions);}returnpermission;}}SpringSecurity4.0以上所有的角色名称默认都要以ROLE_开头,所有本文在获取角色名称是会自动添加前缀。为了简单方便,本文角色对象的permission字段保存着该角色的所有权限列表的JSON字符串5.4权限对象Permission.javapackagecn.coralcloud.security.model;importlombok.Data;importjava.io.Serializable;importjava.util.Map;/***@authorgeff*@namePermission*@description*@date2019-11-2910:07*/@DatapublicclassPermissionimplementsSerializable{privateStringresourceId;privateStringresourceName;privateMap<String,String>privileges;privatebooleanabandon=false;}5.5统一数据返回对象因为是前后端分离项目,所以本文封装了统一数据返回对象Response类packagecn.coralcloud.security.model;importjava.io.Serializable;/***响应*@authorgeff*/publicclassResponse<T>implementsSerializable{privatefinalstaticintSUCCESS=0;privatefinalstaticintERROR=-1;privateintcode;privateTdata;privateStringmessage;publicintgetCode(){returncode;}publicvoidsetCode(intcode){this.code=code;}publicStringgetMessage(){returnmessage;}publicvoidsetMessage(Stringmessage){this.message=message;}publicTgetData(){returndata;}publicvoidsetData(Tdata){this.data=data;}publicstatic<T>Response<T>ok(){Response<T>response=newResponse<>();response.setCode(SUCCESS);returnresponse;}publicstatic<T>Response<T>ok(Tdata){Response<T>response=newResponse<>();response.setCode(SUCCESS);response.setData(data);returnresponse;}publicstatic<T>Response<T>fail(Stringmessage){Response<T>response=newResponse<>();response.setCode(ERROR);response.setMessage(message);returnresponse;}publicstatic<T>Response<T>fail(intcode,Stringmessage){Response<T>response=newResponse<>();response.setCode(code);response.setMessage(message);returnresponse;}}5.6UserDetailsService接口获取用户信息SpringSecurity同时在用户登录认证时会通过调用UserDetailsService的loadUserByUsername来获取当前登录的用户信息,当用户认证通过后会将用户对象保存到自定义的Token对象中。所以本文需要实现UserDetailsService接口,完成loadUserByUsername方法:packagecn.coralcloud.security.service;importcn.coralcloud.security.dao.RoleDao;importcn.coralcloud.security.model.Role;importcn.coralcloud.security.model.User;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.stereotype.Service;importorg.springframework.util.StringUtils;importjava.util.ArrayList;importjava.util.List;/***@authorgeff*/@Service("myUserDetailsService")publicclassUserDetailsServiceImplimplementsUserDetailsService{@AutowiredprivateUserServiceuserService;@AutowiredprivateRoleDaoroleDao;@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{Useruser=userService.findByUsername(username);if(user==null){thrownewUsernameNotFoundException(String.format("Nouserfoundwithusername:%s",username));}if(!StringUtils.isEmpty(user.getRole())){String[]roles=user.getRole().split(",");List<Role>roleList=newArrayList<>();for(StringroleName:roles){Rolerole=roleDao.findByName(roleName);roleList.add(role);}user.setRoles(roleList);}returnuser;}}5.7自定义方法权限处理器自定义方法权限处理器需要实现PermissionEvaluator接口,完成publicbooleanhasPermission(Authenticationauthentication,ObjecttargetDomainObject,Objectpermission)方法。packagecn.coralcloud.security.config;importcn.coralcloud.security.model.User;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.access.PermissionEvaluator;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.GrantedAuthority;importjava.io.Serializable;/***@authorgeff*/@ConfigurationpublicclassMyPermissionEvaluatorimplementsPermissionEvaluator{@OverridepublicbooleanhasPermission(Authenticationauthentication,ObjecttargetDomainObject,Objectpermission){booleanaccessable=false;if(authentication.getPrincipal()instanceofUser){Stringprivilege=targetDomainObject+"-"+permission;for(GrantedAuthorityauthority:authentication.getAuthorities()){if(privilege.equalsIgnoreCase(authority.getAuthority())){accessable=true;break;}}returnaccessable;}returnfalse;}@OverridepublicbooleanhasPermission(Authenticationauthentication,SerializabletargetId,StringtargetType,Objectpermission){//TODOAuto-generatedmethodstubreturnfalse;}}本文通过根据resourceId-privilege方式验证权限5.8系统初始化配置类本文系统初始化配置类主要实现在系统启动时根据配置的users.json和roles.json自动生成初始化用户和角色信息。packagecn.coralcloud.security.component;importcn.coralcloud.security.dao.RoleDao;importcn.coralcloud.security.dao.UserDao;importcn.coralcloud.security.model.Role;importcn.coralcloud.security.model.User;importcom.google.gson.GsonBuilder;importcom.google.gson.reflect.TypeToken;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importorg.springframework.beans.factory.annotation.Value;importjavax.annotation.PostConstruct;importjava.io.InputStream;importjava.io.InputStreamReader;importjava.lang.reflect.Type;importjava.nio.charset.StandardCharsets;importjava.util.ArrayList;/***系统初始化配置类,主要用于加载内置数据到目标数据库上*@authorgeff*/@ComponentpublicclassSystemInitializer{@Value("${initialzation.file.users:users.json}")privateStringuserFileName;@Value("${initialzation.file.roles:roles.json}")privateStringroleFileName;@AutowiredprivateUserDaouserDao;@AutowiredprivateRoleDaoroleDao;@AutowiredprivateGsonBuildergsonBuilder;@PostConstructpublicbooleaninitialize(){try{InputStreamuserInputStream=getClass().getClassLoader().getResourceAsStream(userFileName);if(userInputStream==null){thrownewException("initialzationuserfilenotfound:"+userFileName);}InputStreamroleInputStream=getClass().getClassLoader().getResourceAsStream(roleFileName);if(roleInputStream==null){thrownewException("initialzationrolefilenotfound:"+roleFileName);}//导入初始的系统超级管理员角色TyperoleTokenType=newTypeToken<ArrayList<Role>>(){}.getType();ArrayList<Role>roles=gsonBuilder.create().fromJson(newInputStreamReader(roleInputStream,StandardCharsets.UTF_8),roleTokenType);for(Rolerole:roles){if(roleDao.findByName(role.getName())==null){roleDao.save(role);}}//导入初始的系统管理员用户TypeteacherTokenType=newTypeToken<ArrayList<User>>(){}.getType();ArrayList<User>users=gsonBuilder.create().fromJson(newInputStreamReader(userInputStream,StandardCharsets.UTF_8),teacherTokenType);for(Useruser:users){if(userDao.findByUsername(user.getUsername())==null){userDao.save(user);}}}catch(Exceptione){e.printStackTrace();}returntrue;}}6.用户认证相关自定义实现6.1.自定义用户认证过滤器用户认证过滤器拦截用户发送的认证请求,然后从请求中获取用户账号和密码等认证信息并封装成一个未认证的AthenticationToken对象,然后调用AuthenticationManager对AthenticationToken进行认证。自定义用户认证过滤器需要继承AbstractAuthenticationProcessingFilter,然后重写attemptAuthentication方法,在方法内部根据请求参数封装成未认证的AthenticationToken对象packagecn.coralcloud.security.component;importorg.springframework.security.authentication.AuthenticationServiceException;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;importorg.springframework.security.web.util.matcher.AntPathRequestMatcher;importorg.springframework.util.StringUtils;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;/***自定义的用户名密码认证过滤器*@authorgeff*/publicclassAuthFilterextendsAbstractAuthenticationProcessingFilter{privatestaticStringhttpMethod="POST";publicAuthFilter(){/**设置该过滤器对POST请求/api/user/login进行拦截*/super(newAntPathRequestMatcher("/api/user/login",httpMethod));}@OverridepublicAuthenticationattemptAuthentication(HttpServletRequestrequest,HttpServletResponseresponse)throwsAuthenticationException{if(!request.getMethod().equals(httpMethod)){thrownewAuthenticationServiceException("Authenticationmethodnotsupported:"+request.getMethod());}else{/**从http请求中获取用户输入的用户名和密码信息*这里接收的是form形式的参数,如果要接收json形式的参数,修改这里即可*/Stringusername=this.obtainUsername(request);Stringpassword=this.obtainPassword(request);if(StringUtils.isEmpty(username)&&StringUtils.isEmpty(password)){thrownewUsernameNotFoundException("用户名或密码错误");}/**使用用户输入的用户名和密码信息创建一个未认证的用户认证Token*/AuthTokenauthRequest=newAuthToken(username,password);/**设置一些详情信息*/this.setDetails(request,authRequest);/**通过AuthenticationManager调用相应的AuthenticationProvider进行用户认证*/returnthis.getAuthenticationManager().authenticate(authRequest);}}privateStringobtainUsername(HttpServletRequestrequest){StringusernameParameter="username";returnrequest.getParameter(usernameParameter);}privateStringobtainPassword(HttpServletRequestrequest){StringpasswordParameter="password";returnrequest.getParameter(passwordParameter);}privatevoidsetDetails(HttpServletRequestrequest,AuthTokenauthRequest){authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));}}自定义用户认证拦截器要在构造方法中指定拦截的认证请求(本文中是POST类型的/api/user/login请求,可根据需求设置),并在attemptAuthentication()方法中实现获取用户认证信息、封装AuthenticationToken对象、调用AuthenticationManager对AuthenticationToken进行认证等逻辑。6.2自定义用户认证处理器用户认证处理器主要是对用户提交的认证信息进行认证,SpringSecurity默认实现的认证处理器的认证处理逻辑并不一定符合所有的业务需求(例如,默认的认证处理无法处理验证码),因此,可以自定义用户认证处理器。自定义的用户认证处理器,需要实现AuthenticationProvider接口,主要是实现publicAuthenticationauthenticate(Authenticationauthentication)方法和publicbooleansupports(Class<?>authentication)方法,前者主要是实现具体的认证逻辑,后者主要是指定认证处理器能对哪种AuthenticationToken对象进行认证。packagecn.coralcloud.security.component;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.authentication.AuthenticationProvider;importorg.springframework.security.authentication.BadCredentialsException;importorg.springframework.security.authentication.dao.DaoAuthenticationProvider;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;importorg.springframework.stereotype.Component;/***@authorgeff*@nameAuthProvider*@description*登录认证的Provider,自定义实现了{@linkAuthenticationProvider}<br>*Provider默认实现是{@linkDaoAuthenticationProvider}<br>*{@linkUsernamePasswordAuthenticationFilter}调用=>{@linkAuthenticationManager}=>{@linkAuthenticationProvider}验证<br>**@date2019-11-2915:52*/@Slf4j@ComponentpublicclassAuthProviderimplementsAuthenticationProvider{privatefinalUserDetailsServiceuserDetailService;privatefinalPasswordEncoderpasswordEncoder;@AutowiredpublicAuthProvider(@Qualifier("myUserDetailsService")UserDetailsServiceuserDetailService,PasswordEncoderpasswordEncoder){this.userDetailService=userDetailService;this.passwordEncoder=passwordEncoder;}@OverridepublicAuthenticationauthenticate(Authenticationauthentication)throwsAuthenticationException{/**将未认证的Authentication转换成自定义的用户认证Token*/AuthTokenauthenticationToken=(AuthToken)authentication;/**根据用户Token中的用户名查找用户信息,如果有该用户信息,则验证用户密码是否正确*/UserDetailsuser=userDetailService.loadUserByUsername((String)(authenticationToken.getPrincipal()));if(user==null){thrownewBadCredentialsException("用户名或密码不正确");}elseif(!this.passwordEncoder.matches((CharSequence)authenticationToken.getCredentials(),user.getPassword())){thrownewBadCredentialsException("用户名或密码不正确");}/**认证成功则创建一个已认证的用户认证Token*/AuthTokenauthenticationResult=newAuthToken(user,user.getPassword(),user.getAuthorities());/**设置一些详情信息*/authenticationResult.setDetails(authenticationToken.getDetails());returnauthenticationResult;}/***是否支持处理当前Authentication对象类似*/@Overridepublicbooleansupports(Class<?>authentication){returntrue;}}6.3自定义用户认证对象用户认证对象是在用户认证拦截器中创建的,在用户认证处理器中使用的。用户认证对象(AuthenticationToken)中封装的是用户认证信息,例如UsernamePasswordAuthenticationToken中封装的是用户名和密码。实际业务中,可能需要根据不同的用户信息进行认证(例如,手机号和验证码),此时就需要自定义用户认证对象。自定义的用户认证对象,需要继承AbstractAuthenticationToken类,并设定根据认证时使用的是哪些信息。packagecn.coralcloud.security.component;importorg.springframework.security.authentication.AbstractAuthenticationToken;importorg.springframework.security.core.GrantedAuthority;importjava.util.Collection;/***自定义的用户名密码认证对象*@authorgeff*/publicclassAuthTokenextendsAbstractAuthenticationToken{/***用户名*/privatefinalObjectprincipal;/***密码*/privateObjectcredentials;/***创建未认证的用户名密码认证对象*/publicAuthToken(Objectprincipal,Objectcredentials){super(null);this.principal=principal;this.credentials=credentials;this.setAuthenticated(false);}/***创建已认证的用户密码认证对象*/publicAuthToken(Objectprincipal,Objectcredentials,Collection<?extendsGrantedAuthority>authorities){super(authorities);this.principal=principal;this.credentials=credentials;super.setAuthenticated(true);}@OverridepublicObjectgetCredentials(){returnthis.credentials;}@OverridepublicObjectgetPrincipal(){returnthis.principal;}@OverridepublicvoidsetAuthenticated(booleanisAuthenticated)throwsIllegalArgumentException{if(isAuthenticated){thrownewIllegalArgumentException("Cannotsetthistokentotrusted-useconstructorwhichtakesaGrantedAuthoritylistinstead");}else{super.setAuthenticated(false);}}@OverridepublicvoideraseCredentials(){super.eraseCredentials();this.credentials=null;}}6.4自定义用户认证成功处理器用户认证成功处理器在用户认证成功之后调用,主要是执行一些额外的操作(例如,操作Cookie、页面跳转等)。自定义的用户认证成功处理器可以通过实现AuthenticationSuccessHandler接口,或者通过继承AbstractAuthenticationTargetUrlRequestHandler类及其子类来实现。本文自定义的用户认证成功处理器是通过继承AbstractAuthenticationTargetUrlRequestHandler的子类SavedRequestAwareAuthenticationSuccessHandler来实现的。packagecn.coralcloud.security.component;importcn.coralcloud.security.model.User;importcom.fasterxml.jackson.databind.ObjectMapper;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.core.Authentication;importorg.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;importorg.springframework.stereotype.Component;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;importjava.io.IOException;/***自定义的用户认证成功处理器*@authorgeff*/@Component@Slf4jpublicclassAuthSuccessHandlerextendsSavedRequestAwareAuthenticationSuccessHandler{@AutowiredprivateObjectMapperobjectMapper;publicAuthSuccessHandler(){}@OverridepublicvoidonAuthenticationSuccess(HttpServletRequestrequest,HttpServletResponseresponse,Authenticationauthentication)throwsIOException{//认证成功返回jsonUseruser=(User)authentication.getPrincipal();//写入session?HttpSessionsession=request.getSession();session.setAttribute("User",user);StringjsonStr=objectMapper.writeValueAsString(user);log.info("认证成功:{}",jsonStr);response.setContentType("application/json;charset=UTF-8");response.getWriter().write(jsonStr);}}6.5自定义用户认证失败处理器用户认证失败处理器是在用户认证失败之后调用,主要是执行一些额外的操作(例如操作Cookie、页面跳转、返回错误信息等)。自定义的用户认证失败处理器可以通过实现AuthenticationFailureHandler接口,或者通过继承AuthenticationFailureHandler接口的其它实现类来实现。本文自定义的用户认证失败处理器是通过继承AuthenticationFailureHandler接口的实现类SimpleUrlAuthenticationFailureHandler来实现的。packagecn.coralcloud.security.component;importcn.coralcloud.security.model.Response;importcom.fasterxml.jackson.databind.ObjectMapper;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.HttpStatus;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;importorg.springframework.stereotype.Component;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;/***自定义的用户认证失败处理器*@authorgeff*/@Component@Slf4jpublicclassAuthFailureHandlerextendsSimpleUrlAuthenticationFailureHandler{@AutowiredprivateObjectMapperobjectMapper;@OverridepublicvoidonAuthenticationFailure(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionexception)throwsIOException{/**返回JSON*/log.error("认证失败:{}",exception.getMessage());Responseres=Response.fail(-1,exception.getMessage());response.setStatus(HttpStatus.OK.value());response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(res));response.getWriter().flush();}}6.6自定义访问拒绝处理器自定义访问拒绝处理器用来解决认证过的用户访问无权限资源时的异常。前后端分离的情况下可以通过自定义访问拒绝处理器实现JSON格式的数据返回,自定义访问拒绝处理器通过实现AccessDeniedHandler接口,然后实现publicvoidhandle(HttpServletRequestrequest,HttpServletResponseresponse,AccessDeniedExceptione)方法,在方法内部处理返回数据。packagecn.coralcloud.security.component;importcn.coralcloud.security.model.Response;importcom.fasterxml.jackson.databind.ObjectMapper;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.HttpStatus;importorg.springframework.security.access.AccessDeniedException;importorg.springframework.security.web.access.AccessDeniedHandler;importorg.springframework.stereotype.Component;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;/***@authorgeff*@nameAuthAccessDeniedHandler*@description*@date2019-11-2916:57*/@Slf4j@ComponentpublicclassAuthAccessDeniedHandlerimplementsAccessDeniedHandler{@AutowiredprivateObjectMapperobjectMapper;@Overridepublicvoidhandle(HttpServletRequestrequest,HttpServletResponseresponse,AccessDeniedExceptione)throwsIOException{log.error("无权访问:{}",e.getMessage());Responseres=Response.fail(401,"无权访问");response.setStatus(HttpStatus.UNAUTHORIZED.value());response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(res));response.getWriter().flush();}}6.7自定义加密类本文密码使用Md5(password,salt)的形式,所以需要自定义SpringSecurity加密类,然后再Config配置类注入自定义加密类需要实现PasswordEncoder,完成encode和matches方法packagecn.coralcloud.security.component;importcn.coralcloud.security.utils.SecretUtils;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.util.StringUtils;importjava.util.Objects;/***@authorgeff*@nameMd5SaltPasswordEncoder*@description*@date2019-12-0209:27*/publicclassMd5SaltPasswordEncoderimplementsPasswordEncoder{@OverridepublicStringencode(CharSequencecharSequence){Stringstring=charSequence.toString();String[]array=string.split(",");Stringsalt="";if(array.length>1){salt=array[1];}returnSecretUtils.md5(array[0],salt);}@Overridepublicbooleanmatches(CharSequencecharSequence,Strings){if(!StringUtils.isEmpty(s)){StringencodePassword=encode(charSequence);returnObjects.equals(encodePassword,s);}returnfalse;}}7.SpringSecurity相关配置类7.1自定义的用户名密码认证配置类packagecn.coralcloud.security.component;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.config.annotation.SecurityConfigurerAdapter;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.web.DefaultSecurityFilterChain;importorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;importorg.springframework.stereotype.Component;/***自定义的用户名密码认证配置类*@authorgeff*/@ComponentpublicclassAuthConfigextendsSecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity>{@AutowiredAuthProviderprovider;@AutowiredAuthSuccessHandlerauthSuccessHandler;@AutowiredAuthFailureHandlerauthFailureHandler;@Overridepublicvoidconfigure(HttpSecurityhttp){AuthFilterauthFilter=newAuthFilter();/**自定义用户认证处理逻辑时,需要指定AuthenticationManager,否则无法认证*/authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));/**指定自定义的认证成功和失败的处理器*/authFilter.setAuthenticationSuccessHandler(authSuccessHandler);authFilter.setAuthenticationFailureHandler(authFailureHandler);/**把自定义的用户名密码认证过滤器和处理器添加到UsernamePasswordAuthenticationFilter过滤器之前*/http.authenticationProvider(provider).addFilterBefore(authFilter,UsernamePasswordAuthenticationFilter.class);}}7.2SpringSecurity核心配置类packagecn.coralcloud.security.config;importcn.coralcloud.security.component.AuthAccessDeniedHandler;importcn.coralcloud.security.component.AuthConfig;importcn.coralcloud.security.component.Md5SaltPasswordEncoder;importcn.coralcloud.security.model.Response;importcn.coralcloud.security.service.UserDetailsServiceImpl;importcom.fasterxml.jackson.databind.ObjectMapper;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.crypto.password.PasswordEncoder;importjava.io.PrintWriter;/***@authorgeff*/@Configuration@EnableWebSecurity@Slf4jpublicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateAuthConfigauthConfig;@AutowiredprivateAuthAccessDeniedHandleraccessDeniedHandler;@Overrideprotectedvoidconfigure(HttpSecurityhttpSecurity)throwsException{httpSecurity.apply(authConfig).and().authorizeRequests().antMatchers("/static/**","/api/user/login").permitAll().anyRequest().authenticated().and().csrf().disable();httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint((request,response,e)->{response.setContentType("application/json;charset=utf-8");PrintWriterout=response.getWriter();Responseres=Response.fail(-14,"会话超时,请重新登录!");out.write(newObjectMapper().writeValueAsString(res));out.flush();out.close();});}@AutowiredpublicvoidconfigureGlobal(AuthenticationManagerBuilderauth)throwsException{auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());}@BeanpublicPasswordEncoderpasswordEncoder(){returnnewMd5SaltPasswordEncoder();}@Bean@OverridepublicUserDetailsServiceuserDetailsService(){returnnewUserDetailsServiceImpl();}}本文通过自定义AuthenticationEntryPoint来解决匿名用户访问无权限资源时的异常
1.验证码工具类1.新建包utils2.新建VerifyCodeUtil类packagecn.coralcloud.ims.utils;importjavax.imageio.ImageIO;importjava.awt.*;importjava.awt.geom.AffineTransform;importjava.awt.image.BufferedImage;importjava.io.File;importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.OutputStream;importjava.util.Arrays;importjava.util.Random;/***@authorc-geff*/publicclassVerifyCodeUtil{/***使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符*/privatestaticfinalStringVERIFY_CODES="23456789ABCDEFGHJKLMNPQRSTUVWXYZ";privatestaticRandomrandom=newRandom();/***使用系统默认字符源生成验证码*@paramverifySize验证码长度*@return*/publicstaticStringgenerateVerifyCode(intverifySize){returngenerateVerifyCode(verifySize,VERIFY_CODES);}/***使用指定源生成验证码*@paramverifySize验证码长度*@paramsources验证码字符源*@return*/publicstaticStringgenerateVerifyCode(intverifySize,Stringsources){if(sources==null||sources.length()==0){sources=VERIFY_CODES;}intcodesLen=sources.length();Randomrand=newRandom(System.currentTimeMillis());StringBuilderverifyCode=newStringBuilder(verifySize);for(inti=0;i<verifySize;i++){verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));}returnverifyCode.toString();}/***生成随机验证码文件,并返回验证码值*@paramw*@paramh*@paramoutputFile*@paramverifySize*@return*@throwsIOException*/publicstaticStringoutputVerifyImage(intw,inth,FileoutputFile,intverifySize)throwsIOException{StringverifyCode=generateVerifyCode(verifySize);outputImage(w,h,outputFile,verifyCode);returnverifyCode;}/***输出随机验证码图片流,并返回验证码值*@paramw*@paramh*@paramos*@paramverifySize*@return*@throwsIOException*/publicstaticStringoutputVerifyImage(intw,inth,OutputStreamos,intverifySize)throwsIOException{StringverifyCode=generateVerifyCode(verifySize);outputImage(w,h,os,verifyCode);returnverifyCode;}/***生成指定验证码图像文件*@paramw*@paramh*@paramoutputFile*@paramcode*@throwsIOException*/publicstaticvoidoutputImage(intw,inth,FileoutputFile,Stringcode)throwsIOException{if(outputFile==null){return;}Filedir=outputFile.getParentFile();if(!dir.exists()){dir.mkdirs();}try{outputFile.createNewFile();FileOutputStreamfos=newFileOutputStream(outputFile);outputImage(w,h,fos,code);fos.close();}catch(IOExceptione){throwe;}}/***输出指定验证码图片流*@paramw*@paramh*@paramos*@paramcode*@throwsIOException*/publicstaticvoidoutputImage(intw,inth,OutputStreamos,Stringcode)throwsIOException{intverifySize=code.length();BufferedImageimage=newBufferedImage(w,h,BufferedImage.TYPE_INT_RGB);Randomrand=newRandom();Graphics2Dg2=image.createGraphics();g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);Color[]colors=newColor[5];Color[]colorSpaces=newColor[]{Color.WHITE,Color.CYAN,Color.GRAY,Color.LIGHT_GRAY,Color.MAGENTA,Color.ORANGE,Color.PINK,Color.YELLOW};float[]fractions=newfloat[colors.length];for(inti=0;i<colors.length;i++){colors[i]=colorSpaces[rand.nextInt(colorSpaces.length)];fractions[i]=rand.nextFloat();}Arrays.sort(fractions);//设置边框色g2.setColor(Color.GRAY);g2.fillRect(0,0,w,h);Colorc=getRandColor(200,250);//设置背景色g2.setColor(c);g2.fillRect(0,2,w,h-4);//绘制干扰线Randomrandom=newRandom();//设置线条的颜色g2.setColor(getRandColor(160,200));intloop=20;for(inti=0;i<loop;i++){intx=random.nextInt(w-1);inty=random.nextInt(h-1);intxl=random.nextInt(6)+1;intyl=random.nextInt(12)+1;g2.drawLine(x,y,x+xl+40,y+yl+20);}//添加噪点//噪声率floatyawpRate=0.05f;intarea=(int)(yawpRate*w*h);for(inti=0;i<area;i++){intx=random.nextInt(w);inty=random.nextInt(h);intrgb=getRandomIntColor();image.setRGB(x,y,rgb);}//使图片扭曲shear(g2,w,h,c);g2.setColor(getRandColor(100,160));intfontSize=h-4;Fontfont=newFont("Algerian",Font.ITALIC,fontSize);g2.setFont(font);char[]chars=code.toCharArray();for(inti=0;i<verifySize;i++){AffineTransformaffine=newAffineTransform();affine.setToRotation(Math.PI/4*rand.nextDouble()*(rand.nextBoolean()?1:-1),(w/verifySize)*i+fontSize/2,h/2);g2.setTransform(affine);g2.drawChars(chars,i,1,((w-10)/verifySize)*i+5,h/2+fontSize/2-10);}g2.dispose();ImageIO.write(image,"jpg",os);}privatestaticfinalIntegerCOLOR_MAX_INT=255;privatestaticColorgetRandColor(intfc,intbc){if(fc>COLOR_MAX_INT){fc=COLOR_MAX_INT;}if(bc>COLOR_MAX_INT){bc=COLOR_MAX_INT;}intr=fc+random.nextInt(bc-fc);intg=fc+random.nextInt(bc-fc);intb=fc+random.nextInt(bc-fc);returnnewColor(r,g,b);}privatestaticintgetRandomIntColor(){int[]rgb=getRandomRgb();intcolor=0;for(intc:rgb){color=color<<8;color=color|c;}returncolor;}privatestaticint[]getRandomRgb(){int[]rgb=newint[3];intloop=3;for(inti=0;i<loop;i++){rgb[i]=random.nextInt(255);}returnrgb;}privatestaticvoidshear(Graphicsg,intw1,inth1,Colorcolor){shearx(g,w1,h1,color);sheary(g,w1,h1,color);}privatestaticvoidshearx(Graphicsg,intw1,inth1,Colorcolor){intperiod=random.nextInt(2);booleanborderGap=true;intframes=1;intphase=random.nextInt(2);for(inti=0;i<h1;i++){doubled=(double)(period>>1)*Math.sin((double)i/(double)period+(6.2831853071795862D*(double)phase)/(double)frames);g.copyArea(0,i,w1,1,(int)d,0);if(borderGap){g.setColor(color);g.drawLine((int)d,i,0,i);g.drawLine((int)d+w1,i,w1,i);}}}privatestaticvoidsheary(Graphicsg,intw1,inth1,Colorcolor){intperiod=random.nextInt(40)+10;booleanborderGap=true;intframes=20;intphase=7;for(inti=0;i<w1;i++){doubled=(double)(period>>1)*Math.sin((double)i/(double)period+(6.2831853071795862D*(double)phase)/(double)frames);g.copyArea(i,0,1,h1,0,(int)d);if(borderGap){g.setColor(color);g.drawLine(i,(int)d,i,0);g.drawLine(i,(int)d+h1,i,h1);}}}}2.UserController新增验证码接口2.1新增captcha方法@GetMapping("/captcha")publicvoidcaptcha(HttpServletResponseresponse,HttpSessionsession){Stringcode=VerifyCodeUtil.generateVerifyCode(4);session.setAttribute(SessionKey.ADMIN_CAPTCHA_KEY,code);try{VerifyCodeUtil.outputImage(150,50,response.getOutputStream(),code);}catch(IOExceptione){e.printStackTrace();}}2.2修改login方法,增加验证下方为最终的登录验证方法:@PostMapping("/login")publicModelAndViewlogin(Stringemail,Stringpassword,Stringcaptcha,HttpSessionsession){StringsessionCaptcha=(String)session.getAttribute(SessionKey.ADMIN_CAPTCHA_KEY);ModelAndViewview=newModelAndView();view.setViewName("user/login");//将email和password写回到页面,使得登录失败时输入的账号密码不会丢失view.addObject("email",email);view.addObject("password",password);if(StringUtils.isEmpty(captcha)||!Objects.equals(captcha,sessionCaptcha)){view.addObject("errmsg","验证码错误!");returnview;}Useruser=userService.login(email,password);if(user!=null){session.setAttribute(SessionKey.ADMIN_USER_KEY,user);view.setViewName("redirect:/index");returnview;}view.addObject("errmsg","用户名或密码错误!");returnview;}注:此处将Session的Key值通过一个常量类SessionKey保存2.3SessionKey类在utils包下新建SessionKey类packagecn.coralcloud.ims.utils;/***@authorc-geff*@nameSessionKey*@description*@date2020-11-0410:12*/publicclassSessionKey{publicstaticfinalStringADMIN_USER_KEY="AdminUserKey";publicstaticfinalStringADMIN_CAPTCHA_KEY="AdminLoginCaptchaCode";}3.修改login.ftl3.1引入jquery下载jqeury文件jquery.min.js在/static/js文件夹下新建jquery文件夹,将jquery.min.js复制到文件夹下修改login.ftl文件,最终login.ftl内容为下:<!doctypehtml><htmllang="en"xmlns:th="http://www.thymeleaf.org"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge"><linktype="text/css"rel="stylesheet"href="/static/css/main.css"><linktype="text/css"rel="stylesheet"href="/static/css/login.css"><scriptsrc="/static/js/jquery/jquery.min.js"type="application/javascript"></script><title>用户登录</title></head><body><divclass="login-container"><formaction="/user/login"method="post"><div><h3>用户登录</h3></div><divclass="ims-form-item"><labelclass="ims-form-label"for="email">登录邮箱</label><inputclass="ims-form-input"id="email"autocomplete="off"th:value="${email}"placeholder="请输入邮箱"type="text"name="email"></div><divclass="ims-form-item"><labelclass="ims-form-label"for="password">登录密码</label><inputclass="ims-form-input"id="password"autocomplete="new-password"th:value="${password}"placeholder="请输入密码"type="password"name="password"></div><divclass="ims-form-item"><labelclass="ims-form-label"for="captcha">图片验证</label><inputclass="ims-form-input"id="captcha"type="text"placeholder="请输入图片验证码"name="captcha"><imgsrc="/user/captcha"class="captcha"alt="图片验证码"></div><pclass="errormsg"th:if="${errmsg}!=null"th:text="${errmsg}"></p><div><buttontype="submit"class="ims-button">登录</button></div></form></div></body><scripttype="application/javascript">$(document).ready(function(){$('img.captcha').click(function(){$(this).attr("src","/user/captcha?_="+newDate().getTime())})})</script></html>注,login.ftl变更如下:1.head新增引入jquery.min.js2.input新增读取前面保存的email和password3.新增图片验证码输入框及图片4.新建图片点击监听事件,更换图片验证码4.修改样式login.css#captcha{width:100px;border-top-right-radius:0;border-bottom-right-radius:0;}.captcha{width:1px;flex:1;height:42px;cursor:pointer;border-top-right-radius:4px;border-bottom-right-radius:4px;}5.最终效果
一、Sharding-jdbc简介Sharding-jdbc是开源的数据库操作中间件;定位为轻量级Java框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。官方文档地址:https://shardingsphere.apache.org/document/current/cn/overview/本文demo实现了分库分表功能。作者能力有限,如有错误,欢迎各位在评论中指出。不胜感激!二、项目结构首先创建一个一般的Springboot项目,项目采用三层架构,结构图如下:项目目录结构图POM.xml文件如下:<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.6.RELEASE</version><relativePath/><!--lookupparentfromrepository--></parent><groupId>com.macky</groupId><artifactId>spring-boot-shardingjdbc</artifactId><version>0.0.1-SNAPSHOT</version><name>spring-boot-shardingjdbc</name><description>Demoprojectforspring-boot-shardingjdbc</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--Mybatis-Plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.1.1</version></dependency><!--shardingspherestart--><!--forspringboot--><dependency><groupId>io.shardingsphere</groupId><artifactId>sharding-jdbc-spring-boot-starter</artifactId><version>3.1.0</version></dependency><!--forspringnamespace--><dependency><groupId>io.shardingsphere</groupId><artifactId>sharding-jdbc-spring-namespace</artifactId><version>3.1.0</version></dependency><!--shardingsphereend--><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>实体类以书本为例packagecom.macky.springbootshardingjdbc.entity;importcom.baomidou.mybatisplus.annotation.TableName;importcom.baomidou.mybatisplus.extension.activerecord.Model;importgroovy.transform.EqualsAndHashCode;importlombok.Data;importlombok.experimental.Accessors;/***@authorMacky*@TitleclassBook*@Description:书籍是实体类*@date2019/7/1315:23*/@Data@EqualsAndHashCode(callSuper=true)@Accessors(chain=true)@TableName("book")publicclassBookextendsModel<Book>{privateintid;privateStringname;privateintcount;}开放保存和查询两个接口,代码如下:packagecom.macky.springbootshardingjdbc.controller;importcom.macky.springbootshardingjdbc.entity.Book;importcom.macky.springbootshardingjdbc.service.BookService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.*;importjava.util.List;/***@authorMacky*@TitleclassBookController*@Description:TODO*@date2019/7/1220:53*/@RestControllerpublicclassBookController{@AutowiredBookServicebookService;@RequestMapping(value="/book",method=RequestMethod.GET)publicList<Book>getItems(){returnbookService.getBookList();}@RequestMapping(value="/book",method=RequestMethod.POST)publicBooleansaveItem(Bookbook){returnbookService.save(book);}}BookServiceImpl.javapackagecom.macky.springbootshardingjdbc.service.impl;importcom.baomidou.mybatisplus.core.toolkit.Wrappers;importcom.baomidou.mybatisplus.extension.service.impl.ServiceImpl;importcom.macky.springbootshardingjdbc.entity.Book;importcom.macky.springbootshardingjdbc.mapper.BookMapper;importcom.macky.springbootshardingjdbc.service.BookService;importorg.springframework.stereotype.Service;importjava.util.List;/***@authorMacky*@TitleclassBookServiceImpl*@Description:TODO*@date2019/7/1220:47*/@ServicepublicclassBookServiceImplextendsServiceImpl<BookMapper,Book>implementsBookService{@OverridepublicList<Book>getBookList(){returnbaseMapper.selectList(Wrappers.<Book>lambdaQuery());}@Overridepublicbooleansave(Bookbook){returnsuper.save(book);}}BookMapper.javapackagecom.macky.springbootshardingjdbc.mapper;importcom.baomidou.mybatisplus.core.mapper.BaseMapper;importcom.macky.springbootshardingjdbc.entity.Book;/***@authorMacky*@TitleclassBookMapper*@Description:TODO*@date2019/7/1220:46*/publicinterfaceBookMapperextendsBaseMapper<Book>{}创建数据库表,DDL语句如下:#创建数据库表数据CREATEDATABASEIFNOTEXISTS`db0`;USE`db0`;DROPTABLEIFEXISTS`book_0`;CREATETABLE`book_0`(`id`INT(11)NOTNULL,`name`VARCHAR(255)DEFAULTNULL,`count`INT(11)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=INNODBDEFAULTCHARSET=utf8mb4;DROPTABLEIFEXISTS`book_1`;CREATETABLE`book_1`(`id`INT(11)NOTNULL,`name`VARCHAR(255)DEFAULTNULL,`count`INT(11)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=INNODBDEFAULTCHARSET=utf8mb4;CREATEDATABASEIFNOTEXISTS`db1`;USE`db1`;DROPTABLEIFEXISTS`book_0`;CREATETABLE`book_0`(`id`INT(11)NOTNULL,`name`VARCHAR(255)DEFAULTNULL,`count`INT(11)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=INNODBDEFAULTCHARSET=utf8mb4;DROPTABLEIFEXISTS`book_1`;CREATETABLE`book_1`(`id`INT(11)NOTNULL,`name`VARCHAR(255)DEFAULTNULL,`count`INT(11)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=INNODBDEFAULTCHARSET=utf8mb4;CREATEDATABASEIFNOTEXISTS`db2`;USE`db2`;DROPTABLEIFEXISTS`book_0`;CREATETABLE`book_0`(`id`INT(11)NOTNULL,`name`VARCHAR(255)DEFAULTNULL,`count`INT(11)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=INNODBDEFAULTCHARSET=utf8mb4;DROPTABLEIFEXISTS`book_1`;CREATETABLE`book_1`(`id`INT(11)NOTNULL,`name`VARCHAR(255)DEFAULTNULL,`count`INT(11)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=INNODBDEFAULTCHARSET=utf8mb4;配置分库分表策略application.properties:#数据源db0,db1,db2sharding.jdbc.datasource.names=db0,db1,db2#第一个数据库sharding.jdbc.datasource.db0.type=com.zaxxer.hikari.HikariDataSourcesharding.jdbc.datasource.db0.driver-class-name=com.mysql.cj.jdbc.Driversharding.jdbc.datasource.db0.jdbc-url=jdbc:mysql://localhost:3306/db0?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTCsharding.jdbc.datasource.db0.username=rootsharding.jdbc.datasource.db0.password=Aa123456#第二个数据库sharding.jdbc.datasource.db1.type=com.zaxxer.hikari.HikariDataSourcesharding.jdbc.datasource.db1.driver-class-name=com.mysql.cj.jdbc.Driversharding.jdbc.datasource.db1.jdbc-url=jdbc:mysql://localhost:3306/db1?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTCsharding.jdbc.datasource.db1.username=rootsharding.jdbc.datasource.db1.password=Aa123456#第三个数据库sharding.jdbc.datasource.db2.type=com.zaxxer.hikari.HikariDataSourcesharding.jdbc.datasource.db2.driver-class-name=com.mysql.cj.jdbc.Driversharding.jdbc.datasource.db2.jdbc-url=jdbc:mysql://localhost:3306/db2?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTCsharding.jdbc.datasource.db2.username=rootsharding.jdbc.datasource.db2.password=Aa123456#水平拆分的数据库(表)配置分库+分表策略行表达式分片策略#分库策略sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=idsharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=db$->{id%3}#分表策略其中book为逻辑表分表主要取决于id行sharding.jdbc.config.sharding.tables.book.actual-data-nodes=db$->{0..2}.book_$->{0..2}sharding.jdbc.config.sharding.tables.book.table-strategy.inline.sharding-column=count#分片算法表达式sharding.jdbc.config.sharding.tables.book.table-strategy.inline.algorithm-expression=book_$->{count%3}#主键UUID18位数如果是分布式还要进行一个设置防止主键重复#sharding.jdbc.config.sharding.tables.user.key-generator-column-name=id#打印执行的数据库以及语句sharding.jdbc.config.props..sql.show=truespring.main.allow-bean-definition-overriding=true#读写分离sharding.jdbc.datasource.dsmaster=接口测试使用postman示例:GET请求------>http://localhost:8080/bookPOST请求:------->http://localhost:8080/book?id=1&name=java编程思想&count=8demo的github地址:https://github.com/Macky-He/spring-boot--shardingsphere-examples如各位觉得有帮助的话,还请给个star鼓励鼓励博主,谢谢!三、总结分库分表实现按照官方文档做一个demo是第一步,如需深入还需要研究源码,研究架构,研究思想;此文仅作为入门demo搭建指南,如需深入理解,还请移步至官方文档。参考资料1.官方文档:https://shardingsphere.apache.org/document/current/cn/manual/sharding-jdbc/usage/sharding/
Vue+SpringBoot实现WebSocket通信服务端在SpringBoot项目中添加ServerEndpointExporterBean的方法@BeanpublicServerEndpointExporterexporter(){returnnewServerEndpointExporter();}创建WebSocket客户端管理类:WebSocketComponent.javapackagecn.coralcloud.blog.web.component;importcom.alibaba.fastjson.JSON;importorg.springframework.stereotype.Component;importjavax.websocket.*;importjavax.websocket.server.ServerEndpoint;importjava.io.IOException;importjava.util.Objects;importjava.util.concurrent.CopyOnWriteArraySet;/**@authorgeff@nameWebSocketComponent@description@date2019-12-1814:22/@ServerEndpoint(value="/websocket")@ComponentpublicclassWebSocketComponent{/**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。/privatestaticintonlineCount=0;/**concurrent包的线程安全Set,用来存放每个客户端对应的CumWebSocket对象。/privatestaticCopyOnWriteArraySetwebSocketSet=newCopyOnWriteArraySet<>();/**与某个客户端的连接会话,需要通过它来给客户端发送数据/privateSessionsession;/**连接建立成功调用的方法@paramsessionsession/@OnOpenpublicvoidonOpen(Sessionsession){this.session=session;//加入set中webSocketSet.add(this);//添加在线人数addOnlineCount();System.out.println("新连接接入。当前在线人数为:"+getOnlineCount());}/**连接关闭调用的方法/@OnClosepublicvoidonClose(){//从set中删除webSocketSet.remove(this);//在线数减1subOnlineCount();System.out.println("有连接关闭。当前在线人数为:"+getOnlineCount());}/**收到客户端消息后调用@parammessagemessage@paramsessionsession/@OnMessagepublicvoidonMessage(Stringmessage,Sessionsession){System.out.println("客户端发送的消息:"+message);sendAll(JSON.toJSONString(messageDTO),session.getId());}/**群发@parammessagemessage/privatestaticvoidsendAll(Stringmessage,StringsessionId){webSocketSet.forEach(item->{if(!item.session.getId().equals(sessionId)){//群发try{item.sendMessage(message);}catch(IOExceptione){e.printStackTrace();}}});}/**发生错误时调用@paramsessionsession@paramerrorerror/@OnErrorpublicvoidonError(Sessionsession,Throwableerror){System.out.println("----websocket-------有异常啦");error.printStackTrace();}/**减少在线人数/privatevoidsubOnlineCount(){WebSocketComponent.onlineCount--;}/**添加在线人数/privatevoidaddOnlineCount(){WebSocketComponent.onlineCount++;}/**当前在线人数@returnint/publicstaticsynchronizedintgetOnlineCount(){returnonlineCount;}/**发送信息@parammessagemessagethrowsIOException/publicvoidsendMessage(Stringmessage)throwsIOException{//获取session远程基本连接发送文本消息this.session.getBasicRemote().sendText(message);//this.session.getAsyncRemote().sendText(message);}@Overridepublicbooleanequals(Objecto){if(this==o){returntrue;}if(o==null||getClass()!=o.getClass()){returnfalse;}WebSocketComponentthat=(WebSocketComponent)o;returnObjects.equals(session,that.session);}@OverridepublicinthashCode(){returnObjects.hash(session);}}@ServerEndpoint注解标识当前WebSocket服务端endpoint地址,本文实际前端访问的ws地址为:ws://localhost:8080/websocket。至此,服务端工作完成。页面VUE端<template><el-cardv-loading="loading"element-loading-spinner="el-icon-loading":body-style="{padding:'5px',backgroundColor:'#eee'}"class="socket-box"shadow="hover"><divclass="socket-box__content":style="{height:(boxHeight-125)+'px'}"id="socket-content"><divv-if="hasMore"@click="loadMore"class="load-more"><span>加载更多</span></div><divv-elsestyle="width:100%;text-align:center;font-size:12px">没有更多了</div><divclass="item"v-for="minmessages":class="checkMe(m)?'sender':''"><divclass="slide"><divclass="avatar":style="{background:m.background}">{{m.name.substring(0,1)}}</div><divclass="meta"><divclass="name">{{m.name}}</div><divclass="date">{{m.createTime|datetime}}</div></div></div><p>{{m.content}}</p></div></div><divclass="socket-box__footer"><el-form@submit.native.prevent><el-form-item><el-inputtype="textarea"resize="none":rows="3":disabled="!connect":placeholder="connect?'输入内容...':'当前连接断开,请刷新重试!'":clearable="true"v-model="message"@keydown.native.enter="submitMsgForm"></el-input></el-form-item><el-form-item><el-button@click="sendMsg(message)":disabled="!connect"style="width:100%"type="primary"size="small">发送(Enter)</el-button></el-form-item></el-form></div></el-card></template><script>import{GET}from"@/api";exportdefault{name:"Chatroom",data(){return{messages:[],message:'',//boxHeight:document.documentElement.clientHeight-85,hasMore:true,pager:{pageNo:1,pageSize:10,total:0},loading:false,connect:false}},props:{boxHeight:{type:Number,required:true}},methods:{submitMsgForm(event){if(event.shiftKey){return;}event.preventDefault();this.sendMsg(this.message)},checkMe(message){letuser=localStorage.getItem("socketUser");if(user){user=JSON.parse(user);returnuser.uid===message.uid}else{returnfalse;}},initWebSocket:function(){this.websock=newWebSocket(`ws://localhost:8080/websocket`);this.websock.onopen=this.websocketonopen;this.websock.onerror=this.websocketonerror;this.websock.onmessage=this.websocketonmessage;this.websock.onclose=this.websocketclose;constthat=this;that.loading=true;GET({url:'/api/personal/web/message/socketData?pageNo=1',callback:res=>{if(res.code===200){that.messages=res.data.messages;that.hasMore=res.data.messages.length===that.pager.pageSize;that.$nextTick(function(){document.getElementById("socket-content").scroll({top:document.getElementById("socket-content").scrollHeight,left:0,behavior:'smooth'})})}that.loading=false}})},sendMsg(data){if(/^\s*$/.test(data)){this.message='';return;}//发送时传入JSON(UID,昵称,内容)constlocal=localStorage.getItem("socketUser");if(local){constl=JSON.parse(local);this.send(l,data)}else{//弹框this.$prompt('首次发表,请输入昵称','提示',{confirmButtonText:'确定',cancelButtonText:'取消',}).then(({value})=>{//随机生成UIDconstuid=this.randomVideoUuid(32,16);constform={uid:uid,name:value,background:`rgb(${Math.random()*255},${Math.random()*255},${Math.random()*255})`};localStorage.setItem("socketUser",JSON.stringify(form));this.send(form,data)}).catch(()=>{this.$message({type:'info',message:'取消输入'});});}},send(obj,data){obj.content=data;obj.createTime=newDate().getTime();this.websock.send(JSON.stringify(obj));this.message='';this.messages.push(obj);this.$nextTick(function(){document.getElementById("socket-content").scroll({top:document.getElementById("socket-content").scrollHeight,left:0,behavior:'smooth'})})},loadMore(){this.loading=true;constthat=this;if(this.hasMore){this.pager.pageNo+=1;GET({url:'/api/personal/web/message/socketData?pageNo='+this.pager.pageNo,callback:res=>{if(res.code===200){that.messages=[...res.data.messages,...that.messages];that.hasMore=res.data.messages.length>=that.pager.pageSize;}that.loading=false;}})}},websocketonopen:function(e){console.log("WebSocket连接成功",e);this.connect=true;},websocketonerror:function(e){console.log("WebSocket连接发生错误");this.connect=false;},websocketonmessage:function(e){constda=JSON.parse(e.data);this.messages.push(da);this.$nextTick(function(){document.getElementById("socket-content").scroll({top:document.getElementById("socket-content").scrollHeight,left:0,behavior:'smooth'})})},websocketclose:function(e){console.log("connectionclosed("+e.code+")");},randomVideoUuid(len,radix){letchars='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');letuuid=[];radix=radix||chars.length;if(len){for(leti=0;i<len;i++)uuid[i]=chars[0|Math.random()*radix];}else{letr;uuid[8]=uuid[13]=uuid[18]=uuid[23]='-';uuid[14]='4';for(leti=0;i<36;i++){if(!uuid[i]){r=0|Math.random()*16;uuid[i]=chars[(i===19)?(r&0x3)|0x8:r];}}}returnuuid.join('');},},mounted(){this.initWebSocket();}}</script>本段代码为简单的Vue实现的网页聊天室代码,其中@/api为自己简单封装的JS函数,用户初次进入页面时会生成一个随机UID保存到localStorage中,在mounted周期中初始化websocket连接。本聊天室最终效果地址:https://web.coralcloud.cn/blog/message
- SpringBoot+Thymleaf项目初入(五) - 图片验证码
- SpringBoot+Thymleaf项目初入(四) - 用户登录页面优化
- SpringBoot+Thymleaf项目初入(三) - 用户登录
- SpringBoot+Thymleaf项目初入(二) - 配置基础页面访问
- SpringBoot+Thymleaf项目初入(一) - 基础项目搭建
- MyBatis之where关键字与<where>标签的区别
- 文件上传之@RequestParam与@RequestPart
- Spring注解之@Component
- SpringBoot框架之@Controller和@RestController的区别?
- Centos安装ApacheHadoop2.7.7
