├── src ├── main │ ├── resources │ │ ├── application.properties │ │ └── fonts │ │ │ └── simhei.ttf │ └── java │ │ └── cn │ │ └── wildfirechat │ │ └── app │ │ ├── pojo │ │ ├── UserIdPojo.java │ │ ├── GroupIdPojo.java │ │ ├── ComplainRequest.java │ │ ├── UploadFileResponse.java │ │ ├── RecordingRequest.java │ │ ├── LoadFavoriteRequest.java │ │ ├── ConferenceInfoRequest.java │ │ ├── GroupAnnouncementPojo.java │ │ ├── LoadFavoriteResponse.java │ │ ├── DestroyRequest.java │ │ ├── SendCodeRequest.java │ │ ├── CancelSessionRequest.java │ │ ├── ChangeNameRequest.java │ │ ├── UserIdNamePortraitPojo.java │ │ ├── ChangePasswordRequest.java │ │ ├── ConferenceInfo.java │ │ ├── SendMessageRequest.java │ │ ├── ResetPasswordRequest.java │ │ ├── ConfirmSessionRequest.java │ │ ├── PhoneCodeLoginRequest.java │ │ ├── UserPasswordLoginRequest.java │ │ ├── CreateSessionRequest.java │ │ ├── LoginResponse.java │ │ └── SessionOutput.java │ │ ├── tools │ │ ├── UserNameGenerator.java │ │ ├── PhoneNumberUserNameGenerator.java │ │ ├── UUIDUserNameGenerator.java │ │ ├── LdapUser.java │ │ ├── OrderedIdUserNameGenerator.java │ │ ├── SpinLock.java │ │ ├── NumericIdGenerator.java │ │ ├── ShortUUIDGenerator.java │ │ ├── Utils.java │ │ ├── LdapUtil.java │ │ └── RateLimiter.java │ │ ├── jpa │ │ ├── RecordRepository.java │ │ ├── ShiroSessionRepository.java │ │ ├── UserNameRepository.java │ │ ├── ConferenceEntityRepository.java │ │ ├── UserPrivateConferenceIdRepository.java │ │ ├── UserPasswordRepository.java │ │ ├── UserNameEntry.java │ │ ├── PCSessionRepository.java │ │ ├── AnnouncementRepository.java │ │ ├── FavoriteRepository.java │ │ ├── UserPrivateConferenceId.java │ │ ├── ShiroSession.java │ │ ├── Announcement.java │ │ ├── UserConference.java │ │ ├── UserConferenceRepository.java │ │ ├── FavoriteItem.java │ │ ├── Record.java │ │ ├── UserPassword.java │ │ ├── PCSession.java │ │ └── ConferenceEntity.java │ │ ├── sms │ │ ├── SmsService.java │ │ ├── AliyunSMSConfig.java │ │ ├── TencentSMSConfig.java │ │ └── SmsServiceImpl.java │ │ ├── ForbiddenException.java │ │ ├── avatar │ │ ├── AvatarService.java │ │ ├── GroupAvatarRequest.java │ │ ├── AvatarController.java │ │ ├── NameAvatarBuilder.java │ │ ├── AvatarServiceImpl.java │ │ └── GroupAvatarUtil.java │ │ ├── model │ │ └── ConferenceDTO.java │ │ ├── shiro │ │ ├── PhoneCodeToken.java │ │ ├── TokenAuthenticationToken.java │ │ ├── LdapToken.java │ │ ├── CorsFilter.java │ │ ├── ShiroSessionManager.java │ │ ├── TokenMatcher.java │ │ ├── PhoneCodeRealm.java │ │ ├── ScanCodeRealm.java │ │ ├── JsonAuthLoginFilter.java │ │ ├── LdapRealm.java │ │ ├── UserPasswordRealm.java │ │ ├── LdapMatcher.java │ │ ├── DBSessionDao.java │ │ ├── ShiroConfig.java │ │ └── AuthDataSource.java │ │ ├── conference │ │ ├── ConferenceService.java │ │ └── ConferenceController.java │ │ ├── Application.java │ │ ├── Service.java │ │ ├── RestResult.java │ │ ├── IMExceptionEventController.java │ │ ├── AudioController.java │ │ ├── IMConfig.java │ │ ├── IMCallbackController.java │ │ └── AppController.java ├── lib │ ├── sdk-1.4.2.jar │ └── common-1.4.2.jar └── test │ └── java │ └── cn │ └── wildfirechat │ └── app │ └── ApplicationTests.java ├── .gitignore ├── deb └── control │ ├── postinst │ ├── postrm │ └── control ├── config ├── aliyun_sms.properties ├── tencent_sms.properties ├── im.properties └── application.properties ├── docker ├── Dockerfile └── README.md ├── .github └── workflows │ └── build.yml ├── systemd ├── app-server.service └── README.md ├── aliyun_sms.md ├── nginx └── appserver.conf ├── LICENSE ├── release_note.md ├── mvnw.cmd ├── README.md ├── mvnw └── pom.xml /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | appdata.mv.db 4 | nohup.out 5 | appdata.trace.db 6 | avatar/ 7 | -------------------------------------------------------------------------------- /src/lib/sdk-1.4.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/app-server/HEAD/src/lib/sdk-1.4.2.jar -------------------------------------------------------------------------------- /src/lib/common-1.4.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/app-server/HEAD/src/lib/common-1.4.2.jar -------------------------------------------------------------------------------- /deb/control/postinst: -------------------------------------------------------------------------------- 1 | mv -f /opt/app-server/app-*.jar /opt/app-server/app-server.jar 2 | systemctl daemon-reload 3 | -------------------------------------------------------------------------------- /deb/control/postrm: -------------------------------------------------------------------------------- 1 | rm -rf /opt/app-server 2 | rm -rf /usr/lib/systemd/system/app-server.service 3 | systemctl daemon-reload -------------------------------------------------------------------------------- /src/main/resources/fonts/simhei.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/app-server/HEAD/src/main/resources/fonts/simhei.ttf -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/UserIdPojo.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class UserIdPojo { 4 | public String userId; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/GroupIdPojo.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class GroupIdPojo { 4 | public String groupId; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/ComplainRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class ComplainRequest { 4 | public String text; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/UploadFileResponse.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class UploadFileResponse { 4 | public String url; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/RecordingRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class RecordingRequest { 4 | public boolean recording; 5 | } 6 | -------------------------------------------------------------------------------- /config/aliyun_sms.properties: -------------------------------------------------------------------------------- 1 | alisms.accessKeyId=MTAI82gOTQQTuKtW 2 | alisms.accessSecret=4p7HlgMTOQWHsX82IICabcea556677 3 | alisms.signName=\u91CE\u706BIM 4 | alisms.templateCode=SMS_170843232 5 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/UserNameGenerator.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | public interface UserNameGenerator { 4 | String getUserName(String phone); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/LoadFavoriteRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class LoadFavoriteRequest { 4 | public long id; 5 | public int count; 6 | } 7 | -------------------------------------------------------------------------------- /config/tencent_sms.properties: -------------------------------------------------------------------------------- 1 | sms.secretId=AKIsaepMSEL91dsMESAUMO2smphIdgSxB8oD 2 | sms.secretKey=91dADocdksuw23AEFCD78lsdudf35ta0 3 | sms.appId=1432000001 4 | sms.templateId=592276 5 | sms.sign=北京野火无限网络科技 -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/ConferenceInfoRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class ConferenceInfoRequest { 4 | public String conferenceId; 5 | public String password; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/RecordRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | public interface RecordRepository extends CrudRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/sms/SmsService.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.sms; 2 | 3 | 4 | import cn.wildfirechat.app.RestResult; 5 | 6 | public interface SmsService { 7 | RestResult.RestCode sendCode(String mobile, String code); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/ShiroSessionRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | public interface ShiroSessionRepository extends CrudRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/GroupAnnouncementPojo.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class GroupAnnouncementPojo { 4 | public String groupId; 5 | public String author; 6 | public String text; 7 | public long timestamp; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/LoadFavoriteResponse.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | import cn.wildfirechat.app.jpa.FavoriteItem; 4 | 5 | import java.util.List; 6 | 7 | public class LoadFavoriteResponse { 8 | public List items; 9 | public boolean hasMore; 10 | } 11 | -------------------------------------------------------------------------------- /deb/control/control: -------------------------------------------------------------------------------- 1 | Package: app-server 2 | Version: [[version]] 3 | Section: misc 4 | Priority: optional 5 | Architecture: all 6 | Maintainer: Wildfirechat 7 | Description: App Server 8 | Distribution: development 9 | Depends: openjdk-8-jre-headless 10 | Homepage: https://wildfirechat.cn 11 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/DestroyRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class DestroyRequest { 4 | private String code; 5 | 6 | public String getCode() { 7 | return code; 8 | } 9 | 10 | public void setCode(String code) { 11 | this.code = code; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/ForbiddenException.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.FORBIDDEN, reason="Forbidden") 7 | public class ForbiddenException extends RuntimeException { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/SendCodeRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class SendCodeRequest { 4 | private String mobile; 5 | 6 | public String getMobile() { 7 | return mobile; 8 | } 9 | 10 | public void setMobile(String mobile) { 11 | this.mobile = mobile; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/CancelSessionRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class CancelSessionRequest { 4 | private String token; 5 | 6 | public String getToken() { 7 | return token; 8 | } 9 | 10 | public void setToken(String token) { 11 | this.token = token; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/UserNameRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | 6 | @RepositoryRestResource() 7 | public interface UserNameRepository extends CrudRepository {} 8 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/ChangeNameRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class ChangeNameRequest { 4 | private String newName; 5 | 6 | public String getNewName() { 7 | return newName; 8 | } 9 | 10 | public void setNewName(String newName) { 11 | this.newName = newName; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/PhoneNumberUserNameGenerator.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | @Component 6 | public class PhoneNumberUserNameGenerator implements UserNameGenerator { 7 | @Override 8 | public String getUserName(String phone) { 9 | return phone; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jre-alpine 2 | 3 | COPY ../target/app-*.jar /opt/app-server/app.jar 4 | COPY ../config /opt/app-server/config 5 | 6 | WORKDIR /opt/app-server 7 | 8 | VOLUME /opt/app-server/config 9 | VOLUME /opt/app-server/h2db 10 | 11 | EXPOSE 8888/tcp 12 | 13 | ENV JVM_XMX 256M 14 | ENV JVM_XMS 256M 15 | 16 | CMD java -server -Xmx$JVM_XMX -Xms$JVM_XMS -jar app.jar 17 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/ConferenceEntityRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.springframework.data.repository.PagingAndSortingRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | 6 | @RepositoryRestResource() 7 | public interface ConferenceEntityRepository extends PagingAndSortingRepository { 8 | 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/UserPrivateConferenceIdRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.springframework.data.repository.PagingAndSortingRepository; 4 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 5 | 6 | @RepositoryRestResource() 7 | public interface UserPrivateConferenceIdRepository extends PagingAndSortingRepository { 8 | 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/UUIDUserNameGenerator.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.UUID; 6 | 7 | @Component 8 | public class UUIDUserNameGenerator implements UserNameGenerator { 9 | @Override 10 | public String getUserName(String phone) { 11 | return "wfid-" + UUID.randomUUID().toString().replaceAll("-", ""); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/cn/wildfirechat/app/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/UserPasswordRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.repository.PagingAndSortingRepository; 5 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 6 | 7 | @RepositoryRestResource() 8 | public interface UserPasswordRepository extends CrudRepository { 9 | 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/UserNameEntry.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import javax.persistence.*; 4 | 5 | @Entity 6 | @Table(name = "t_user_name") 7 | public class UserNameEntry { 8 | @Id 9 | @GeneratedValue(strategy=GenerationType.IDENTITY) 10 | @Column(name = "id") 11 | private Integer id; 12 | 13 | public Integer getId() { 14 | return id; 15 | } 16 | 17 | public void setId(Integer id) { 18 | this.id = id; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/PCSessionRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.springframework.data.jpa.repository.Modifying; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | public interface PCSessionRepository extends CrudRepository { 8 | 9 | @Modifying 10 | @Transactional 11 | Long deleteByCreateDtBefore(long timestamp); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/AnnouncementRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.springframework.data.repository.PagingAndSortingRepository; 4 | import org.springframework.data.repository.query.Param; 5 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 6 | 7 | import java.util.List; 8 | 9 | @RepositoryRestResource() 10 | public interface AnnouncementRepository extends PagingAndSortingRepository { 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/UserIdNamePortraitPojo.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class UserIdNamePortraitPojo { 4 | public String userId; 5 | public String name; 6 | public String portrait; 7 | 8 | public UserIdNamePortraitPojo() { 9 | } 10 | 11 | public UserIdNamePortraitPojo(String userId, String name, String portrait) { 12 | this.userId = userId; 13 | this.name = name; 14 | this.portrait = portrait; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/avatar/AvatarService.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.avatar; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | 5 | import java.io.IOException; 6 | import java.net.URISyntaxException; 7 | import java.util.concurrent.CompletableFuture; 8 | 9 | public interface AvatarService { 10 | ResponseEntity avatar(String name) throws IOException; 11 | 12 | CompletableFuture> groupAvatar(GroupAvatarRequest requeset) throws IOException, URISyntaxException; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/LdapUser.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | public class LdapUser { 4 | public final String uid, cn, mail, phone, dn; 5 | public LdapUser(String uid, String cn, String mail, String phone, String dn) { 6 | this.uid = uid; this.cn = cn; this.mail = mail; this.phone = phone; this.dn = dn; 7 | } 8 | @Override public String toString() { 9 | return String.format("User{uid='%s', cn='%s', mail='%s', phone='%s', dn='%s'}", uid, cn, mail, phone, dn); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # 野火应用服务docker使用说明 2 | 3 | ## 编译镜像 4 | 首先需要先编译应用服务,使用下面命令编译 5 | ``` 6 | mvn clean package 7 | ``` 8 | 9 | 然后进入到docker目录编译镜像 10 | ``` 11 | sudo docker build -t app-server -f Dockerfile .. 12 | ``` 13 | 14 | ## 运行 15 | 直接运行: 16 | ``` 17 | sudo docker run -it -p 8888:8888 -e JVM_XMX=256M -e JVM_XMS=256M app-server 18 | ``` 19 | 20 | 配置: 21 | 如果配置需要修改,可以修改config目录下的配置,然后重新打包镜像,也可以手动指定配置目录,这样不用重新打包镜像。手动指定配置目录的方法如下,注意路径需要绝对路径 22 | ``` 23 | sudo docker run -it -v $PATH_TO_CONFIG:/opt/app-server/config -v $PATH_TO_H2DB:/opt/app-server/h2db -e JVM_XMX=256M -e JVM_XMS=256M -p 8888:8888 app-server 24 | ``` 25 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/model/ConferenceDTO.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.model; 2 | 3 | public interface ConferenceDTO { 4 | String getId(); 5 | String getConference_title(); 6 | String getPassword(); 7 | String getPin(); 8 | String getOwner(); 9 | public String getManages(); 10 | long getStart_time(); 11 | long getEnd_time(); 12 | boolean isAudience(); 13 | boolean isAdvance(); 14 | boolean isAllow_switch_mode(); 15 | boolean isNo_join_before_start(); 16 | boolean isRecording(); 17 | String getFocus(); 18 | int getMax_participants(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/ChangePasswordRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class ChangePasswordRequest { 4 | private String oldPassword; 5 | private String newPassword; 6 | 7 | public String getOldPassword() { 8 | return oldPassword; 9 | } 10 | 11 | public void setOldPassword(String oldPassword) { 12 | this.oldPassword = oldPassword; 13 | } 14 | 15 | public String getNewPassword() { 16 | return newPassword; 17 | } 18 | 19 | public void setNewPassword(String newPassword) { 20 | this.newPassword = newPassword; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/PhoneCodeToken.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | import org.apache.shiro.authc.AuthenticationToken; 4 | 5 | public class PhoneCodeToken implements AuthenticationToken { 6 | final private String phone; 7 | final private String code; 8 | 9 | public PhoneCodeToken(String phone, String code) { 10 | this.phone = phone; 11 | this.code = code; 12 | } 13 | 14 | @Override 15 | public Object getPrincipal() { 16 | return phone; 17 | } 18 | 19 | @Override 20 | public Object getCredentials() { 21 | return code; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/FavoriteRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.springframework.data.jpa.repository.Query; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 6 | 7 | import java.util.List; 8 | 9 | @RepositoryRestResource() 10 | public interface FavoriteRepository extends CrudRepository { 11 | 12 | @Query(value = "select * from t_favorites where user_id = ?1 and id < ?2 order by id desc limit ?3", nativeQuery = true) 13 | List loadFav(String userId, long startId, int count); 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Build with Maven 24 | run: mvn -B package --file pom.xml 25 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/ConferenceInfo.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | import java.util.List; 4 | 5 | public class ConferenceInfo { 6 | public String conferenceId; 7 | public String conferenceTitle; 8 | public String password; 9 | public String pin; 10 | public String owner; 11 | public List managers; 12 | public long startTime; 13 | public long endTime; 14 | public boolean audience; 15 | public boolean advance; 16 | public boolean allowSwitchMode; 17 | public boolean noJoinBeforeStart; 18 | public boolean recording; 19 | public String focus; 20 | public int maxParticipants; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/SendMessageRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | import java.util.List; 4 | 5 | public class SendMessageRequest { 6 | public int type; 7 | public String target; 8 | public int line; 9 | 10 | public int content_type; 11 | public String content_searchable; 12 | public String content_binary; 13 | public String content; 14 | public String content_push; 15 | public String content_push_data; 16 | public int content_media_type; 17 | public String content_remote_url; 18 | public String content_extra; 19 | public int content_mentioned_type; 20 | public List content_mentioned_targets; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/OrderedIdUserNameGenerator.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | import cn.wildfirechat.app.jpa.UserNameEntry; 4 | import cn.wildfirechat.app.jpa.UserNameRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class OrderedIdUserNameGenerator implements UserNameGenerator { 10 | @Autowired 11 | private UserNameRepository userIdRepository; 12 | 13 | @Override 14 | public String getUserName(String phone) { 15 | UserNameEntry entry = new UserNameEntry(); 16 | userIdRepository.save(entry); 17 | return entry.getId() + ""; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/TokenAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | import org.apache.shiro.authc.AuthenticationToken; 4 | 5 | public class TokenAuthenticationToken implements AuthenticationToken { 6 | private String token; 7 | 8 | public TokenAuthenticationToken(String token) { 9 | this.token = token; 10 | } 11 | 12 | public String getToken() { 13 | return token; 14 | } 15 | 16 | public void setToken(String token) { 17 | this.token = token; 18 | } 19 | 20 | @Override 21 | public Object getPrincipal() { 22 | return token; 23 | } 24 | 25 | @Override 26 | public Object getCredentials() { 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/SpinLock.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | import java.util.concurrent.atomic.AtomicReference; 4 | 5 | public class SpinLock { 6 | //java中原子(CAS)操作 7 | AtomicReference owner = new AtomicReference<>();//持有自旋锁的线程对象 8 | private int count; 9 | public void lock() { 10 | Thread cur = Thread.currentThread(); 11 | //lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。当有第二个线程调用lock操作时由于owner值不为空,导致循环 12 | 13 | //一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。 14 | while (!owner.compareAndSet(null, cur)){ 15 | } 16 | } 17 | public void unLock() { 18 | Thread cur = Thread.currentThread(); 19 | owner.compareAndSet(cur, null); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/LdapToken.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | import org.apache.shiro.authc.AuthenticationToken; 4 | 5 | public class LdapToken implements AuthenticationToken { 6 | final private String phone; 7 | final private String password; 8 | final private String ldapUrl; 9 | 10 | public LdapToken(String phone, String password, String ldapUrl) { 11 | this.phone = phone; 12 | this.password = password; 13 | this.ldapUrl = ldapUrl; 14 | } 15 | 16 | @Override 17 | public Object getPrincipal() { 18 | return phone; 19 | } 20 | 21 | @Override 22 | public Object getCredentials() { 23 | return password; 24 | } 25 | 26 | public String getLdapUrl() { 27 | return ldapUrl; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/ResetPasswordRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class ResetPasswordRequest { 4 | private String mobile; 5 | private String resetCode; 6 | private String newPassword; 7 | 8 | public String getMobile() { 9 | return mobile; 10 | } 11 | 12 | public void setMobile(String mobile) { 13 | this.mobile = mobile; 14 | } 15 | 16 | public String getResetCode() { 17 | return resetCode; 18 | } 19 | 20 | public void setResetCode(String resetCode) { 21 | this.resetCode = resetCode; 22 | } 23 | 24 | public String getNewPassword() { 25 | return newPassword; 26 | } 27 | 28 | public void setNewPassword(String newPassword) { 29 | this.newPassword = newPassword; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /systemd/app-server.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WildfirechatAPP 3 | Documentation=https://docs.wildfirechat.cn 4 | Wants=network-online.target 5 | After=network-online.target 6 | 7 | [Service] 8 | WorkingDirectory=/opt/app-server 9 | #ExecStart=/usr/bin/java -server -Xmx2G -Xms2G -jar app-server.jar 2>&1 10 | ExecStart=/usr/bin/java -server -jar app-server.jar 2>&1 11 | 12 | # Let systemd restart this service always 13 | Restart=always 14 | RestartSec=5 15 | 16 | # Specifies the maximum file descriptor number that can be opened by this process 17 | LimitNOFILE=65536 18 | 19 | # Specifies the maximum number of threads this process can create 20 | TasksMax=infinity 21 | 22 | # Disable timeout logic and wait until process is stopped 23 | TimeoutStopSec=infinity 24 | SendSIGKILL=no 25 | 26 | [Install] 27 | WantedBy=multi-user.target 28 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/ConfirmSessionRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class ConfirmSessionRequest { 4 | private String token; 5 | private String user_id; 6 | private int quick_login; 7 | 8 | //{"token":"22295ee9-d4e3-4fc0-bda3-bcfe008dce08","user_id":"CeDRCRtt","quick_login":true} 9 | public String getToken() { 10 | return token; 11 | } 12 | 13 | public void setToken(String token) { 14 | this.token = token; 15 | } 16 | 17 | public String getUser_id() { 18 | return user_id; 19 | } 20 | 21 | public void setUser_id(String user_id) { 22 | this.user_id = user_id; 23 | } 24 | 25 | public int getQuick_login() { 26 | return quick_login; 27 | } 28 | 29 | public void setQuick_login(int quick_login) { 30 | this.quick_login = quick_login; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/avatar/GroupAvatarRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.avatar; 2 | 3 | import java.util.List; 4 | 5 | public class GroupAvatarRequest { 6 | private List members; 7 | 8 | public List getMembers() { 9 | return members; 10 | } 11 | 12 | public void setMembers(List members) { 13 | this.members = members; 14 | } 15 | 16 | public static class GroupMemberInfo { 17 | private String name; 18 | private String avatarUrl; 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | 24 | public void setName(String name) { 25 | this.name = name; 26 | } 27 | 28 | public String getAvatarUrl() { 29 | return avatarUrl; 30 | } 31 | 32 | public void setAvatarUrl(String avatarUrl) { 33 | this.avatarUrl = avatarUrl; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/PhoneCodeLoginRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class PhoneCodeLoginRequest { 4 | private String mobile; 5 | private String code; 6 | private String clientId; 7 | private Integer platform; 8 | 9 | public String getClientId() { 10 | return clientId; 11 | } 12 | 13 | public void setClientId(String clientId) { 14 | this.clientId = clientId; 15 | } 16 | 17 | public String getMobile() { 18 | return mobile; 19 | } 20 | 21 | public void setMobile(String mobile) { 22 | this.mobile = mobile; 23 | } 24 | 25 | public String getCode() { 26 | return code; 27 | } 28 | 29 | public Integer getPlatform() { 30 | return platform; 31 | } 32 | 33 | public void setPlatform(Integer platform) { 34 | this.platform = platform; 35 | } 36 | 37 | public void setCode(String code) { 38 | this.code = code; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/UserPrivateConferenceId.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Id; 6 | import javax.persistence.Table; 7 | 8 | @Entity 9 | @Table(name = "user_private_conference_id") 10 | public class UserPrivateConferenceId { 11 | @Id 12 | @Column(length = 128) 13 | private String userId; 14 | 15 | private String conferenceId; 16 | 17 | public UserPrivateConferenceId() { 18 | } 19 | 20 | public UserPrivateConferenceId(String userId, String conferenceId) { 21 | this.userId = userId; 22 | this.conferenceId = conferenceId; 23 | } 24 | 25 | public String getUserId() { 26 | return userId; 27 | } 28 | 29 | public void setUserId(String userId) { 30 | this.userId = userId; 31 | } 32 | 33 | public String getConferenceId() { 34 | return conferenceId; 35 | } 36 | 37 | public void setConferenceId(String conferenceId) { 38 | this.conferenceId = conferenceId; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/ShiroSession.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import org.hibernate.annotations.Type; 4 | 5 | import javax.persistence.*; 6 | 7 | @Entity 8 | @Table(name = "shiro_session") 9 | public class ShiroSession { 10 | @Id 11 | @Column(length = 128) 12 | private String sessionId; 13 | 14 | @Lob 15 | @Column(name="session_data", length = 2048) 16 | @Type(type="org.hibernate.type.BinaryType") 17 | private byte[] sessionData; 18 | 19 | public ShiroSession(String sessionId, byte[] sessionData) { 20 | this.sessionId = sessionId; 21 | this.sessionData = sessionData; 22 | } 23 | 24 | public ShiroSession() { 25 | } 26 | 27 | public String getSessionId() { 28 | return sessionId; 29 | } 30 | 31 | public void setSessionId(String sessionId) { 32 | this.sessionId = sessionId; 33 | } 34 | 35 | public byte[] getSessionData() { 36 | return sessionData; 37 | } 38 | 39 | public void setSessionData(byte[] sessionData) { 40 | this.sessionData = sessionData; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/UserPasswordLoginRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class UserPasswordLoginRequest { 4 | private String mobile; 5 | private String password; 6 | private String clientId; 7 | private Integer platform; 8 | 9 | public String getClientId() { 10 | return clientId; 11 | } 12 | 13 | public void setClientId(String clientId) { 14 | this.clientId = clientId; 15 | } 16 | 17 | public String getMobile() { 18 | return mobile; 19 | } 20 | 21 | public void setMobile(String mobile) { 22 | this.mobile = mobile; 23 | } 24 | 25 | public String getPassword() { 26 | return password; 27 | } 28 | 29 | public Integer getPlatform() { 30 | return platform; 31 | } 32 | 33 | public void setPlatform(Integer platform) { 34 | this.platform = platform; 35 | } 36 | 37 | public void setPassword(String password) { 38 | this.password = password; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /aliyun_sms.md: -------------------------------------------------------------------------------- 1 | # 阿里云短信功能说明 2 | 3 | ## 短信对接 4 | 1. 在[这里](https://usercenter.console.aliyun.com/#/manage/ak)申请阿里云***accessKeyId***和***accessSecret*** 5 | 2. 开通短信服务,并申请短信签名和短信模版。注意申请短信签名和模版都是需要审核的,可以同时申请,以便节省您的时间 6 | 3. 修改```config```目录下的```aliyun_sms.properities```,填入上述四个参数。比如 7 | ```$xslt 8 | alisms.accessKeyId=LTXXXXXXXXXXXXtW 9 | alisms.accessSecret=4pXXXXXXXXXXXXXXXXXXXXXXXXXXXXyU 10 | alisms.signName=野火IM 11 | alisms.templateCode=SMS_170000000 12 | ``` 13 | 4. 修改默认使用阿里云短信,在```application.properites```文件中修改```sms.vendor```为***2*** 14 | 5. 运行测试。 15 | 16 | > 上述几个参数如果不明白,可以参考[阿里云文档](https://help.aliyun.com/document_detail/55284.html?spm=a2c4e.11153987.0.0.5861aeecePRLPH) 17 | 18 | ## 迁移阿里云短信功能 19 | 指导如何把阿里云短信功能迁移到客户应用服务中 20 | 1. 引入jar包 21 | ```$xslt 22 | 23 | com.aliyun 24 | aliyun-java-sdk-core 25 | 4.1.0 26 | 27 | ``` 28 | 29 | 2. 拷贝除了```Application.java```以外的所有源码到客户应用服务器. 30 | 31 | 3. 拷贝配置文件到客户应用服务,需要注意配置文件会依赖特定的路径,请放置正确的路径 -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/Announcement.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import javax.persistence.*; 4 | 5 | @Entity 6 | @Table(name = "text") 7 | public class Announcement { 8 | @Id 9 | @Column(length = 128) 10 | private String groupId; 11 | 12 | private String author; 13 | 14 | @Column(length = 2048) 15 | private String announcement; 16 | 17 | private long timestamp; 18 | 19 | public String getGroupId() { 20 | return groupId; 21 | } 22 | 23 | public void setGroupId(String groupId) { 24 | this.groupId = groupId; 25 | } 26 | 27 | public String getAnnouncement() { 28 | return announcement; 29 | } 30 | 31 | public void setAnnouncement(String announcement) { 32 | this.announcement = announcement; 33 | } 34 | 35 | public String getAuthor() { 36 | return author; 37 | } 38 | 39 | public void setAuthor(String author) { 40 | this.author = author; 41 | } 42 | 43 | public long getTimestamp() { 44 | return timestamp; 45 | } 46 | 47 | public void setTimestamp(long timestamp) { 48 | this.timestamp = timestamp; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/im.properties: -------------------------------------------------------------------------------- 1 | im.admin_url=http://localhost:18080 2 | #需要和im server里面配置的http.admin.secret_key一致 3 | im.admin_secret=123456 4 | 5 | #发送通知消息的管理员用户ID 6 | im.admin_user_id=admin 7 | 8 | #如果发送消息为乱码,请检查服务器是否支持中文 9 | #如果您不需要登录欢迎消息,请删掉下面两行 10 | im.welcome_for_new_user=欢迎您的加入!(如果您自己部署的这个服务,可以在应用服务的配置文件im.properties文件中修改这条欢迎语) 11 | im.welcome_for_back_user=欢迎您的归来!。(如果您自己部署的这个服务,可以在应用服务的配置文件im.properties文件中修改这条欢迎语) 12 | 13 | #是否使用随机用户名 14 | im.use_random_name=true 15 | 16 | # 新用户注册时,自动添加机器人为好友。这里可以修改为添加销售人员id,或者客服人员id,用户跟客户进行沟通。 17 | # 如果不需要要设置机器人为好友和发送欢迎信息,请删除下面三行 18 | im.new_user_robot_friend=true 19 | im.robot_friend_id=FireRobot 20 | im.robot_welcome=您好,我是人见人爱、花见花开、天下第一帅的机器人小火!可以跟我聊天哦!\n\n也可以给我打音视频电话,我现在可以接听电话了,我会延迟三秒播放您的声音。\n\n给我发送 流式文本 四个字,我会像 ChatGPT 那样采用流式输出的方式回复你。 21 | 22 | # 用户登录后发送广告语,一条文本消息,再加上一条图片消息。如果为空就不发送。 23 | im.prompt_text= 24 | im.image_msg_url= 25 | im.image_msg_base64_thumbnail= 26 | 27 | # 新用户注册时,自动关注频道,频道ID不能加双引号。如果不需要关注,下面配置内容设置为空就OK了。 28 | # im.new_user_subscribe_channel_id=vwzqmws2k 29 | im.new_user_subscribe_channel_id= 30 | # 用户再次登陆时,自动关注频道,频道ID不能加双引号。如果不需要关注,下面配置内容设置为空就OK了。 31 | im.back_user_subscribe_channel_id= 32 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/conference/ConferenceService.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.conference; 2 | 3 | 4 | import cn.wildfirechat.app.RestResult; 5 | import cn.wildfirechat.app.jpa.FavoriteItem; 6 | import cn.wildfirechat.app.pojo.*; 7 | import cn.wildfirechat.pojos.InputCreateDevice; 8 | import org.springframework.web.multipart.MultipartFile; 9 | 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | public interface ConferenceService { 13 | RestResult getUserConferenceId(String userId); 14 | RestResult getMyConferenceId(); 15 | RestResult getConferenceInfo(String conferenceId, String password); 16 | RestResult putConferenceInfo(ConferenceInfo info); 17 | RestResult createConference(ConferenceInfo info); 18 | RestResult destroyConference(String conferenceId); 19 | RestResult recordingConference(String conferenceId, boolean recording); 20 | RestResult focusConference(String conferenceId, String userId); 21 | RestResult favConference(String conferenceId); 22 | RestResult unfavConference(String conferenceId); 23 | RestResult getFavConferences(); 24 | RestResult isFavConference(String conferenceId); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/UserConference.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import javax.persistence.*; 4 | 5 | import java.util.List; 6 | 7 | import static javax.persistence.CascadeType.ALL; 8 | 9 | @Entity 10 | @Table(name = "user_conference", uniqueConstraints = {@UniqueConstraint(columnNames = {"userId","conferenceId"})}) 11 | public class UserConference { 12 | @Id 13 | @GeneratedValue(strategy=GenerationType.IDENTITY) 14 | @Column(name = "id") 15 | public Long id; 16 | 17 | @Column(length = 128) 18 | private String userId; 19 | 20 | private String conferenceId; 21 | 22 | private long timestamp; 23 | 24 | public UserConference() { 25 | } 26 | 27 | public UserConference(String userId, String conferenceId) { 28 | this.userId = userId; 29 | this.conferenceId = conferenceId; 30 | this.timestamp = System.currentTimeMillis(); 31 | } 32 | 33 | public String getUserId() { 34 | return userId; 35 | } 36 | 37 | public void setUserId(String userId) { 38 | this.userId = userId; 39 | } 40 | 41 | public String getConferenceId() { 42 | return conferenceId; 43 | } 44 | 45 | public void setConferenceId(String conferenceId) { 46 | this.conferenceId = conferenceId; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/avatar/AvatarController.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.avatar; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import java.io.IOException; 9 | import java.net.URISyntaxException; 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | @RestController 13 | @RequestMapping(value = "/avatar") 14 | public class AvatarController { 15 | @Autowired 16 | AvatarService avatarService; 17 | 18 | @CrossOrigin 19 | @GetMapping() 20 | public ResponseEntity avatar(@RequestParam("name") String name) throws IOException { 21 | return avatarService.avatar(name); 22 | } 23 | 24 | @GetMapping("/group") 25 | public CompletableFuture> groupAvatar(@RequestParam("request") String request) throws IOException, URISyntaxException { 26 | ObjectMapper mapper = new ObjectMapper(); 27 | GroupAvatarRequest groupAvatarRequest = mapper.readValue(request, GroupAvatarRequest.class); 28 | return avatarService.groupAvatar(groupAvatarRequest); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/UserConferenceRepository.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import cn.wildfirechat.app.model.ConferenceDTO; 4 | import org.springframework.data.jpa.repository.Modifying; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.PagingAndSortingRepository; 7 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | @RepositoryRestResource() 14 | public interface UserConferenceRepository extends PagingAndSortingRepository { 15 | @Transactional 16 | @Modifying 17 | @Query(value = "delete from user_conference where user_id = ?1 and conference_id = ?2", nativeQuery = true) 18 | void deleteByUserIdAndConferenceId(String userId, String conferenceId); 19 | 20 | @Query(value = "select c.* from user_conference uc, conference c where uc.user_id = ?1 and uc.conference_id = c.id and (c.end_time = 0 or c.end_time > ?2) order by id desc", nativeQuery = true) 21 | List findByUserId(String userId, long now); 22 | 23 | Optional findByUserIdAndConferenceId(String userId, String conferenceId); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/CorsFilter.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.filter.OncePerRequestFilter; 6 | 7 | import javax.servlet.FilterChain; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import java.io.IOException; 12 | 13 | @Component 14 | public class CorsFilter extends OncePerRequestFilter { 15 | @Override 16 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 17 | String origin = request.getHeader("Origin"); 18 | response.setHeader("Access-Control-Allow-Origin", origin == null ? "*" : origin); 19 | response.setHeader("Access-Control-Allow-Credentials", "true"); 20 | response.setHeader("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers, Cookie, X-Requested-With, authToken"); 21 | // refer to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers 22 | response.setHeader("Access-Control-Expose-Headers", "authToken"); 23 | 24 | filterChain.doFilter(request, response); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/CreateSessionRequest.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class CreateSessionRequest { 4 | private String token; 5 | private String device_name; 6 | private String clientId; 7 | private int platform; 8 | // 0,表示pc端为旧版本,不支持快速登录;1,表示pc端为新版本,支持快速登录 9 | private int flag; 10 | private String userId; 11 | 12 | public int getPlatform() { 13 | return platform; 14 | } 15 | 16 | public void setPlatform(int platform) { 17 | this.platform = platform; 18 | } 19 | 20 | public String getToken() { 21 | return token; 22 | } 23 | 24 | public void setToken(String token) { 25 | this.token = token; 26 | } 27 | 28 | public String getDevice_name() { 29 | return device_name; 30 | } 31 | 32 | public void setDevice_name(String device_name) { 33 | this.device_name = device_name; 34 | } 35 | 36 | public String getClientId() { 37 | return clientId; 38 | } 39 | 40 | public void setClientId(String clientId) { 41 | this.clientId = clientId; 42 | } 43 | 44 | public int getFlag() { 45 | return flag; 46 | } 47 | 48 | public void setFlag(int flag) { 49 | this.flag = flag; 50 | } 51 | 52 | public String getUserId() { 53 | return userId; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/FavoriteItem.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import javax.annotation.Nullable; 4 | import javax.persistence.*; 5 | 6 | @Entity 7 | @Table(name = "t_favorites", indexes = {@Index(columnList = "user_id, type")}) 8 | public class FavoriteItem { 9 | @Id 10 | @GeneratedValue(strategy=GenerationType.IDENTITY) 11 | @Column(name = "id") 12 | public Long id; 13 | 14 | @Column(name = "messageUid") 15 | @Nullable 16 | public Long messageUid; 17 | 18 | @Column(name = "user_id", length = 64) 19 | public String userId; 20 | 21 | @Column(name = "type") 22 | public int type; 23 | 24 | @Column(name = "timestamp") 25 | public long timestamp; 26 | 27 | @Column(name = "conv_type") 28 | public int convType; 29 | 30 | @Column(name = "conv_line") 31 | public int convLine; 32 | 33 | @Column(name = "conv_target") 34 | public String convTarget; 35 | 36 | @Column(name = "origin") 37 | public String origin; 38 | 39 | @Column(name = "sender") 40 | public String sender; 41 | 42 | @Column(name="title",columnDefinition="LONGTEXT") 43 | public String title; 44 | 45 | @Column(name="url",length = 1024) 46 | public String url; 47 | 48 | @Column(name = "thumb_url",length = 1024) 49 | public String thumbUrl; 50 | 51 | @Column(name="data",columnDefinition="LONGTEXT") 52 | public String data; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /nginx/appserver.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name apptest.wildfirechat.cn; 4 | rewrite ^(.*)$ https://apptest.wildfirechat.cn permanent; 5 | location ~ / { 6 | index index.html index.php index.htm; 7 | } 8 | } 9 | 10 | server { 11 | listen 443 ssl; 12 | server_name apptest.wildfirechat.cn; 13 | 14 | root html; 15 | index index.html index.htm; 16 | client_max_body_size 30m; #文件最大大小 17 | ssl_certificate cert/app.pem; 18 | ssl_certificate_key cert/app.key; 19 | ssl_session_timeout 5m; 20 | ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; 21 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 22 | ssl_prefer_server_ciphers on; 23 | 24 | ## 不需要添加 add_header Access-Control-Allow-Origin $http_origin; 等添加跨域相关 header 的配置,app-server 已经处理了跨域了,所有请求透传过去即可 25 | ## 26 | ## send request back to app server ## 27 | location / { 28 | # 扫码超时时间是 1 分钟,配置了大于一分钟 29 | proxy_read_timeout 100s; 30 | proxy_pass http://127.0.0.1:8888; 31 | } 32 | 33 | ## 如果需要通过 path 来分流的话,请参考下的配置,path后面的/和 8888 后面的/ 都不能省略,否则会提示 没有登录 34 | # 可参考这儿:https://www.jb51.net/article/244331.htm 35 | #location /app/ { 36 | # proxy_pass http://127.0.0.1:8888/; 37 | #} 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/sms/AliyunSMSConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.sms; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix="alisms") 9 | @PropertySource(value = "file:config/aliyun_sms.properties",encoding = "utf-8") 10 | public class AliyunSMSConfig { 11 | String accessKeyId; 12 | String accessSecret; 13 | String signName; 14 | String templateCode; 15 | 16 | public String getAccessKeyId() { 17 | return accessKeyId; 18 | } 19 | 20 | public void setAccessKeyId(String accessKeyId) { 21 | this.accessKeyId = accessKeyId; 22 | } 23 | 24 | public String getAccessSecret() { 25 | return accessSecret; 26 | } 27 | 28 | public void setAccessSecret(String accessSecret) { 29 | this.accessSecret = accessSecret; 30 | } 31 | 32 | public String getSignName() { 33 | return signName; 34 | } 35 | 36 | public void setSignName(String signName) { 37 | this.signName = signName; 38 | } 39 | 40 | public String getTemplateCode() { 41 | return templateCode; 42 | } 43 | 44 | public void setTemplateCode(String templateCode) { 45 | this.templateCode = templateCode; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/LoginResponse.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class LoginResponse { 4 | private String userId; 5 | private String token; 6 | private boolean register; 7 | private String userName; 8 | private String portrait; 9 | private String resetCode; 10 | 11 | public String getUserId() { 12 | return userId; 13 | } 14 | 15 | public void setUserId(String userId) { 16 | this.userId = userId; 17 | } 18 | 19 | public String getToken() { 20 | return token; 21 | } 22 | 23 | public void setToken(String token) { 24 | this.token = token; 25 | } 26 | 27 | public boolean isRegister() { 28 | return register; 29 | } 30 | 31 | public void setRegister(boolean register) { 32 | this.register = register; 33 | } 34 | 35 | public String getUserName() { 36 | return userName; 37 | } 38 | 39 | public void setUserName(String userName) { 40 | this.userName = userName; 41 | } 42 | 43 | public String getPortrait() { 44 | return portrait; 45 | } 46 | 47 | public void setPortrait(String portrait) { 48 | this.portrait = portrait; 49 | } 50 | 51 | public String getResetCode() { 52 | return resetCode; 53 | } 54 | 55 | public void setResetCode(String resetCode) { 56 | this.resetCode = resetCode; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/sms/TencentSMSConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.sms; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix="sms") 9 | @PropertySource(value = "file:config/tencent_sms.properties", encoding = "UTF-8") 10 | public class TencentSMSConfig { 11 | public String secretId; 12 | public String secretKey; 13 | public String appId; 14 | public String templateId; 15 | public String sign; 16 | 17 | public String getSecretId() { 18 | return secretId; 19 | } 20 | 21 | public void setSecretId(String secretId) { 22 | this.secretId = secretId; 23 | } 24 | 25 | public String getSecretKey() { 26 | return secretKey; 27 | } 28 | 29 | public void setSecretKey(String secretKey) { 30 | this.secretKey = secretKey; 31 | } 32 | 33 | public String getAppId() { 34 | return appId; 35 | } 36 | 37 | public void setAppId(String appId) { 38 | this.appId = appId; 39 | } 40 | 41 | public String getTemplateId() { 42 | return templateId; 43 | } 44 | 45 | public void setTemplateId(String templateId) { 46 | this.templateId = templateId; 47 | } 48 | 49 | public String getSign() { 50 | return sign; 51 | } 52 | 53 | public void setSign(String sign) { 54 | this.sign = sign; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/NumericIdGenerator.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | public class NumericIdGenerator { 8 | public static String getId(List firstNumber, List firstExceptNumber, int idLength) { 9 | List numbers = new ArrayList<>(); 10 | if(firstNumber != null && !firstNumber.isEmpty()) { 11 | numbers.addAll(firstNumber); 12 | } else { 13 | for (int i = 0; i <= 9; i++) { 14 | numbers.add(i); 15 | } 16 | } 17 | 18 | if(firstExceptNumber != null && !firstExceptNumber.isEmpty()) { 19 | numbers.removeAll(firstExceptNumber); 20 | } 21 | 22 | numbers.remove((Integer)4); 23 | 24 | StringBuilder sb = new StringBuilder(); 25 | for (int i = 0; i < idLength; i++) { 26 | if(i == 0 && !numbers.isEmpty()) { 27 | sb.append(numbers.get((int)(Math.random() * numbers.size()))); 28 | } else { 29 | int n; 30 | do { 31 | n = (int)(Math.random()*10); 32 | } while (n == 4); 33 | sb.append(n); 34 | } 35 | } 36 | return sb.toString(); 37 | } 38 | public static void main(String[] args) { 39 | for (int i = 0; i < 100; i++) { 40 | String id = getId(null, Arrays.asList(0), 6); 41 | System.out.println(id); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/ShiroSessionManager.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | 4 | import com.aliyuncs.utils.StringUtils; 5 | import org.apache.shiro.web.servlet.ShiroHttpServletRequest; 6 | import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; 7 | import org.apache.shiro.web.util.WebUtils; 8 | 9 | import javax.servlet.ServletRequest; 10 | import javax.servlet.ServletResponse; 11 | import java.io.Serializable; 12 | 13 | public class ShiroSessionManager extends DefaultWebSessionManager { 14 | 15 | private static final String AUTHORIZATION = "authToken"; 16 | 17 | private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; 18 | 19 | public ShiroSessionManager(){ 20 | super(); 21 | } 22 | 23 | @Override 24 | protected Serializable getSessionId(ServletRequest request, ServletResponse response){ 25 | String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION); 26 | if(StringUtils.isEmpty(id)){ 27 | //如果没有携带id参数则按照父类的方式在cookie进行获取 28 | System.out.println("super:"+super.getSessionId(request, response)); 29 | return super.getSessionId(request, response); 30 | }else{ 31 | //如果请求头中有 authToken 则其值为sessionId 32 | request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE); 33 | request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id); 34 | request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE); 35 | return id; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/Application.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | import cn.wildfirechat.app.jpa.PCSessionRepository; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.web.servlet.MultipartConfigFactory; 8 | import org.springframework.boot.web.servlet.ServletComponentScan; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.scheduling.annotation.EnableScheduling; 11 | import org.springframework.scheduling.annotation.Scheduled; 12 | import org.springframework.util.unit.DataSize; 13 | 14 | import javax.servlet.MultipartConfigElement; 15 | 16 | @SpringBootApplication 17 | @ServletComponentScan 18 | @EnableScheduling 19 | public class Application { 20 | @Autowired 21 | private PCSessionRepository pcSessionRepository; 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(Application.class, args); 25 | } 26 | 27 | 28 | /** 29 | * 文件上传配置 30 | * @return 31 | */ 32 | @Bean 33 | public MultipartConfigElement multipartConfigElement() { 34 | MultipartConfigFactory factory = new MultipartConfigFactory(); 35 | //单个文件最大 36 | factory.setMaxFileSize(DataSize.ofMegabytes(20)); //20MB 37 | /// 设置总上传数据总大小 38 | factory.setMaxRequestSize(DataSize.ofMegabytes(100)); 39 | return factory.createMultipartConfig(); 40 | } 41 | 42 | @Scheduled(fixedRate = 60 * 60 * 1000) 43 | public void clearPCSession(){ 44 | pcSessionRepository.deleteByCreateDtBefore(System.currentTimeMillis() - 60 * 60 * 1000); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/TokenMatcher.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | import cn.wildfirechat.app.RestResult; 4 | import cn.wildfirechat.pojos.InputOutputUserInfo; 5 | import cn.wildfirechat.sdk.UserAdmin; 6 | import cn.wildfirechat.sdk.model.IMResult; 7 | import cn.wildfirechat.sdk.utilities.AdminHttpUtils; 8 | import org.apache.shiro.authc.AuthenticationInfo; 9 | import org.apache.shiro.authc.AuthenticationToken; 10 | import org.apache.shiro.authc.credential.CredentialsMatcher; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Service; 13 | 14 | @Service 15 | public class TokenMatcher implements CredentialsMatcher { 16 | @Autowired 17 | private AuthDataSource authDataSource; 18 | 19 | @Override 20 | public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { 21 | if (token instanceof TokenAuthenticationToken) { 22 | TokenAuthenticationToken tt = (TokenAuthenticationToken)token; 23 | RestResult.RestCode restCode = authDataSource.checkPcSession(tt.getToken()); 24 | if (restCode == RestResult.RestCode.SUCCESS) { 25 | return true; 26 | } 27 | } 28 | return false; 29 | } 30 | 31 | public static void main(String[] args) { 32 | AdminHttpUtils.init("http://wildfirechat.cn:18080", "37923"); 33 | try { 34 | IMResult userByMobile = UserAdmin.getUserByMobile("13888888888"); 35 | System.out.println(userByMobile.msg); 36 | 37 | } catch (Exception e) { 38 | e.printStackTrace(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/ShortUUIDGenerator.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | import java.util.UUID; 8 | 9 | @Component 10 | public class ShortUUIDGenerator implements UserNameGenerator { 11 | public static String[] chars = new String[] { "0", "1", "2", "3", "4", "5", 12 | "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", 13 | "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", 14 | "W", "X", "Y", "Z" }; 15 | 16 | @Override 17 | public String getUserName(String phone) { 18 | return getShortUUID(); 19 | } 20 | 21 | public String getShortUUID() { 22 | StringBuffer shortBuffer = new StringBuffer(); 23 | String uuid = UUID.randomUUID().toString().replace("-", ""); 24 | for (int i = 0; i < 8; i++) { 25 | String str = uuid.substring(i * 4, i * 4 + 4); 26 | int x = Integer.parseInt(str, 16); 27 | shortBuffer.append(chars[x % chars.length]); 28 | } 29 | return shortBuffer.toString(); 30 | } 31 | public static void main(String[] args) { 32 | Set idSet = new HashSet<>(); 33 | ShortUUIDGenerator generator = new ShortUUIDGenerator(); 34 | 35 | int duplatedCount = 0; 36 | for (int i = 0; i < 1000000; i++) { 37 | String id = generator.getUserName(null); 38 | if(!idSet.add(id)) { 39 | System.out.println("Duplated id of " + id); 40 | duplatedCount++; 41 | } 42 | } 43 | 44 | System.out.println("Duplated id count is " + duplatedCount); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 wildfirechat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | 1. ikidou/TypeBuilder 25 | Copyright 2016 ikidou 26 | 27 | Licensed under the Apache License, Version 2.0 (the "License"); 28 | you may not use this file except in compliance with the License. 29 | You may obtain a copy of the License at 30 | 31 | http://www.apache.org/licenses/LICENSE-2.0 32 | 33 | Unless required by applicable law or agreed to in writing, software 34 | distributed under the License is distributed on an "AS IS" BASIS, 35 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 36 | See the License for the specific language governing permissions and 37 | limitations under the License. 38 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/pojo/SessionOutput.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.pojo; 2 | 3 | public class SessionOutput { 4 | private String token; 5 | private int status; 6 | private long expired; 7 | private int platform; 8 | private String device_name; 9 | private String userId; 10 | 11 | public String getUserId() { 12 | return userId; 13 | } 14 | 15 | public void setUserId(String userId) { 16 | this.userId = userId; 17 | } 18 | 19 | public SessionOutput() { 20 | } 21 | 22 | public SessionOutput(String userId, String token, int status, long expired, String device_name, int platform) { 23 | this.userId = userId; 24 | this.token = token; 25 | this.status = status; 26 | this.expired = expired; 27 | this.device_name = device_name; 28 | this.platform = platform; 29 | } 30 | 31 | public String getToken() { 32 | return token; 33 | } 34 | 35 | public void setToken(String token) { 36 | this.token = token; 37 | } 38 | 39 | public int getStatus() { 40 | return status; 41 | } 42 | 43 | public void setStatus(int status) { 44 | this.status = status; 45 | } 46 | 47 | public long getExpired() { 48 | return expired; 49 | } 50 | 51 | public void setExpired(long expired) { 52 | this.expired = expired; 53 | } 54 | 55 | public String getDevice_name() { 56 | return device_name; 57 | } 58 | 59 | public void setDevice_name(String device_name) { 60 | this.device_name = device_name; 61 | } 62 | 63 | public int getPlatform() { 64 | return platform; 65 | } 66 | 67 | public void setPlatform(int platform) { 68 | this.platform = platform; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/Utils.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | import java.nio.file.Paths; 4 | import java.util.Random; 5 | import java.util.UUID; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | public class Utils { 10 | public static String getRandomCode(int length) { 11 | StringBuilder sb = new StringBuilder(); 12 | for (int i = 0; i < length; i++) { 13 | sb.append(((int)(Math.random()*100))%10); 14 | } 15 | return sb.toString(); 16 | } 17 | public static boolean isMobile(String mobile) { 18 | boolean flag = false; 19 | try { 20 | Pattern p = Pattern.compile("^(1[3-9][0-9])\\d{8}$"); 21 | Matcher m = p.matcher(mobile); 22 | flag = m.matches(); 23 | } catch (Exception e) { 24 | flag = false; 25 | } 26 | return flag; 27 | } 28 | 29 | public static String getSafeFileName(String fileName) { 30 | if (fileName == null || fileName.isEmpty()) { 31 | return UUID.randomUUID().toString(); 32 | } 33 | 34 | // 使用 Paths.get 解析文件名 35 | try { 36 | String newName = Paths.get(fileName).getFileName().toString(); 37 | if(!newName.isEmpty()) { 38 | return newName; 39 | } 40 | } catch (Exception e) { 41 | // 处理解析异常 42 | e.printStackTrace(); 43 | } 44 | return UUID.randomUUID().toString(); 45 | } 46 | 47 | public static void main(String[] args) { 48 | String filename1 = "/aa../../../hello.txt"; 49 | String filename2 = "..\\..\\1.txt"; 50 | System.out.println(getSafeFileName(filename1)); 51 | System.out.println(getSafeFileName(filename2)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/PhoneCodeRealm.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | 4 | import cn.wildfirechat.app.RestResult; 5 | import org.apache.shiro.authc.*; 6 | import org.apache.shiro.authz.AuthorizationInfo; 7 | import org.apache.shiro.authz.SimpleAuthorizationInfo; 8 | import org.apache.shiro.realm.AuthorizingRealm; 9 | import org.apache.shiro.subject.PrincipalCollection; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | import javax.annotation.PostConstruct; 14 | 15 | @Service 16 | public class PhoneCodeRealm extends AuthorizingRealm { 17 | 18 | @Autowired 19 | AuthDataSource authDataSource; 20 | 21 | @PostConstruct 22 | void initRealm() { 23 | setAuthenticationTokenClass(PhoneCodeToken.class); 24 | } 25 | 26 | @Override 27 | protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 28 | SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 29 | // Set stringSet = new HashSet<>(); 30 | // stringSet.add("user:show"); 31 | // stringSet.add("user:admin"); 32 | // info.setStringPermissions(stringSet); 33 | return info; 34 | } 35 | 36 | @Override 37 | protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { 38 | if (authenticationToken instanceof PhoneCodeToken) { 39 | String mobile = (String) authenticationToken.getPrincipal(); 40 | String code = (String)authenticationToken.getCredentials(); 41 | RestResult.RestCode restCode = authDataSource.verifyCode(mobile, code); 42 | if (restCode == RestResult.RestCode.SUCCESS) { 43 | return new SimpleAuthenticationInfo(mobile, code.getBytes(), getName()); 44 | } 45 | } 46 | 47 | throw new AuthenticationException("没发送验证码或者验证码过期"); 48 | } 49 | } -------------------------------------------------------------------------------- /systemd/README.md: -------------------------------------------------------------------------------- 1 | # Linux Service 方式运行 2 | 除了命令行方式直接执行APP服务外,还可以以linux systemd service方式来运行,注意以这种方式运行,APP服务的配置还是需要按照常规方法来配置。 3 | 4 | ## 获取软件包 5 | 下载野火release或则会自己源码编译,得到```app-${version}.jar```、```app-${version}.deb```和```app-${version}.rpm```。 6 | 7 | ## 手动部署 8 | ### 依赖 9 | 野火IM依赖JRE1.8手动部署需要手动安装JRE1.8,确保命令:```java -version```能看到正确的java版本信息才行。 10 | 11 | ### 部署软件包 12 | 创建```/opt/app-server```目录,把Jar包```app-${version}.jar```改名为```app-server.jar```;把config目录也拷贝到```/opt/app-server```目录下。 13 | 14 | ### 放置systemd server file 15 | 把```app-server.service```放到```/usr/lib/systemd/system/```目录下。 16 | 17 | ### 测试 18 | 根据下面管理服务的说明,启动服务,查看控制台日志,确认启动没有异常,服务器本地执行 ```curl -v http://127.0.0.1:8888``` 能够返回字符串```Ok```。 19 | 20 | ## 安装部署 21 | ### 依赖 22 | 安装包安装将会自动安装依赖,不需要手动安装java。如果服务器上有其他版本的Java,请注意可能的冲突问题。 23 | 24 | ### 部署软件包 25 | 可以直接安装```deb```和```rpm```格式的安装包,在debian系的linux系统(Ubuntu等使用```apt```命令安装软件的系统)中,使用命令: 26 | ```shell 27 | sudo apt install ./app-server-{version}.deb 28 | ``` 29 | 30 | 在红帽系的linux系统(Centos等使用```yum```命令安装软件的系统)中,使用命令: 31 | ```shell 32 | sudo yum install ./app-server-${version}.deb 33 | ``` 34 | 35 | 注意在上述两个命令中,都使用的是本地安装,注意安装包名前的```./```路径。如果使用```dpkg -i ./app-server-${version}.deb```命令将不会安装依赖。 36 | 37 | ### 测试 38 | 根据下面管理服务的说明,启动服务,查看控制台日志,确认启动没有异常,服务器本地执行 ```curl -v http://127.0.0.1:8888``` 能够返回字符串```Ok```。 39 | 40 | 41 | ## 管理服务 42 | * 刷新配置,当安装或者更新后需要执行: ```sudo systemctl daemon-reload``` 43 | * 启动服务: ```sudo systemctl start app-server``` 44 | * 停止服务: ```sudo systemctl stop app-server``` 45 | * 重启服务: ```sudo systemctl restart app-server``` 46 | * 查看服务状态:```sudo systemctl status app-server``` 47 | * 设置开机自启动:```sudo systemctl enable app-server``` 48 | * 禁止开机自启动:```sudo systemctl disable app-server``` 49 | * 查看控制台日志: ```journalctl -f -u app-server``` 50 | 51 | ## 日志 52 | 日志主要看制台日志。如果需要看日志,请使用命令```journalctl -f -u app-server```来查看日志。 53 | 54 | ## 配置 55 | 需要对APP服务配置来达到最好的执行效果,配置文件在````/opt/app-server/config````目录下。另外还可以设置服务的内存大小,修改```/usr/lib/systemd/system/app-server```文件,在java命令中添加```-Xmx```参数。 56 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/ScanCodeRealm.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | 4 | import cn.wildfirechat.app.jpa.PCSession; 5 | import org.apache.shiro.authc.*; 6 | import org.apache.shiro.authz.AuthorizationInfo; 7 | import org.apache.shiro.authz.SimpleAuthorizationInfo; 8 | import org.apache.shiro.realm.AuthorizingRealm; 9 | import org.apache.shiro.subject.PrincipalCollection; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | import javax.annotation.PostConstruct; 14 | 15 | @Service 16 | public class ScanCodeRealm extends AuthorizingRealm { 17 | 18 | @Autowired 19 | AuthDataSource authDataSource; 20 | 21 | @Autowired 22 | TokenMatcher tokenMatcher; 23 | 24 | @PostConstruct 25 | private void initMatcher() { 26 | setCredentialsMatcher(tokenMatcher); 27 | } 28 | 29 | @Override 30 | protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 31 | SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 32 | // Set stringSet = new HashSet<>(); 33 | // stringSet.add("user:show"); 34 | // stringSet.add("user:admin"); 35 | // info.setStringPermissions(stringSet); 36 | return info; 37 | } 38 | 39 | @Override 40 | public boolean supports(AuthenticationToken token) { 41 | if (token instanceof TokenAuthenticationToken) 42 | return true; 43 | return super.supports(token); 44 | } 45 | 46 | @Override 47 | protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { 48 | String token = (String) authenticationToken.getPrincipal(); 49 | PCSession session = authDataSource.getSession(token, false); 50 | if (session == null) { 51 | throw new AuthenticationException("会话不存在"); 52 | } 53 | 54 | return new SimpleAuthenticationInfo(token, token, getName()); 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/Record.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Id; 6 | import javax.persistence.Table; 7 | 8 | @Entity 9 | @Table(name = "phone_code_record") 10 | public class Record { 11 | @Id 12 | @Column(length = 128) 13 | private String mobile; 14 | private String code; 15 | //发送时间,当小于1分钟不允许发送。 16 | private long timestamp; 17 | //计算时间段内发送次数的起始时间 18 | private long startTime; 19 | //startTime到现在的发送次数 20 | private int requestCount; 21 | 22 | public Record(String code, String mobile) { 23 | this.code = code; 24 | this.mobile = mobile; 25 | this.timestamp = 0; 26 | this.startTime = System.currentTimeMillis(); 27 | this.requestCount = 0; 28 | } 29 | 30 | public Record() { 31 | } 32 | 33 | public boolean increaseAndCheck() { 34 | long now = System.currentTimeMillis(); 35 | if (now - startTime > 86400000) { 36 | reset(); 37 | } 38 | requestCount++; 39 | if (requestCount > 10) { 40 | return false; 41 | } 42 | return true; 43 | } 44 | 45 | public void reset() { 46 | requestCount = 1; 47 | startTime = System.currentTimeMillis(); 48 | } 49 | 50 | public int getRequestCount() { 51 | return requestCount; 52 | } 53 | 54 | public String getCode() { 55 | return code; 56 | } 57 | 58 | public void setCode(String code) { 59 | this.code = code; 60 | } 61 | 62 | public String getMobile() { 63 | return mobile; 64 | } 65 | 66 | public long getTimestamp() { 67 | return timestamp; 68 | } 69 | 70 | public void setTimestamp(long timestamp) { 71 | this.timestamp = timestamp; 72 | } 73 | 74 | public long getStartTime() { 75 | return startTime; 76 | } 77 | 78 | public void setStartTime(long startTime) { 79 | this.startTime = startTime; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/JsonAuthLoginFilter.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | import cn.wildfirechat.app.RestResult; 4 | import com.google.gson.Gson; 5 | import org.apache.shiro.web.filter.AccessControlFilter; 6 | 7 | import org.apache.shiro.SecurityUtils; 8 | import org.apache.shiro.subject.Subject; 9 | 10 | import javax.servlet.ServletRequest; 11 | import javax.servlet.ServletResponse; 12 | import javax.servlet.http.HttpServletRequest; 13 | import java.io.IOException; 14 | import java.io.PrintWriter; 15 | 16 | public class JsonAuthLoginFilter extends AccessControlFilter { 17 | 18 | @Override 19 | protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) 20 | throws Exception { 21 | if (request instanceof HttpServletRequest) { 22 | if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) { 23 | return true; 24 | } 25 | } 26 | 27 | Subject subject = SecurityUtils.getSubject(); 28 | 29 | if(null != subject){ 30 | if(subject.isRemembered()){ 31 | return Boolean.TRUE; 32 | } 33 | if(subject.isAuthenticated()){ 34 | return Boolean.TRUE; 35 | } 36 | } 37 | 38 | return Boolean.FALSE ; 39 | } 40 | 41 | @Override 42 | protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { 43 | PrintWriter out = null; 44 | try { 45 | response.setCharacterEncoding("UTF-8"); 46 | response.setContentType("application/json; charset=utf-8"); 47 | out = response.getWriter(); 48 | 49 | out.write(new Gson().toJson(RestResult.error(RestResult.RestCode.ERROR_NOT_LOGIN))); 50 | } catch (IOException e) { 51 | e.printStackTrace(); 52 | } finally { 53 | if (out != null) { 54 | out.close(); 55 | } 56 | } 57 | return Boolean.FALSE ; 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/Service.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | 4 | import cn.wildfirechat.app.jpa.FavoriteItem; 5 | import cn.wildfirechat.app.pojo.*; 6 | import cn.wildfirechat.pojos.InputCreateDevice; 7 | import org.springframework.web.multipart.MultipartFile; 8 | 9 | import javax.servlet.http.HttpServletResponse; 10 | 11 | public interface Service { 12 | RestResult sendLoginCode(String mobile); 13 | RestResult sendResetCode(String mobile); 14 | RestResult loginWithMobileCode(HttpServletResponse response, String mobile, String code, String clientId, int platform); 15 | RestResult loginWithPassword(HttpServletResponse response, String mobile, String password, String clientId, int platform); 16 | RestResult changePassword(String oldPwd, String newPwd); 17 | RestResult resetPassword(String mobile, String resetCode, String newPwd); 18 | RestResult sendDestroyCode(); 19 | RestResult destroy(HttpServletResponse response, String code); 20 | 21 | RestResult createPcSession(CreateSessionRequest request); 22 | RestResult loginWithSession(String token); 23 | 24 | RestResult scanPc(String token); 25 | RestResult confirmPc(ConfirmSessionRequest request); 26 | RestResult cancelPc(CancelSessionRequest request); 27 | 28 | RestResult changeName(String newName); 29 | RestResult complain(String text); 30 | 31 | RestResult putGroupAnnouncement(GroupAnnouncementPojo request); 32 | RestResult getGroupAnnouncement(String groupId); 33 | 34 | RestResult saveUserLogs(String userId, MultipartFile file); 35 | 36 | RestResult addDevice(InputCreateDevice createDevice); 37 | RestResult getDeviceList(); 38 | RestResult delDevice(InputCreateDevice createDevice); 39 | 40 | RestResult sendUserMessage(SendMessageRequest request); 41 | RestResult uploadMedia(int mediaType, MultipartFile file); 42 | 43 | RestResult putFavoriteItem(FavoriteItem request); 44 | RestResult removeFavoriteItems(long id); 45 | RestResult getFavoriteItems(long id, int count); 46 | RestResult getGroupMembersForPortrait(String groupId); 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/LdapRealm.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | 4 | import cn.wildfirechat.app.jpa.UserPassword; 5 | import cn.wildfirechat.app.jpa.UserPasswordRepository; 6 | import org.apache.shiro.authc.*; 7 | import org.apache.shiro.authc.credential.Sha1CredentialsMatcher; 8 | import org.apache.shiro.authz.AuthorizationInfo; 9 | import org.apache.shiro.authz.SimpleAuthorizationInfo; 10 | import org.apache.shiro.realm.AuthorizingRealm; 11 | import org.apache.shiro.subject.PrincipalCollection; 12 | import org.apache.shiro.util.ByteSource; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | 16 | import javax.annotation.PostConstruct; 17 | import java.nio.charset.StandardCharsets; 18 | import java.util.Optional; 19 | 20 | @Service 21 | public class LdapRealm extends AuthorizingRealm { 22 | @Autowired 23 | private UserPasswordRepository userPasswordRepository; 24 | 25 | @PostConstruct 26 | private void initMatcher() { 27 | LdapMatcher matcher = new LdapMatcher(); 28 | setCredentialsMatcher(matcher); 29 | } 30 | @Override 31 | protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 32 | SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 33 | // Set stringSet = new HashSet<>(); 34 | // stringSet.add("user:show"); 35 | // stringSet.add("user:admin"); 36 | // info.setStringPermissions(stringSet); 37 | return info; 38 | } 39 | 40 | @Override 41 | public boolean supports(AuthenticationToken token) { 42 | if (token instanceof LdapToken) 43 | return true; 44 | return super.supports(token); 45 | } 46 | 47 | @Override 48 | protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { 49 | if (authenticationToken instanceof LdapToken) { 50 | String userId = (String) authenticationToken.getPrincipal(); 51 | return new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), authenticationToken.getCredentials(), getName()); 52 | } 53 | throw new AuthenticationException("没有密码"); 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/UserPassword.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Id; 6 | import javax.persistence.Table; 7 | 8 | @Entity 9 | @Table(name = "user_password") 10 | public class UserPassword { 11 | @Id 12 | @Column(length = 128) 13 | private String userId; 14 | 15 | private String password; 16 | 17 | private String salt; 18 | 19 | private String resetCode; 20 | 21 | private long resetCodeTime; 22 | 23 | private int tryCount; 24 | 25 | private long lastTryTime; 26 | 27 | public UserPassword() { 28 | } 29 | 30 | public UserPassword(String userId) { 31 | this.userId = userId; 32 | } 33 | 34 | public UserPassword(String userId, String password, String salt) { 35 | this.userId = userId; 36 | this.password = password; 37 | this.salt = salt; 38 | this.resetCodeTime = 0; 39 | this.tryCount = 0; 40 | this.lastTryTime = 0; 41 | } 42 | 43 | public UserPassword(String userId, String password, String salt, String resetCode, long resetCodeTime) { 44 | this.userId = userId; 45 | this.password = password; 46 | this.salt = salt; 47 | this.resetCode = resetCode; 48 | this.resetCodeTime = resetCodeTime; 49 | this.tryCount = 0; 50 | this.lastTryTime = 0; 51 | } 52 | 53 | public String getUserId() { 54 | return userId; 55 | } 56 | 57 | public void setUserId(String userId) { 58 | this.userId = userId; 59 | } 60 | 61 | public String getPassword() { 62 | return password; 63 | } 64 | 65 | public void setPassword(String password) { 66 | this.password = password; 67 | } 68 | 69 | public String getSalt() { 70 | return salt; 71 | } 72 | 73 | public void setSalt(String salt) { 74 | this.salt = salt; 75 | } 76 | 77 | public String getResetCode() { 78 | return resetCode; 79 | } 80 | 81 | public void setResetCode(String resetCode) { 82 | this.resetCode = resetCode; 83 | } 84 | 85 | public long getResetCodeTime() { 86 | return resetCodeTime; 87 | } 88 | 89 | public void setResetCodeTime(long resetCodeTime) { 90 | this.resetCodeTime = resetCodeTime; 91 | } 92 | 93 | public int getTryCount() { 94 | return tryCount; 95 | } 96 | 97 | public void setTryCount(int tryCount) { 98 | this.tryCount = tryCount; 99 | } 100 | 101 | public long getLastTryTime() { 102 | return lastTryTime; 103 | } 104 | 105 | public void setLastTryTime(long lastTryTime) { 106 | this.lastTryTime = lastTryTime; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/UserPasswordRealm.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | 4 | import cn.wildfirechat.app.RestResult; 5 | import cn.wildfirechat.app.jpa.ShiroSession; 6 | import cn.wildfirechat.app.jpa.UserPassword; 7 | import cn.wildfirechat.app.jpa.UserPasswordRepository; 8 | import org.apache.shiro.authc.*; 9 | import org.apache.shiro.authc.credential.HashedCredentialsMatcher; 10 | import org.apache.shiro.authc.credential.Sha1CredentialsMatcher; 11 | import org.apache.shiro.authz.AuthorizationInfo; 12 | import org.apache.shiro.authz.SimpleAuthorizationInfo; 13 | import org.apache.shiro.crypto.hash.Sha1Hash; 14 | import org.apache.shiro.realm.AuthorizingRealm; 15 | import org.apache.shiro.subject.PrincipalCollection; 16 | import org.apache.shiro.util.ByteSource; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.stereotype.Service; 19 | 20 | import javax.annotation.PostConstruct; 21 | import java.nio.charset.StandardCharsets; 22 | import java.util.Optional; 23 | 24 | @Service 25 | public class UserPasswordRealm extends AuthorizingRealm { 26 | @Autowired 27 | private UserPasswordRepository userPasswordRepository; 28 | 29 | @PostConstruct 30 | private void initMatcher() { 31 | Sha1CredentialsMatcher matcher = new Sha1CredentialsMatcher(); 32 | matcher.setStoredCredentialsHexEncoded(false); 33 | setCredentialsMatcher(matcher); 34 | } 35 | @Override 36 | protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 37 | SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 38 | // Set stringSet = new HashSet<>(); 39 | // stringSet.add("user:show"); 40 | // stringSet.add("user:admin"); 41 | // info.setStringPermissions(stringSet); 42 | return info; 43 | } 44 | 45 | @Override 46 | protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { 47 | if (authenticationToken instanceof UsernamePasswordToken) { 48 | String userId = (String) authenticationToken.getPrincipal(); 49 | Optional optional = userPasswordRepository.findById(userId); 50 | if (optional.isPresent()) { 51 | UserPassword up = optional.get(); 52 | return new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), up.getPassword(), ByteSource.Util.bytes(up.getSalt().getBytes(StandardCharsets.UTF_8)), getName()); 53 | } 54 | } 55 | 56 | throw new AuthenticationException("没有密码"); 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/PCSession.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import cn.wildfirechat.app.pojo.SessionOutput; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | 10 | @Entity 11 | @Table(name = "pc_session") 12 | public class PCSession { 13 | public interface PCSessionStatus { 14 | int Session_Created = 0; 15 | int Session_Scanned = 1; 16 | int Session_Pre_Verify = 3; 17 | int Session_Verified = 2; 18 | int Session_Canceled = 4; 19 | } 20 | 21 | @Id 22 | @Column(length = 128) 23 | private String token; 24 | private String clientId; 25 | private long createDt; 26 | private long duration; 27 | //PCSessionStatus 28 | private int status; 29 | private String confirmedUserId; 30 | private String device_name; 31 | private int platform; 32 | 33 | public int getPlatform() { 34 | return platform; 35 | } 36 | 37 | public void setPlatform(int platform) { 38 | this.platform = platform; 39 | } 40 | 41 | public String getToken() { 42 | return token; 43 | } 44 | 45 | public void setToken(String token) { 46 | this.token = token; 47 | } 48 | 49 | public String getClientId() { 50 | return clientId; 51 | } 52 | 53 | public void setClientId(String clientId) { 54 | this.clientId = clientId; 55 | } 56 | 57 | public long getCreateDt() { 58 | return createDt; 59 | } 60 | 61 | public void setCreateDt(long createDt) { 62 | this.createDt = createDt; 63 | } 64 | 65 | public long getDuration() { 66 | return duration; 67 | } 68 | 69 | public void setDuration(long duration) { 70 | this.duration = duration; 71 | } 72 | 73 | public int getStatus() { 74 | return status; 75 | } 76 | 77 | public void setStatus(int status) { 78 | this.status = status; 79 | } 80 | 81 | public String getConfirmedUserId() { 82 | return confirmedUserId; 83 | } 84 | 85 | public void setConfirmedUserId(String confirmedUserId) { 86 | this.confirmedUserId = confirmedUserId; 87 | } 88 | 89 | public String getDevice_name() { 90 | return device_name; 91 | } 92 | 93 | public void setDevice_name(String device_name) { 94 | this.device_name = device_name; 95 | } 96 | 97 | public SessionOutput toOutput() { 98 | return new SessionOutput(confirmedUserId, token, status, duration - (System.currentTimeMillis() - createDt), device_name, platform); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/avatar/NameAvatarBuilder.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.avatar; 2 | 3 | import org.springframework.core.io.ClassPathResource; 4 | 5 | import java.awt.*; 6 | import java.awt.image.BufferedImage; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | 11 | import javax.imageio.ImageIO; 12 | 13 | public class NameAvatarBuilder { 14 | 15 | private BufferedImage templateImage; 16 | private Graphics2D templateG2D; 17 | private int templateWidth; 18 | private int templateHeight; 19 | 20 | private String fullName; 21 | 22 | private static volatile Font font; 23 | 24 | public NameAvatarBuilder(String bgRGB) { 25 | templateImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); 26 | templateG2D = templateImage.createGraphics(); 27 | templateWidth = templateImage.getWidth(); 28 | templateHeight = templateImage.getHeight(); 29 | templateG2D.setBackground(Color.decode(bgRGB)); 30 | templateG2D.clearRect(0, 0, templateWidth, templateHeight); 31 | } 32 | 33 | public NameAvatarBuilder name(String drawName, String fullName) { 34 | this.fullName = fullName; 35 | // Get the FontMetrics 36 | // 加载自定义字体 37 | if (font == null) { 38 | try (InputStream inputStream = new ClassPathResource("fonts/simhei.ttf").getInputStream()) { 39 | // 加载自定义字体 40 | Font customFont = Font.createFont(Font.TRUETYPE_FONT, inputStream); 41 | // 设置字体样式 42 | font = customFont.deriveFont(Font.PLAIN, 40); 43 | } catch (IOException | FontFormatException e) { 44 | e.printStackTrace(); 45 | } 46 | } 47 | 48 | FontMetrics metrics = templateG2D.getFontMetrics(font); 49 | // Determine the X coordinate for the text 50 | int x = (templateWidth - metrics.stringWidth(drawName)) / 2; 51 | // Determine the Y coordinate for the text (note we add the ascent, as in java 2d 0 is top of the screen) 52 | int y = ((templateHeight - metrics.getHeight()) / 2) + metrics.getAscent(); 53 | // Set the font 54 | templateG2D.setFont(font); 55 | // Draw the String 56 | templateG2D.drawString(drawName, x, y); 57 | return this; 58 | } 59 | 60 | public File build() { 61 | templateG2D.dispose(); 62 | templateImage.flush(); 63 | File file = new File(AvatarServiceImpl.AVATAR_DIR, this.fullName.hashCode() + ".png"); 64 | try { 65 | ImageIO.write(templateImage, "png", file); 66 | } catch (IOException e) { 67 | e.printStackTrace(); 68 | } 69 | // System.gc(); 70 | return file; 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/RestResult.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | public class RestResult { 4 | public enum RestCode { 5 | SUCCESS(0, "success"), 6 | ERROR_INVALID_MOBILE(1, "无效的电话号码"), 7 | ERROR_SEND_SMS_OVER_FREQUENCY(3, "请求验证码太频繁"), 8 | ERROR_SERVER_ERROR(4, "服务器异常"), 9 | ERROR_CODE_EXPIRED(5, "验证码已过期"), 10 | ERROR_CODE_INCORRECT(6, "验证码或密码错误"), 11 | ERROR_SERVER_CONFIG_ERROR(7, "服务器配置错误"), 12 | ERROR_SESSION_EXPIRED(8, "会话不存在或已过期"), 13 | ERROR_SESSION_NOT_VERIFIED(9, "会话没有验证"), 14 | ERROR_SESSION_NOT_SCANED(10, "会话没有被扫码"), 15 | ERROR_SERVER_NOT_IMPLEMENT(11, "功能没有实现"), 16 | ERROR_GROUP_ANNOUNCEMENT_NOT_EXIST(12, "群公告不存在"), 17 | ERROR_NOT_LOGIN(13, "没有登录"), 18 | ERROR_NO_RIGHT(14, "没有权限"), 19 | ERROR_INVALID_PARAMETER(15, "无效参数"), 20 | ERROR_NOT_EXIST(16, "对象不存在"), 21 | ERROR_USER_NAME_ALREADY_EXIST(17, "用户名已经存在"), 22 | ERROR_SESSION_CANCELED(18, "会话已经取消"), 23 | ERROR_PASSWORD_INCORRECT(19, "密码错误"), 24 | ERROR_FAILURE_TOO_MUCH_TIMES(20, "密码错误次数太多,请等5分钟再试试"), 25 | ERROR_USER_FORBIDDEN(21, "用户被封禁"); 26 | public int code; 27 | public String msg; 28 | 29 | RestCode(int code, String msg) { 30 | this.code = code; 31 | this.msg = msg; 32 | } 33 | 34 | } 35 | private int code; 36 | private String message; 37 | private Object result; 38 | 39 | public static RestResult ok() { 40 | return new RestResult(RestCode.SUCCESS, null); 41 | } 42 | 43 | public static RestResult ok(Object object) { 44 | return new RestResult(RestCode.SUCCESS, object); 45 | } 46 | 47 | public static RestResult error(RestCode code) { 48 | return new RestResult(code, null); 49 | } 50 | 51 | public static RestResult result(RestCode code, Object object){ 52 | return new RestResult(code, object); 53 | } 54 | 55 | public static RestResult result(int code, String message, Object object){ 56 | RestResult r = new RestResult(RestCode.SUCCESS, object); 57 | r.code = code; 58 | r.message = message; 59 | return r; 60 | } 61 | 62 | private RestResult(RestCode code, Object result) { 63 | this.code = code.code; 64 | this.message = code.msg; 65 | this.result = result; 66 | } 67 | 68 | public int getCode() { 69 | return code; 70 | } 71 | 72 | public void setCode(int code) { 73 | this.code = code; 74 | } 75 | 76 | public String getMessage() { 77 | return message; 78 | } 79 | 80 | public void setMessage(String message) { 81 | this.message = message; 82 | } 83 | 84 | public Object getResult() { 85 | return result; 86 | } 87 | 88 | public void setResult(Object result) { 89 | this.result = result; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/LdapMatcher.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | import cn.wildfirechat.app.tools.LdapUser; 4 | import org.apache.shiro.authc.AuthenticationInfo; 5 | import org.apache.shiro.authc.AuthenticationToken; 6 | import org.apache.shiro.authc.credential.CredentialsMatcher; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.stereotype.Service; 9 | 10 | import javax.naming.AuthenticationException; 11 | import javax.naming.Context; 12 | import javax.naming.NamingEnumeration; 13 | import javax.naming.NamingException; 14 | import javax.naming.directory.*; 15 | import java.util.ArrayList; 16 | import java.util.Hashtable; 17 | import java.util.List; 18 | 19 | @Service 20 | public class LdapMatcher implements CredentialsMatcher { 21 | 22 | @Override 23 | public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { 24 | if (token instanceof LdapToken) { 25 | try { 26 | LdapToken tt = (LdapToken)token; 27 | Hashtable env = new Hashtable<>(); 28 | env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); 29 | env.put(Context.PROVIDER_URL, tt.getLdapUrl()); 30 | env.put(Context.SECURITY_AUTHENTICATION, "simple"); 31 | env.put(Context.SECURITY_PRINCIPAL, tt.getPrincipal().toString()); 32 | env.put(Context.SECURITY_CREDENTIALS, tt.getCredentials().toString()); 33 | new InitialDirContext(env).close(); // 能 bind 就算成功 34 | return true; 35 | } catch (NamingException e) { 36 | e.printStackTrace(); 37 | return false; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | 44 | 45 | /* --------- 验证入口 --------- */ 46 | public static boolean authenticate(String ldapUrl, String dn, String password) { 47 | try { 48 | Hashtable env = new Hashtable<>(); 49 | env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); 50 | env.put(Context.PROVIDER_URL, ldapUrl); 51 | env.put(Context.SECURITY_AUTHENTICATION, "simple"); 52 | env.put(Context.SECURITY_PRINCIPAL, dn); 53 | env.put(Context.SECURITY_CREDENTIALS, password); 54 | 55 | /* 成功 bind 立即关闭 */ 56 | new InitialDirContext(env).close(); 57 | return true; 58 | } catch (AuthenticationException e) { 59 | // 密码错或账号不存在 60 | return false; 61 | } catch (NamingException e) { 62 | throw new RuntimeException("LDAP 异常", e); 63 | } 64 | } 65 | 66 | private static final String LDAP_URL = "ldap://192.168.1.48:389"; // 换成你的 LDAP 地址 67 | private static final String USER_DN = "uid=user6,ou=people,dc=wildfirechat,dc=net"; 68 | 69 | public static void main(String[] args) { 70 | boolean ok = authenticate(LDAP_URL, USER_DN, "123456"); // 与条目里明文一致 71 | System.out.println(ok ? "登录成功" : "用户名或密码错误"); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/jpa/ConferenceEntity.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.jpa; 2 | 3 | import javax.persistence.*; 4 | 5 | @Entity 6 | @Table(name = "conference") 7 | public class ConferenceEntity { 8 | @Id 9 | @Column(length = 12) 10 | public String id; 11 | public String conferenceTitle; 12 | public String password; 13 | public String pin; 14 | public String owner; 15 | public String manages; 16 | public long startTime; 17 | public long endTime; 18 | public boolean audience; 19 | public boolean advance; 20 | public boolean allowSwitchMode; 21 | public boolean noJoinBeforeStart; 22 | public boolean recording; 23 | public String focus; 24 | public int maxParticipants; 25 | 26 | public String getId() { 27 | return id; 28 | } 29 | 30 | public void setId(String id) { 31 | this.id = id; 32 | } 33 | 34 | public String getConferenceTitle() { 35 | return conferenceTitle; 36 | } 37 | 38 | public void setConferenceTitle(String conferenceTitle) { 39 | this.conferenceTitle = conferenceTitle; 40 | } 41 | 42 | public String getPassword() { 43 | return password; 44 | } 45 | 46 | public void setPassword(String password) { 47 | this.password = password; 48 | } 49 | 50 | public String getPin() { 51 | return pin; 52 | } 53 | 54 | public void setPin(String pin) { 55 | this.pin = pin; 56 | } 57 | 58 | public String getOwner() { 59 | return owner; 60 | } 61 | 62 | public void setOwner(String owner) { 63 | this.owner = owner; 64 | } 65 | 66 | public long getStartTime() { 67 | return startTime; 68 | } 69 | 70 | public void setStartTime(long startTime) { 71 | this.startTime = startTime; 72 | } 73 | 74 | public long getEndTime() { 75 | return endTime; 76 | } 77 | 78 | public void setEndTime(long endTime) { 79 | this.endTime = endTime; 80 | } 81 | 82 | public boolean isAudience() { 83 | return audience; 84 | } 85 | 86 | public void setAudience(boolean audience) { 87 | this.audience = audience; 88 | } 89 | 90 | public boolean isAdvance() { 91 | return advance; 92 | } 93 | 94 | public void setAdvance(boolean advance) { 95 | this.advance = advance; 96 | } 97 | 98 | public boolean isAllowSwitchMode() { 99 | return allowSwitchMode; 100 | } 101 | 102 | public void setAllowSwitchMode(boolean allowSwitchMode) { 103 | this.allowSwitchMode = allowSwitchMode; 104 | } 105 | 106 | public boolean isNoJoinBeforeStart() { 107 | return noJoinBeforeStart; 108 | } 109 | 110 | public void setNoJoinBeforeStart(boolean noJoinBeforeStart) { 111 | this.noJoinBeforeStart = noJoinBeforeStart; 112 | } 113 | 114 | public boolean isRecording() { 115 | return recording; 116 | } 117 | 118 | public void setRecording(boolean recording) { 119 | this.recording = recording; 120 | } 121 | 122 | public String getManages() { 123 | return manages; 124 | } 125 | 126 | public void setManages(String manages) { 127 | this.manages = manages; 128 | } 129 | 130 | public String getFocus() { 131 | return focus; 132 | } 133 | 134 | public void setFocus(String focus) { 135 | this.focus = focus; 136 | } 137 | 138 | public int getMaxParticipants() { 139 | return maxParticipants; 140 | } 141 | 142 | public void setMaxParticipants(int maxParticipants) { 143 | this.maxParticipants = maxParticipants; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/DBSessionDao.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | import cn.wildfirechat.app.jpa.ShiroSession; 4 | import cn.wildfirechat.app.jpa.ShiroSessionRepository; 5 | import com.google.gson.Gson; 6 | import org.apache.shiro.session.Session; 7 | import org.apache.shiro.session.UnknownSessionException; 8 | import org.apache.shiro.session.mgt.SimpleSession; 9 | import org.apache.shiro.session.mgt.eis.SessionDAO; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.io.*; 14 | import java.util.Collection; 15 | import java.util.Map; 16 | import java.util.UUID; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | 19 | @Component 20 | public class DBSessionDao implements SessionDAO { 21 | private Map sessionMap = new ConcurrentHashMap<>(); 22 | 23 | @Autowired 24 | private ShiroSessionRepository shiroSessionRepository; 25 | 26 | @Override 27 | public Serializable create(Session session) { 28 | String sessionId = UUID.randomUUID().toString().replaceAll("-", ""); 29 | ((SimpleSession) session).setId(sessionId); 30 | return sessionId; 31 | } 32 | 33 | @Override 34 | public Session readSession(Serializable sessionId) throws UnknownSessionException { 35 | // return sessionMap.get(sessionId); 36 | ShiroSession shiroSession = shiroSessionRepository.findById((String) sessionId).orElse(null); 37 | if (shiroSession != null) { 38 | Session session = byteToSession(shiroSession.getSessionData()); 39 | return session; 40 | } 41 | return null; 42 | } 43 | 44 | @Override 45 | public void update(Session session) throws UnknownSessionException { 46 | byte[] bb = sessionToByte(session); 47 | ShiroSession shiroSession = new ShiroSession((String)session.getId(), bb); 48 | // sessionMap.put(session.getId(), session); 49 | shiroSessionRepository.save(shiroSession); 50 | } 51 | 52 | @Override 53 | public void delete(Session session) { 54 | sessionMap.remove(session.getId()); 55 | } 56 | 57 | @Override 58 | public Collection getActiveSessions() { 59 | return sessionMap.values(); 60 | } 61 | 62 | // convert session object to byte, then store it to redis 63 | private byte[] sessionToByte(Session session){ 64 | ByteArrayOutputStream bo = new ByteArrayOutputStream(); 65 | byte[] bytes = null; 66 | try { 67 | ObjectOutputStream oo = new ObjectOutputStream(bo); 68 | oo.writeObject(session); 69 | bytes = bo.toByteArray(); 70 | } catch (IOException e) { 71 | e.printStackTrace(); 72 | } 73 | return bytes; 74 | } 75 | 76 | // restore session 77 | private Session byteToSession(byte[] bytes){ 78 | ByteArrayInputStream bi = new ByteArrayInputStream(bytes); 79 | ObjectInputStream in; 80 | SimpleSession session = null; 81 | try { 82 | in = new ObjectInputStream(bi); 83 | session = (SimpleSession) in.readObject(); 84 | } catch (ClassNotFoundException e) { 85 | e.printStackTrace(); 86 | } catch (IOException e) { 87 | e.printStackTrace(); 88 | } 89 | 90 | return session; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/conference/ConferenceController.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.conference; 2 | 3 | import cn.wildfirechat.app.Service; 4 | import cn.wildfirechat.app.pojo.*; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.io.IOException; 11 | 12 | @RestController 13 | public class ConferenceController { 14 | private static final Logger LOG = LoggerFactory.getLogger(ConferenceController.class); 15 | @Autowired 16 | private ConferenceService mService; 17 | 18 | @CrossOrigin 19 | @PostMapping(value = "/conference/get_id/{userId}") 20 | public Object getUserConferenceId(@PathVariable("userId") String userId) throws IOException { 21 | return mService.getUserConferenceId(userId); 22 | } 23 | 24 | @CrossOrigin 25 | @PostMapping(value = "/conference/get_my_id") 26 | public Object getMyConferenceId() throws IOException { 27 | return mService.getMyConferenceId(); 28 | } 29 | 30 | @CrossOrigin 31 | @PostMapping(value = "/conference/info") 32 | public Object getConferenceInfo(@RequestBody ConferenceInfoRequest request) throws IOException { 33 | return mService.getConferenceInfo(request.conferenceId, request.password); 34 | } 35 | 36 | @CrossOrigin 37 | @PostMapping(value = "/conference/put_info") 38 | public Object putConferenceInfo(@RequestBody ConferenceInfo info) throws IOException { 39 | return mService.putConferenceInfo(info); 40 | } 41 | 42 | @CrossOrigin 43 | @PostMapping(value = "/conference/create") 44 | public Object createConference(@RequestBody ConferenceInfo info) throws IOException { 45 | return mService.createConference(info); 46 | } 47 | 48 | @CrossOrigin 49 | @PostMapping(value = "/conference/destroy/{conferenceId}") 50 | public Object destroyConference(@PathVariable("conferenceId") String conferenceId) throws IOException { 51 | return mService.destroyConference(conferenceId); 52 | } 53 | 54 | @CrossOrigin 55 | @PostMapping(value = "/conference/recording/{conferenceId}") 56 | public Object recordingConference(@PathVariable("conferenceId") String conferenceId, @RequestBody RecordingRequest recordingRequest) throws IOException { 57 | return mService.recordingConference(conferenceId, recordingRequest.recording); 58 | } 59 | 60 | @CrossOrigin 61 | @PostMapping(value = "/conference/focus/{conferenceId}") 62 | public Object focusConference(@PathVariable("conferenceId") String conferenceId, @RequestBody UserIdPojo request) throws IOException { 63 | return mService.focusConference(conferenceId, request.userId); 64 | } 65 | 66 | @CrossOrigin 67 | @PostMapping(value = "/conference/fav/{conferenceId}") 68 | public Object favConference(@PathVariable("conferenceId") String conferenceId) throws IOException { 69 | return mService.favConference(conferenceId); 70 | } 71 | 72 | @CrossOrigin 73 | @PostMapping(value = "/conference/unfav/{conferenceId}") 74 | public Object unfavConference(@PathVariable("conferenceId") String conferenceId) throws IOException { 75 | return mService.unfavConference(conferenceId); 76 | } 77 | 78 | @CrossOrigin 79 | @PostMapping(value = "/conference/is_fav/{conferenceId}") 80 | public Object isFavConference(@PathVariable("conferenceId") String conferenceId) throws IOException { 81 | return mService.isFavConference(conferenceId); 82 | } 83 | 84 | @CrossOrigin 85 | @PostMapping(value = "/conference/fav_conferences") 86 | public Object getFavConferences() throws IOException { 87 | return mService.getFavConferences(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/IMExceptionEventController.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | import cn.wildfirechat.common.IMExceptionEvent; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.core.io.InputStreamResource; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.mail.SimpleMailMessage; 12 | import org.springframework.mail.javamail.JavaMailSender; 13 | import org.springframework.mail.javamail.MimeMessageHelper; 14 | import org.springframework.web.bind.annotation.*; 15 | import ws.schild.jave.*; 16 | 17 | import javax.annotation.PostConstruct; 18 | import javax.mail.MessagingException; 19 | import javax.mail.internet.MimeMessage; 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.FileNotFoundException; 23 | import java.net.MalformedURLException; 24 | import java.net.URL; 25 | import java.util.List; 26 | import java.util.Set; 27 | import java.util.concurrent.BlockingDeque; 28 | import java.util.concurrent.CompletableFuture; 29 | import java.util.concurrent.ConcurrentSkipListSet; 30 | import java.util.concurrent.LinkedBlockingDeque; 31 | import java.util.function.Supplier; 32 | 33 | @RestController 34 | public class IMExceptionEventController { 35 | private BlockingDeque events = new LinkedBlockingDeque<>(); 36 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 37 | 38 | @Value("${spring.mail.username}") 39 | private String from; 40 | 41 | @Value("${spring.mail.to_lists}") 42 | private String toLists; 43 | 44 | @Autowired 45 | private JavaMailSender mailSender; 46 | 47 | @PostConstruct 48 | void init() { 49 | new Thread(()->{ 50 | while (true) { 51 | try { 52 | IMExceptionEvent event = events.take(); 53 | if (event.event_type == IMExceptionEvent.EventType.HEART_BEAT) { 54 | sendTextMail("恭喜您,您的服务已经连续24小时没有异常发生了", "恭喜您,您的服务已经连续24小时没有异常发生了"); 55 | } else { 56 | sendTextMail("IM服务报警通知:节点" + event.node_id + ",发生" + event.count + "次 " + event.msg, "call stack:" + event.call_stack); 57 | } 58 | } catch (Exception e) { 59 | e.printStackTrace(); 60 | } 61 | } 62 | }).start(); 63 | } 64 | 65 | @PostMapping("im_exception_event") 66 | public String onIMException(@RequestBody IMExceptionEvent event) { 67 | System.out.println(event); 68 | events.add(event); 69 | return "ok"; 70 | } 71 | 72 | /** 73 | * 文本邮件 74 | * @param subject 邮件主题 75 | * @param content 邮件内容 76 | */ 77 | public void sendTextMail(String subject, String content){ 78 | SimpleMailMessage message = new SimpleMailMessage(); 79 | String[] tos = toLists.split(","); 80 | message.setTo(tos); 81 | message.setSubject(subject); 82 | message.setText(content); 83 | message.setFrom(from); 84 | 85 | mailSender.send(message); 86 | } 87 | 88 | //content HTML内容 89 | public void sendHtmlMail(String subject, String content) throws MessagingException { 90 | MimeMessage message = mailSender.createMimeMessage(); 91 | 92 | MimeMessageHelper helper = new MimeMessageHelper(message, true); 93 | String[] tos = toLists.split(","); 94 | helper.setTo(tos); 95 | helper.setSubject(subject); 96 | helper.setText(content, true); 97 | helper.setFrom(from); 98 | 99 | mailSender.send(message); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/LdapUtil.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | import javax.naming.Context; 4 | import javax.naming.NamingEnumeration; 5 | import javax.naming.NamingException; 6 | import javax.naming.directory.*; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.ArrayList; 9 | import java.util.Base64; 10 | import java.util.Hashtable; 11 | import java.util.List; 12 | 13 | public class LdapUtil { 14 | 15 | /** 根据电话号码反向查人 */ 16 | public static List findUserByPhone(String phone, String ldapUrl, String searchBase, String adminDn, String adminPwd) throws NamingException { 17 | Hashtable env = new Hashtable<>(); 18 | env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); 19 | env.put(Context.PROVIDER_URL, ldapUrl); 20 | env.put(Context.SECURITY_AUTHENTICATION, "simple"); 21 | env.put(Context.SECURITY_PRINCIPAL, adminDn); 22 | env.put(Context.SECURITY_CREDENTIALS, adminPwd); 23 | 24 | DirContext ctx = new InitialDirContext(env); 25 | try { 26 | /* 搜索过滤器:电话号码完全匹配 */ 27 | String filter = "(&(objectClass=inetOrgPerson)(telephoneNumber={0}))"; 28 | Object[] params = { phone }; 29 | 30 | SearchControls ctls = new SearchControls(); 31 | ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); 32 | /* 只取我们关心的属性 */ 33 | ctls.setReturningAttributes(new String[] { "uid", "cn", "mail", "telephoneNumber" }); 34 | 35 | NamingEnumeration rs = ctx.search(searchBase, filter, params, ctls); 36 | List ldapUsers = new ArrayList<>(); 37 | while (rs.hasMore()) { 38 | SearchResult sr = rs.next(); 39 | Attributes attrs = sr.getAttributes(); 40 | String dn = sr.getNameInNamespace(); 41 | ldapUsers.add(new LdapUser( 42 | getAttr(attrs, "uid"), 43 | getAttr(attrs, "cn"), 44 | getAttr(attrs, "mail"), 45 | getAttr(attrs, "telephoneNumber"), 46 | dn)); 47 | } 48 | return ldapUsers; 49 | } finally { 50 | ctx.close(); 51 | } 52 | } 53 | 54 | /** 55 | * OpenLDAP 密码加密:SSHA(Salted SHA-1,带随机盐值,更安全) 56 | * @param password 明文密码 57 | * @return Base64 编码后的 SSHA 密码(LDAP 存储格式) 58 | */ 59 | private static String encodeSshaPassword(String password) { 60 | try { 61 | // 1. 生成 8 字节随机盐值(增加破解难度) 62 | byte[] salt = new byte[8]; 63 | new java.security.SecureRandom().nextBytes(salt); 64 | 65 | // 2. 密码字节 + 盐值字节 66 | byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8); 67 | byte[] passwordWithSalt = new byte[passwordBytes.length + salt.length]; 68 | System.arraycopy(passwordBytes, 0, passwordWithSalt, 0, passwordBytes.length); 69 | System.arraycopy(salt, 0, passwordWithSalt, passwordBytes.length, salt.length); 70 | 71 | // 3. SHA-1 哈希(可替换为 SHA-256,需 LDAP 服务器支持) 72 | byte[] shaHash = java.security.MessageDigest.getInstance("SHA-1").digest(passwordWithSalt); 73 | 74 | // 4. SSHA 格式:{SSHA} + Base64(哈希值 + 盐值) 75 | byte[] sshaBytes = new byte[shaHash.length + salt.length]; 76 | System.arraycopy(shaHash, 0, sshaBytes, 0, shaHash.length); 77 | System.arraycopy(salt, 0, sshaBytes, shaHash.length, salt.length); 78 | 79 | return "{SSHA}" + Base64.getEncoder().encodeToString(sshaBytes); 80 | } catch (Exception e) { 81 | throw new RuntimeException("SSHA 密码加密失败", e); 82 | } 83 | } 84 | private static String getAttr(Attributes attrs, String name) throws NamingException { 85 | Attribute attr = attrs.get(name); 86 | return attr == null ? null : (String) attr.get(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/AudioController.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.core.io.InputStreamResource; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import ws.schild.jave.*; 11 | 12 | import javax.annotation.PostConstruct; 13 | import java.io.File; 14 | import java.io.FileInputStream; 15 | import java.io.FileNotFoundException; 16 | import java.net.MalformedURLException; 17 | import java.net.URL; 18 | import java.util.concurrent.CompletableFuture; 19 | import java.util.function.Supplier; 20 | 21 | @RestController 22 | public class AudioController { 23 | 24 | @Value("${wfc.audio.cache.dir}") 25 | String cacheDirPath; 26 | private File cacheDir; 27 | 28 | @PostConstruct 29 | public void init() { 30 | cacheDir = new File(cacheDirPath); 31 | if (!cacheDir.exists()) { 32 | cacheDir.mkdirs(); 33 | } 34 | } 35 | 36 | @GetMapping("amr2mp3") 37 | public CompletableFuture> amr2mp3(@RequestParam("path") String amrUrl) throws FileNotFoundException { 38 | 39 | MediaType mediaType = new MediaType("audio", "mp3"); 40 | String mp3FileName = amrUrl.substring(amrUrl.lastIndexOf('/') + 1) + ".mp3"; 41 | 42 | File mp3File = new File(cacheDir, mp3FileName); 43 | if (mp3File.exists()) { 44 | InputStreamResource resource = new InputStreamResource(new FileInputStream(mp3File)); 45 | return CompletableFuture.completedFuture(ResponseEntity.ok() 46 | // .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + mp3File.getName()) 47 | .contentType(mediaType) 48 | .contentLength(mp3File.length()) 49 | .body(resource)); 50 | } 51 | 52 | return CompletableFuture.supplyAsync(new Supplier>() { 53 | /** 54 | * Gets a result. 55 | * 56 | * @return a result 57 | */ 58 | @Override 59 | public ResponseEntity get() { 60 | 61 | try { 62 | amr2mp3(amrUrl, mp3File); 63 | InputStreamResource resource = new InputStreamResource(new FileInputStream(mp3File)); 64 | return ResponseEntity.ok() 65 | // .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + mp3File.getName()) 66 | .contentType(mediaType) 67 | .contentLength(mp3File.length()) 68 | .body(resource); 69 | } catch (MalformedURLException e) { 70 | System.out.println(amrUrl); 71 | e.printStackTrace(); 72 | } catch (EncoderException e) { 73 | e.printStackTrace(); 74 | } catch (FileNotFoundException e) { 75 | e.printStackTrace(); 76 | } 77 | return ResponseEntity.status(500).build(); 78 | } 79 | }); 80 | } 81 | 82 | private static void amr2mp3(String sourceUrl, File target) throws MalformedURLException, EncoderException { 83 | //Audio Attributes 84 | AudioAttributes audio = new AudioAttributes(); 85 | audio.setCodec("libmp3lame"); 86 | audio.setBitRate(128000); 87 | audio.setChannels(2); 88 | audio.setSamplingRate(44100); 89 | 90 | //Encoding attributes 91 | EncodingAttributes attrs = new EncodingAttributes(); 92 | attrs.setFormat("mp3"); 93 | attrs.setAudioAttributes(audio); 94 | 95 | //Encode 96 | Encoder encoder = new Encoder(); 97 | encoder.encode(new MultimediaObject(new URL(sourceUrl)), target, attrs); 98 | 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /release_note.md: -------------------------------------------------------------------------------- 1 | # 当前版本更新记录 2 | 0.70 Release note: 3 | 1. 解决上传文件可能存在的漏洞 4 | 5 | # 升级注意事项 6 | 1. 如果从0.40以前版本升级上来,需要注意升级兼容有问题 请参考Readme中的兼容问题说明 7 | 8 | 2. 如果从0.42以前版本升级上来,需要注意升级兼容有问题 请参考Readme中的兼容问题说明 9 | 10 | 3. 如果从0.45以前版本升级上来,需要注意升级PC和Web代码到0.45版本发布日志之后的版本 11 | 12 | 4. 如果从0.45.1以前版本升级上来,需要注意添加配置文件中的wfc.all_client_support_ssl开关 13 | 14 | 5. 0.51版本添加了token认证。可以同时支持token和cookies认证,客户端也做了对应修改,优先使用token。注意做好兼容。 15 | 16 | 6. 从0.53版本开始,所以数据都存储在数据库中,因此应用服务为无状态服务,可以部署多台应用服务做高可用和水平扩展。需要注意数据都是存储在数据库中,如果用户量较大或者业务量比较大,可以自己二开应用服务,添加redis缓存。 17 | 18 | 7. 从0.54版本开始,替换了腾讯云SDK,使用腾讯云短信的客户需要注意修改配置文件调试短信功能 19 | 20 | # 历史更新记录 21 | ------------- 22 | 0.71 Release note: 23 | 1. 升级IM SDK到1.3.8 24 | 2. 获取群组成员头像接口额外返回用户名称 25 | 3. 解决ios分享绕过限制问题 26 | 27 | ------------- 28 | 0.70 Release note: 29 | 1. 解决上传文件漏洞问题 30 | 31 | ------------- 32 | 0.69 Release note: 33 | 1. 升级IM SDK 34 | 2. 每次登录时都要发送机器人欢迎语 35 | 3. amr2mp3允许匿名访问 36 | 37 | ------------- 38 | 0.68 Release note: 39 | 1. 调整 amr转 mp3 失败时的状态码。 40 | 2. 添加默认密码功能。 41 | 3. 配置json去掉null的属性。 42 | 4. 升级IM server SDK。 43 | 44 | ------------- 45 | 0.67 Release note: 46 | 1. 修改群公告长度到2000。 47 | 2. 升级IM server SDK。 48 | 3. 生成头像的接口允许匿名访问。 49 | 50 | ------------- 51 | 0.66 Release note: 52 | 1. 添加生成用户和群组头像功能。 53 | 2. 修正使用超级验证码无法更改密码的问题。 54 | 55 | ------------- 56 | 0.65 Release note: 57 | 1. 通过应用服务发送消息时,返回消息ID和消息时间信息 58 | 59 | ------------- 60 | 0.64 Release note: 61 | 1. 当用户名登录时,当用户不存在时也返回密码错误。 62 | 2. 会议设置最大参与人数参数 63 | 64 | ------------- 65 | 0.63 Release note: 66 | 1. 删掉部分无用依赖减少程序大小 67 | 2. 会议支持设置焦点用户功能 68 | 3. 添加nginx示例配置 69 | 4. 升级IM SDK 70 | 5. 定期清理无效的pcsession 71 | 72 | ------------- 73 | 0.62 Release note: 74 | 1. 回归用户自动关注官方频道 75 | 2. 优化报警通知 76 | 3. 发送登录验证短信之前先检查是否被封禁。 77 | 4. 升级IM SDK 78 | 5. 添加会议录制接口 79 | 6. 添加rpm和deb格式 80 | 81 | ------------- 82 | 0.61 Release note: 83 | 1. 销毁用户时删掉密码 84 | 85 | ------------- 86 | 0.60 Release note: 87 | 1. 添加当新用户注册时关注频道功能 88 | 89 | ------------- 90 | 0.59 Release note: 91 | 1. mysql connector升级到8.0.28 92 | 2. commons-io升级到2.7 93 | 3. 添加密码登录方式 94 | 4. 升级im server sdk到0.92 95 | 96 | ------------- 97 | 0.58 Release note: 98 | 1. 升级log4j2版本到2.17.2 99 | 2. 升级server sdk到0.89 100 | 3. 添加删除账户功能 101 | 102 | ------------- 103 | 0.57 Release note: 104 | 1. 升级log4j2版本到2.17.1 105 | 2. 升级server sdk到0.86 106 | 3. 添加获取群组成员接口用来拼接头像 107 | 4. 添加对腾讯云对象存储的支持 108 | 109 | ------------- 110 | 0.56 Release note: 111 | 1. 升级log4j2版本到2.17.0 112 | 113 | ------------- 114 | 0.55 Release note: 115 | 1. 升级log4j2版本 116 | 117 | ------------- 118 | 0.54 Release note: 119 | 1. 更新腾讯云短信SDK,使用新的SDK进行接入短信,使用腾讯云短信的客户需要重新配置和调试短信功能。 120 | 6. 升级野火Server SDK。 121 | 122 | 123 | -------------- 124 | 0.53 Release note: 125 | 1. 短信验证码存储到数据库中 126 | 3. PC和Web端登录的session存储到数据库中 127 | 7. 支持会议相关接口 128 | 129 | -------------- 130 | 0.52 Release note: 131 | 1. 升级IM SDK 132 | 2. 修正token鉴权方式错误 133 | 134 | -------------- 135 | 0.51 Release note: 136 | 1. 优化收藏功能 137 | 2. 添加token认证方式 138 | 139 | -------------- 140 | 0.50 Release note: 141 | 1. 升级IM Server SDK 142 | 143 | -------------- 144 | 0.49 Release note: 145 | 1. 解决某些情况下mysql连不上的问题 146 | 2. 解决收藏错误问题 147 | 4. 添加第三方敏感词接口示例 148 | 8. 更新腾讯短信接口 149 | 150 | -------------- 151 | 0.48 Release note: 152 | 1. 添加IM服务异常报警功能 153 | 2. 修改PC快速登录消息有效时间为1分钟 154 | 155 | -------------- 156 | 0.47.1 Release note: 157 | 1. 修正收藏内容没有区分用户的问题,0.47版本都需要升级。 158 | 159 | -------------- 160 | 0.47 Release note: 161 | 1. 添加收藏功能 162 | 163 | -------------- 164 | 0.46.1 Release note: 165 | 1. 紧急修复PC端同时显示二维码太多无法登录的问题 166 | 167 | -------------- 168 | 0.46 Release note: 169 | 1. 升级spring boot版本 170 | 2. 支持iOS客户端通过appserver分享 171 | 172 | -------------- 173 | 0.45.1 Release note: 174 | 1. 添加配置,客户端是否支持SSL 175 | 176 | -------------- 177 | 0.45 Release note: 178 | 1. PC和Web增加快速登录功能 179 | 2. 解决PC和Web群公告获取失败的问题 180 | 181 | -------------- 182 | 0.44 Release note: 183 | 1. 添加针对请求IP限频。 184 | 2. 升级server api版本。 185 | 186 | -------------- 187 | 0.43 Release note: 188 | 1. 添加语音转码功能,支持微信小程序语音消息。 189 | 2. 修改pc或web端登录方式,改为长轮训方式。 190 | 3. 修改用户名生成方式,默认使用短ID。 191 | 192 | -------------- 193 | 0.42 Release note: 194 | 1. 更新到IM server SDK 0.41 195 | 2. 添加对物联网设备的支持(仅支持专业版) 196 | 197 | -------------- 198 | 0.41 Release note: 199 | 1. 生成用户账户时,使用uuid作为账户名称 200 | 201 | -------------- 202 | 0.40 Release note: 203 | 1. 增加阿里云短信配置,可以选择阿里云或者腾讯云短信 204 | 2. 增加了客户端上传日志功能,移动端在设置中选择上传日志 205 | 3. 为扫码登录和群公告添加shiro控制,提供系统的安全性。 206 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/IMConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix="im") 9 | @PropertySource(value = "file:config/im.properties", encoding = "UTF-8") 10 | public class IMConfig { 11 | public String admin_url; 12 | public String admin_secret; 13 | 14 | public String admin_user_id; 15 | 16 | public boolean isUse_random_name() { 17 | return use_random_name; 18 | } 19 | 20 | public void setUse_random_name(boolean use_random_name) { 21 | this.use_random_name = use_random_name; 22 | } 23 | 24 | boolean use_random_name; 25 | String welcome_for_new_user; 26 | String welcome_for_back_user; 27 | 28 | boolean new_user_robot_friend; 29 | String robot_friend_id; 30 | String robot_welcome; 31 | 32 | String prompt_text; 33 | String image_msg_url; 34 | String image_msg_base64_thumbnail; 35 | 36 | String new_user_subscribe_channel_id; 37 | String back_user_subscribe_channel_id; 38 | 39 | public String getAdmin_url() { 40 | return admin_url; 41 | } 42 | 43 | public void setAdmin_url(String admin_url) { 44 | this.admin_url = admin_url; 45 | } 46 | 47 | public String getAdmin_secret() { 48 | return admin_secret; 49 | } 50 | 51 | public void setAdmin_secret(String admin_secret) { 52 | this.admin_secret = admin_secret; 53 | } 54 | 55 | public String getWelcome_for_new_user() { 56 | return welcome_for_new_user; 57 | } 58 | 59 | public void setWelcome_for_new_user(String welcome_for_new_user) { 60 | this.welcome_for_new_user = welcome_for_new_user; 61 | } 62 | 63 | public String getWelcome_for_back_user() { 64 | return welcome_for_back_user; 65 | } 66 | 67 | public void setWelcome_for_back_user(String welcome_for_back_user) { 68 | this.welcome_for_back_user = welcome_for_back_user; 69 | } 70 | 71 | public boolean isNew_user_robot_friend() { 72 | return new_user_robot_friend; 73 | } 74 | 75 | public void setNew_user_robot_friend(boolean new_user_robot_friend) { 76 | this.new_user_robot_friend = new_user_robot_friend; 77 | } 78 | 79 | public String getRobot_friend_id() { 80 | return robot_friend_id; 81 | } 82 | 83 | public void setRobot_friend_id(String robot_friend_id) { 84 | this.robot_friend_id = robot_friend_id; 85 | } 86 | 87 | public String getRobot_welcome() { 88 | return robot_welcome; 89 | } 90 | 91 | public void setRobot_welcome(String robot_welcome) { 92 | this.robot_welcome = robot_welcome; 93 | } 94 | 95 | public String getNew_user_subscribe_channel_id() { 96 | return new_user_subscribe_channel_id; 97 | } 98 | 99 | public void setNew_user_subscribe_channel_id(String new_user_subscribe_channel_id) { 100 | this.new_user_subscribe_channel_id = new_user_subscribe_channel_id; 101 | } 102 | 103 | public String getBack_user_subscribe_channel_id() { 104 | return back_user_subscribe_channel_id; 105 | } 106 | 107 | public void setBack_user_subscribe_channel_id(String back_user_subscribe_channel_id) { 108 | this.back_user_subscribe_channel_id = back_user_subscribe_channel_id; 109 | } 110 | 111 | public String getAdmin_user_id() { 112 | return admin_user_id; 113 | } 114 | 115 | public void setAdmin_user_id(String admin_user_id) { 116 | this.admin_user_id = admin_user_id; 117 | } 118 | 119 | public String getPrompt_text() { 120 | return prompt_text; 121 | } 122 | 123 | public void setPrompt_text(String prompt_text) { 124 | this.prompt_text = prompt_text; 125 | } 126 | 127 | public String getImage_msg_url() { 128 | return image_msg_url; 129 | } 130 | 131 | public void setImage_msg_url(String image_msg_url) { 132 | this.image_msg_url = image_msg_url; 133 | } 134 | 135 | public String getImage_msg_base64_thumbnail() { 136 | return image_msg_base64_thumbnail; 137 | } 138 | 139 | public void setImage_msg_base64_thumbnail(String image_msg_base64_thumbnail) { 140 | this.image_msg_base64_thumbnail = image_msg_base64_thumbnail; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/tools/RateLimiter.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.tools; 2 | 3 | 4 | import java.util.HashMap; 5 | import java.util.Iterator; 6 | import java.util.Map; 7 | 8 | /** 9 | * 漏桶算法 10 | * capacity * 1000是为了更精确, 漏水的小洞更小^~^ 11 | */ 12 | public class RateLimiter { 13 | private static final int DEFAULT_LIMIT_TIME_SECOND = 5; 14 | private static final int DEFAULT_LIMIT_COUNT = 100; 15 | private static final long expire = 2 * 60 * 60 * 1000; 16 | private double rate = (double) DEFAULT_LIMIT_COUNT / (DEFAULT_LIMIT_TIME_SECOND); 17 | private long capacity = DEFAULT_LIMIT_COUNT * 1000; 18 | private long lastCleanTime; 19 | private Map requestCountMap = new HashMap<>(); 20 | private Map requestTimeMap = new HashMap<>(); 21 | 22 | private SpinLock lock = new SpinLock(); 23 | 24 | public RateLimiter() { 25 | 26 | } 27 | 28 | public RateLimiter(int limitTimeSecond, int limitCount) { 29 | if (limitTimeSecond <= 0 || limitCount <= 0) { 30 | throw new IllegalArgumentException(); 31 | } 32 | this.capacity = limitCount * 1000; 33 | this.rate = (double) limitCount / limitTimeSecond; 34 | } 35 | 36 | /** 37 | * 漏桶算法,https://en.wikipedia.org/wiki/Leaky_bucket 38 | */ 39 | public boolean isGranted(String userId) { 40 | try { 41 | lock.lock(); 42 | long current = System.currentTimeMillis(); 43 | cleanUp(current); 44 | Long lastRequestTime = requestTimeMap.get(userId); 45 | long count = 0; 46 | if (lastRequestTime == null) { 47 | count += 1000; 48 | requestTimeMap.put(userId, current); 49 | requestCountMap.put(userId, count); 50 | return true; 51 | } else { 52 | count = requestCountMap.get(userId); 53 | lastRequestTime = requestTimeMap.get(userId); 54 | count -= (current - lastRequestTime) * rate; 55 | count = count > 0 ? count : 0; 56 | requestTimeMap.put(userId, current); 57 | if (count < capacity) { 58 | count += 1000; 59 | requestCountMap.put(userId, count); 60 | return true; 61 | } else { 62 | requestCountMap.put(userId, count); 63 | return false; 64 | } 65 | } 66 | } finally { 67 | lock.unLock(); 68 | } 69 | } 70 | 71 | private void cleanUp(long current) { 72 | if (current - lastCleanTime > expire) { 73 | for (Iterator> it = requestTimeMap.entrySet().iterator(); it.hasNext();) { 74 | Map.Entry entry = it.next(); 75 | if (entry.getValue() < current - expire) { 76 | it.remove(); 77 | requestCountMap.remove(entry.getKey()); 78 | } 79 | } 80 | lastCleanTime = current; 81 | } 82 | } 83 | 84 | public static void main(String[] args) throws InterruptedException { 85 | RateLimiter limiter = new RateLimiter(1, 10); 86 | long start = System.currentTimeMillis(); 87 | for (int i = 0; i < 53; i++) { 88 | if (!limiter.isGranted("test")) { 89 | System.out.println("1 too frequency " + i); 90 | } 91 | } 92 | Thread.sleep(1 * 1000); 93 | System.out.println("sleep 1 s"); 94 | for (int i = 0; i < 53; i++) { 95 | if (!limiter.isGranted("test")) { 96 | System.out.println("2 too frequency " + i); 97 | } 98 | } 99 | 100 | Thread.sleep(5 * 1000); 101 | System.out.println("sleep 5 s"); 102 | for (int i = 0; i < 53; i++) { 103 | if (!limiter.isGranted("test")) { 104 | System.out.println("3 too frequency " + i); 105 | } 106 | } 107 | 108 | Thread.sleep(5 * 1000); 109 | System.out.println("sleep 5 s"); 110 | long second = System.currentTimeMillis(); 111 | for (int i = 0; i < 100; i++) { 112 | if (!limiter.isGranted("test")) { 113 | System.out.println("4 too frequency " + i); 114 | } 115 | Thread.sleep(50); 116 | } 117 | System.out.println("second: " + (System.currentTimeMillis() - second)); 118 | System.out.println("end: " + (System.currentTimeMillis() - start)); 119 | } 120 | } -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/ShiroConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | 4 | import org.apache.shiro.SecurityUtils; 5 | import org.apache.shiro.mgt.SecurityManager; 6 | import org.apache.shiro.session.mgt.SessionManager; 7 | import org.apache.shiro.spring.web.ShiroFilterFactoryBean; 8 | import org.apache.shiro.web.mgt.DefaultWebSecurityManager; 9 | import org.apache.shiro.web.servlet.Cookie; 10 | import org.apache.shiro.web.servlet.ShiroHttpSession; 11 | import org.apache.shiro.web.servlet.SimpleCookie; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | 17 | import java.util.Arrays; 18 | import java.util.LinkedHashMap; 19 | import java.util.Map; 20 | 21 | @Configuration 22 | public class ShiroConfig { 23 | 24 | @Autowired 25 | DBSessionDao dbSessionDao; 26 | 27 | @Autowired 28 | private PhoneCodeRealm phoneCodeRealm; 29 | 30 | @Autowired 31 | private ScanCodeRealm scanCodeRealm; 32 | 33 | @Autowired 34 | private UserPasswordRealm userPasswordRealm; 35 | 36 | @Autowired 37 | private LdapRealm ldapRealm; 38 | 39 | @Value("${wfc.all_client_support_ssl}") 40 | private boolean All_Client_Support_SSL; 41 | 42 | @Bean(name = "shiroFilter") 43 | public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { 44 | ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); 45 | shiroFilterFactoryBean.setSecurityManager(securityManager); 46 | shiroFilterFactoryBean.setLoginUrl("/login"); 47 | shiroFilterFactoryBean.setUnauthorizedUrl("/notRole"); 48 | Map filterChainDefinitionMap = new LinkedHashMap<>(); 49 | 50 | // 51 | filterChainDefinitionMap.put("/send_code", "anon"); 52 | filterChainDefinitionMap.put("/login", "anon"); 53 | filterChainDefinitionMap.put("/pc_session", "anon"); 54 | 55 | filterChainDefinitionMap.put("/login_pwd", "anon"); 56 | filterChainDefinitionMap.put("/send_reset_code", "anon"); 57 | filterChainDefinitionMap.put("/reset_pwd", "anon"); 58 | filterChainDefinitionMap.put("/session_login/**", "anon"); 59 | filterChainDefinitionMap.put("/user/online_event", "anon"); 60 | filterChainDefinitionMap.put("/logs/**", "anon"); 61 | filterChainDefinitionMap.put("/im_event/**", "anon"); 62 | filterChainDefinitionMap.put("/im_exception_event/**", "anon"); 63 | filterChainDefinitionMap.put("/message/censor", "anon"); 64 | filterChainDefinitionMap.put("/", "anon"); 65 | 66 | filterChainDefinitionMap.put("/confirm_pc", "login"); 67 | filterChainDefinitionMap.put("/cancel_pc", "login"); 68 | filterChainDefinitionMap.put("/scan_pc/**", "login"); 69 | filterChainDefinitionMap.put("/put_group_announcement", "login"); 70 | filterChainDefinitionMap.put("/get_group_announcement", "login"); 71 | filterChainDefinitionMap.put("/things/add_device", "login"); 72 | filterChainDefinitionMap.put("/things/list_device", "login"); 73 | 74 | filterChainDefinitionMap.put("/amr2mp3", "anon"); 75 | filterChainDefinitionMap.put("/avatar/**", "anon"); 76 | 77 | //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证 78 | filterChainDefinitionMap.put("/**", "login"); 79 | shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); 80 | shiroFilterFactoryBean.getFilters().put("login", new JsonAuthLoginFilter()); 81 | return shiroFilterFactoryBean; 82 | 83 | } 84 | 85 | @Bean 86 | public SecurityManager securityManager() { 87 | DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager(); 88 | defaultSecurityManager.setRealms(Arrays.asList(phoneCodeRealm, scanCodeRealm, userPasswordRealm, ldapRealm)); 89 | ShiroSessionManager sessionManager = new ShiroSessionManager(); 90 | sessionManager.setGlobalSessionTimeout(Long.MAX_VALUE); 91 | sessionManager.setSessionDAO(dbSessionDao); 92 | 93 | Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME); 94 | if (All_Client_Support_SSL) { 95 | cookie.setSameSite(Cookie.SameSiteOptions.NONE); 96 | cookie.setSecure(true); 97 | } else { 98 | cookie.setSameSite(null); 99 | } 100 | cookie.setMaxAge(Integer.MAX_VALUE); 101 | sessionManager.setSessionIdCookie(cookie); 102 | sessionManager.setSessionIdCookieEnabled(true); 103 | sessionManager.setSessionIdUrlRewritingEnabled(true); 104 | 105 | defaultSecurityManager.setSessionManager(sessionManager); 106 | SecurityUtils.setSecurityManager(defaultSecurityManager); 107 | return defaultSecurityManager; 108 | } 109 | } -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /config/application.properties: -------------------------------------------------------------------------------- 1 | spring.message.encoding=UTF-8 2 | server.port=8888 3 | 4 | ## 给服务添加统一的路径前缀,方便代理统一转换。 5 | ## 注意,如果这里改了,在客户端配置文件中修改APP_SERVER_ADDRESS,加上这个地址 6 | #server.servlet.context-path=/wfapp_api 7 | 8 | # 短信服务提供商,1是腾讯,2是阿里云 9 | sms.verdor=1 10 | # 在没有短信服务器时可以使用super code进行登录,上线时需要置为空(禁止超级验证码登录),或者改为较为严格的密码 11 | # 但是不能直接把这一行直接删除,或者注释了 12 | sms.super_code=66666 13 | 14 | # json序列化时去掉为null的属性,避免iOS出现NSNull的问题 15 | spring.jackson.default-property-inclusion=NON_NULL 16 | 17 | # h2适合开发使用,上线时请切换到mysql。切换时把下面h2部分配置注释掉,打开mysql部署配置。 18 | ##*********************** h2 DB begin *************************** 19 | spring.datasource.url=jdbc:h2:file:./appdata 20 | spring.datasource.username=sa 21 | spring.datasource.password= 22 | spring.datasource.driver-class-name=org.h2.Driver 23 | spring.jpa.hibernate.ddl-auto=update 24 | ##*********************** h2 DB end ****************************** 25 | 26 | # mysql默认配置 27 | # mysql需要手动创建数据库,mysql命令行下执行 create database appdata; appdata可以换为别的库名,但注意不能使用IM服务器使用的数据库"wfchat",否则会引起冲突。 28 | ##*********************** mysql DB begin ************************* 29 | #spring.datasource.url=jdbc:mysql://localhost:3306/appdata?serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false 30 | #spring.datasource.username=root 31 | #spring.datasource.password=123456 32 | #spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 33 | #spring.jpa.database=mysql 34 | #spring.jpa.hibernate.ddl-auto=update 35 | ## 遇到后面的报错时,请打开下面的注释:Storage engine MyISAM is disabled (Table creation is disallowed). 36 | ##spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect 37 | ##*********************** mysql DB end *************************** 38 | 39 | # ldap登录的配置 40 | ## 是否开启ldap登录。开启后普通密码登录就失效了,只能用ldap登录。另外客户端代码也需要修改一下,去掉短信登录和修改密码等相关代码。 41 | ldap.enable=true 42 | ldap.admin_dn=cn=admin,dc=wildfirechat,dc=net 43 | ldap.admin_password=123456@abc 44 | ldap.ldap_url=ldap://192.168.1.48:389 45 | ldap.search_base=dc=wildfirechat,dc=net 46 | 47 | # PC快速登录兼容旧的版本。仅当已经有未支持PC快速登录的移动端上线了,需要兼容时打开此开关。 48 | wfc.compat_pc_quick_login=false 49 | 50 | # 用户上传协议日志存放目录,上线时请修改可用路径 51 | logs.user_logs_path=/Users/imhao/wildfire_user_logs/ 52 | 53 | # *************************** 上线必看 ********************************* 54 | # demo工程为了方便大家运行测试,使用了数据库作为SessionDao的缓存,上线后,当用户较多时会是一个瓶颈,请在上线前切换成redis的缓存。 55 | # 细节请参考 https://www.baidu.com/s?wd=shiro+redis&tn=84053098_3_dg&ie=utf-8 56 | 57 | # 小程序不能播放amr格式的音频,需要将amr转化成mp3格式 58 | # amr转mp3缓存目录,本目录会存储转换后的mp3文件,可以定时清理 59 | wfc.audio.cache.dir=/data/wfc/audio/cache 60 | 61 | # 是否支持SSL,如果所有客户端调用appserver都支持https,请把下面开关设置为true,否则为false。 62 | # 如果为false,在Web端和wx端的appserve的群公告等功能将不可用。 63 | # 详情请参考 https://www.baidu.com/s?wd=cookie+SameSite&ie=utf-8 64 | wfc.all_client_support_ssl=false 65 | 66 | ## 是否添加用户默认密码。可以开启此配置,使用手机号码的后六位作为初始密码。首次登录之后必须修改密码。其他情况不用打开此开关。 67 | ## 用户设置密码时,不能设置为手机号码的后6位 68 | wfc.default_user_password=false 69 | 70 | ## iOS系统使用share extension来处理分享,客户端无法调用SDK发送消息和文件,只能通过应用服务来进行。 71 | ## 这里配置为了满足iOS设备在share extension中进行上传文件的需求。 72 | ## 存储使用类型,0使用内置文件服务器(这里无法使用),1使用七牛云存储,2使用阿里云对象存储,3野火私有对象存储, 73 | ## 4野火对象存储网关(当使用4时,需要处理 uploadMedia和putFavoriteItem方法),5腾讯云存储。 74 | ## 默认的七牛/阿里OSS/野火私有存储账户信息不可用,请按照下面说明配置 75 | ## https://docs.wildfirechat.cn/server/oss.html 76 | media.server.media_type=1 77 | 78 | # 使用这个目录作为临时目录,必须配置有效目录。 79 | local.media.temp_storage=/Users/imhao/wildfire_upload_tmp/ 80 | 81 | 82 | ## OSS配置,可以是七牛/阿里云OSS/野火私有OSS。 83 | ## 注意与IM服务的配置格式不太一样,这里是用"Key=Vaue"的格式,IM服务配置里是"Key Value",拷贝粘贴时要注意修改。 84 | 85 | ## 配置请参考IM服务 86 | ## 下面是七牛云的示例,如果是腾讯云或者阿里云,server_url应该是 cos.ap-nanjing.myqcloud.com 或 oss-cn-beijing.aliyuncs.com 这样。 87 | media.server_url=http://up.qbox.me 88 | media.access_key=tU3vdBK5BL5j4N7jI5N5uZgq_HQDo170w5C9Amnn 89 | media.secret_key=YfQIJdgp5YGhwEw14vGpaD2HJZsuJldWtqens7i5 90 | 91 | ## bucket名字及Domain 92 | media.bucket_general_name=media 93 | media.bucket_general_domain=http://cdn.wildfirechat.cn 94 | media.bucket_image_name=media 95 | media.bucket_image_domain=http://cdn.wildfirechat.cn 96 | media.bucket_voice_name=media 97 | media.bucket_voice_domain=http://cdn.wildfirechat.cn 98 | media.bucket_video_name=media 99 | media.bucket_video_domain=http://cdn.wildfirechat.cn 100 | media.bucket_file_name=media 101 | media.bucket_file_domain=http://cdn.wildfirechat.cn 102 | media.bucket_sticker_name=media 103 | media.bucket_sticker_domain=http://cdn.wildfirechat.cn 104 | media.bucket_moments_name=media 105 | media.bucket_moments_domain=http://cdn.wildfirechat.cn 106 | media.bucket_portrait_name=storage 107 | media.bucket_portrait_domain=http://cdn2.wildfirechat.cn 108 | media.bucket_favorite_name=storage 109 | media.bucket_favorite_domain=http://cdn2.wildfirechat.cn 110 | 111 | # 报警发送邮件配置 112 | # 当IM服务异常时,会把异常信息推送到应用服务,由应用服务来给运维人员发送邮件,建议上线时调通次功能 113 | spring.mail.host=smtp.wildfirechat.com 114 | spring.mail.username=admin@wildfirechat.cn 115 | # 注意有些邮件服务商会提供客户端授权码,不能用邮箱账户密码。 116 | spring.mail.password=xxxxxxxx 117 | spring.mail.port=465 118 | spring.mail.protocol=smtp 119 | spring.mail.default-encoding=UTF-8 120 | spring.mail.test-connection=false 121 | spring.mail.properties.mail.smtp.auth=true 122 | spring.mail.properties.mail.smtp.ssl.enable=true 123 | spring.mail.properties.mail.smtp.starttls.enable=true 124 | spring.mail.properties.mail.smtp.starttls.required=true 125 | spring.mail.properties.mail.imap.ssl.socketFactory.fallback=false 126 | 127 | # 邮箱必须是有效邮箱,如果是无效邮箱可能会发送失败 128 | spring.mail.to_lists=admin1@wildfirechat.cn,admin2@wildfirechat.cn,admin3@wildfirechat.cn 129 | 130 | # 头像背景颜色可选列表,逗号分隔,中间不能有空格 131 | avatar.bg.corlors=#D32F2F,#D81B60,#880E4F,#9C27B0,#6A1B9A,#4A148C,#AA00FF,#C51162,#673AB7,#311B92,#651FFF,#5C6BC0,#283593,#1A237E,#304FFE,#1976D2,#0D47A1,#2962FF,#0D47A1,#0277BD,#01579B 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 野火IM解决方案 2 | 3 | 野火IM是专业级即时通讯和实时音视频整体解决方案,由北京野火无限网络科技有限公司维护和支持。 4 | 5 | 主要特性有:私有部署安全可靠,性能强大,功能齐全,全平台支持,开源率高,部署运维简单,二次开发友好,方便与第三方系统对接或者嵌入现有系统中。详细情况请参考[在线文档](https://docs.wildfirechat.cn)。 6 | 7 | 主要包括一下项目: 8 | 9 | 10 | | [GitHub仓库地址(主站)](https://github.com/wildfirechat) | [码云仓库地址(镜像)](https://gitee.com/wfchat) | 说明 | 备注 | 11 | | ------------------------------------------------------------ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- | 12 | | [im-server](https://github.com/wildfirechat/im-server) | [im-server](https://gitee.com/wfchat/im-server) | IM Server | | 13 | | [android-chat](https://github.com/wildfirechat/android-chat) | [android-chat](https://gitee.com/wfchat/android-chat) | 野火IM Android SDK源码和App源码 | 可以很方便地进行二次开发,或集成到现有应用当中 | 14 | | [ios-chat](https://github.com/wildfirechat/ios-chat) | [ios-chat](https://gitee.com/wfchat/ios-chat) | 野火IM iOS SDK源码和App源码 | 可以很方便地进行二次开发,或集成到现有应用当中 | 15 | | [pc-chat](https://github.com/wildfirechat/vue-pc-chat) | [pc-chat](https://gitee.com/wfchat/vue-pc-chat) | 基于[Electron](https://electronjs.org/)开发的PC 端 | | 16 | | [web-chat](https://github.com/wildfirechat/vue-chat) | [web-chat](https://gitee.com/wfchat/vue-chat) | 野火IM Web 端, [体验地址](http://web.wildfirechat.cn) | | 17 | | [wx-chat](https://github.com/wildfirechat/wx-chat) | [wx-chat](https://gitee.com/wfchat/wx-chat) | 小程序平台的Demo(支持微信、百度、阿里、字节、QQ 等小程序平台) | | 18 | | [app server](https://github.com/wildfirechat/app_server) | [app server](https://gitee.com/wfchat/app_server) | 应用服务端 | | 19 | | [robot_server](https://github.com/wildfirechat/robot_server) | [robot_server](https://gitee.com/wfchat/robot_server) | 机器人服务端 | | 20 | | [push_server](https://github.com/wildfirechat/push_server) | [push_server](https://gitee.com/wfchat/push_server) | 推送服务器 | | 21 | | [docs](https://github.com/wildfirechat/docs) | [docs](https://gitee.com/wfchat/docs) | 野火IM相关文档,包含设计、概念、开发、使用说明,[在线查看](https://docs.wildfirechat.cn/) | | 22 | 23 | ## 野火IM后端应用 24 | 作为野火IM的后端应用的演示,本工程具有如下功能: 25 | 1. 短信登陆和注册功能,用来演示登陆应用,获取token的场景. 26 | 2. PC端扫码登录的功能. 27 | 3. 群公告的获取和更新功能. 28 | 4. 客户端上传日志功能. 29 | > 本工程为Demo工程,实际使用时需要把对应功能移植到您的应用服务中。如果需要直接使用,请按照后面的说明解决掉性能瓶颈问题。 30 | 31 | #### 编译 32 | ``` 33 | mvn clean package 34 | ``` 35 | 36 | ## 打包RPM格式 37 | 打包会生成Java包和deb安装包,如果需要rpm安装包,请在```pom.xml```中取消注释生成rpm包的plugin。另外还需要本地安装有rpm,在linux或者mac系统中很容易安装,在windows系统需要安装cygwin并安装rpm,具体信息请百度查询。 38 | 39 | 修改之后运行编译命令```mvn clean package```,rpm包生成在```target```目录下。 40 | 41 | #### 短信资源 42 | 应用使用的是腾讯云短信功能,需要申请到```appid/appkey/templateId```这三个参数,并配置到```tencent_sms.properties```中去。用户也可以自行更换为自己喜欢的短信提供商。在没有短信供应商的情况下,为了测试可以使用```superCode```,设置好后,客户端可以直接使用```superCode```进行登陆。上线时一定要注意删掉```superCode```。 43 | 44 | #### 修改配置 45 | 本演示服务有4个配置文件在工程的```config```目录下,分别是```application.properties```, ```im.properties```, ```aliyun_sms.properties```和```tencent_sms.properties```。请正确配置放到jar包所在的目录下的```config```目录下。 46 | > ```application.properties```配置中的```sms.verdor```决定是使用那个短信服务商,1为腾讯短信,2为阿里云短信 47 | 48 | #### 运行 49 | 在```target```目录找到```app-XXXX.jar```,把jar包和放置配置文件的```config```目录放到一起,然后执行下面命令: 50 | ``` 51 | java -jar app-XXXXX.jar 52 | ``` 53 | 54 | #### 性能瓶颈 55 | 本服务最早只提供获取token功能,后来逐渐增加了群公告/Shiro等功能,需要引入数据库。为了提高用户体验的便利性,引入了数据库[H2](http://www.h2database.com),让用户可以无需安装任何软件就可以直接运行(JRE还是需要的),另外shiro的session也存储在h2数据库中。提高了便利性的同时导致一方面性能有瓶颈,另外一方面也不能水平扩展和高可用。因此需要使用本工程上线必须修改2个地方。 56 | 1. 切换到MySQL,切换方法请参考 ```application.properties``` 文件中的描述。 57 | 2. 使用RedisSessionDao,详情请参考 https://www.baidu.com/s?wd=shiro+redis&tn=84053098_3_dg&ie=utf-8 58 | 3. 从0.53版本开始,应用服务改为无状态服务,可以集群部署。验证码和PC会话等信息都存放到数据库中,如果压力较大,可以二开引入redis缓存。 59 | 60 | #### 版本兼容 61 | + 0.40版本引入了shiro功能,在升级本服务之前,需要确保客户端已经引入了本工程0.40版本发布时或之后的移动客户端。并且在升级之后,客户端需要退出重新登录一次以便保存session(退出登录时调用disconnect,需要使用false值,这样重新登录才能保留历史聊天记录,一定要在新版本中改成这样)。如果是旧版本或者没有重新登录,群公告和扫码登录功能将不可用。为了系统的安全性,建议升级。 62 | 63 | + 0.43版本把Web和PC登录的短轮询改为长轮询,如果应用服务升级需要对Web和PC进行对应修改。 64 | 65 | + 0.45.1 配置文件中添加了```wfc.all_client_support_ssl```开关,当升级到这个版本或之后时,需要配置文件中添加这个开关。 66 | 67 | + 0.51版本添加了token认证。可以同时支持token和cookies认证,客户端也做了对应修改,优先使用token。注意做好兼容。 68 | 69 | + 从0.53版本开始,所以数据都存储在数据库中,因此应用服务为无状态服务,可以部署多台应用服务做高可用和水平扩展。需要注意数据都是存储在数据库中,如果用户量较大或者业务量比较大,可以自己二开应用服务,添加redis缓存。 70 | 71 | #### 使用LDAP统一认证 72 | 代码中添加了AD域登录的示例代码,可以在```application.properties```配置文件中打开ldap的配置并正确配置。需要ldap用户信息中,包含有电话号码。当登录时,先用电话号码查询到用户的dn,再用这个dn登录。如果遇到问题,请自己调试一下。调试时重点关注一下类```LdapMatcher```和```loginWithLdap```方法。 73 | 74 | #### 修改其他登录方式 75 | 野火把登录功能从IM服务剥离,放到了应用服务中,目的是为了让客户更灵活的接入各种业务系统中进行登录。可以修改这个服务的任意代码,只要确保登录后从IM服务获取IM token返回给用户即可。 76 | 77 | #### 注意事项 78 | 服务中对同一个IP的请求会有限频,默认是一个ip一小时可以请求200次,可以根据您的实际情况调整(搜索rateLimiter字符串就能找到)。如果使用了nginx做反向代理需要注意把用户真实ip传递过去(使用X-Real-IP或X-Forwarded-For),避免获取不到真实ip从而影响正常使用。 79 | 80 | #### 使用到的开源代码 81 | 1. [TypeBuilder](https://github.com/ikidou/TypeBuilder) 一个用于生成泛型的简易Builder 82 | 83 | #### LICENSE 84 | UNDER MIT LICENSE. 详情见LICENSE文件 85 | 86 | 87 | #### 使用阿里云短信 88 | 请参考说明[使用阿里云短信](./aliyun_sms.md) 89 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/avatar/AvatarServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.avatar; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.util.StreamUtils; 9 | import org.springframework.util.StringUtils; 10 | 11 | import javax.annotation.PostConstruct; 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.net.MalformedURLException; 16 | import java.net.URISyntaxException; 17 | import java.net.URL; 18 | import java.nio.file.Files; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.function.Supplier; 23 | 24 | @Service 25 | public class AvatarServiceImpl implements AvatarService { 26 | @Value("${avatar.bg.corlors}") 27 | String bgColors; 28 | 29 | public static final String AVATAR_DIR = "./avatar"; 30 | 31 | @PostConstruct 32 | public void init() { 33 | File avatarDir = new File(AvatarServiceImpl.AVATAR_DIR); 34 | if (!avatarDir.exists()) { 35 | avatarDir.mkdirs(); 36 | } 37 | } 38 | 39 | @Override 40 | public ResponseEntity avatar(String name) throws IOException { 41 | File file = nameAvatar(name); 42 | if (file != null && file.exists()) { 43 | byte[] bytes = StreamUtils.copyToByteArray(Files.newInputStream(file.toPath())); 44 | return ResponseEntity 45 | .ok() 46 | .contentType(MediaType.IMAGE_PNG) 47 | .header("Cache-Control", "max-age=604800") 48 | .body(bytes); 49 | } else { 50 | return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); 51 | } 52 | } 53 | 54 | @Override 55 | public CompletableFuture> groupAvatar(GroupAvatarRequest request) throws MalformedURLException { 56 | List infos = request.getMembers(); 57 | List paths = new ArrayList<>(); 58 | long hashCode = 0; 59 | for (int i = 0; i < infos.size() && i < 9; i++) { 60 | GroupAvatarRequest.GroupMemberInfo info = infos.get(i); 61 | if (!StringUtils.isEmpty(info.getAvatarUrl())) { 62 | paths.add(new URL(info.getAvatarUrl())); 63 | hashCode += info.getAvatarUrl().hashCode(); 64 | } else { 65 | File file = nameAvatar(info.getName()); 66 | if (file != null && file.exists()) { 67 | paths.add(file.toURI().toURL()); 68 | hashCode += info.getName().hashCode(); 69 | } 70 | } 71 | } 72 | File file = new File(AVATAR_DIR, hashCode + "-group.png"); 73 | if (!file.exists()) { 74 | return CompletableFuture.supplyAsync(new Supplier>() { 75 | @Override 76 | public ResponseEntity get() { 77 | InputStream inputStream = null; 78 | try { 79 | GroupAvatarUtil.getCombinationOfHead(paths, file); 80 | if (file.exists()) { 81 | inputStream = Files.newInputStream(file.toPath()); 82 | byte[] bytes = StreamUtils.copyToByteArray(inputStream); 83 | return ResponseEntity.ok() 84 | .contentType(MediaType.IMAGE_PNG) 85 | .header("Cache-Control", "max-age=604800") 86 | .body(bytes); 87 | } else { 88 | return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); 89 | } 90 | } catch (IOException | URISyntaxException e) { 91 | e.printStackTrace(); 92 | } finally { 93 | if (inputStream != null) { 94 | try { 95 | inputStream.close(); 96 | } catch (IOException e) { 97 | // do nothing 98 | } 99 | } 100 | } 101 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); 102 | } 103 | 104 | }); 105 | } else { 106 | try ( 107 | InputStream inputStream = Files.newInputStream(file.toPath()) 108 | ) { 109 | byte[] bytes = StreamUtils.copyToByteArray(inputStream); 110 | return CompletableFuture.completedFuture(ResponseEntity.ok() 111 | .contentType(MediaType.IMAGE_PNG) 112 | .header("Cache-Control", "max-age=604800") 113 | .body(bytes)); 114 | } catch (IOException e) { 115 | e.printStackTrace(); 116 | } 117 | return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); 118 | } 119 | } 120 | 121 | private File nameAvatar(String name) { 122 | if (StringUtils.isEmpty(name)) { 123 | return null; 124 | } 125 | String[] colors = bgColors.split(","); 126 | int len = colors.length; 127 | int hashCode = name.hashCode(); 128 | File file = new File(AVATAR_DIR, hashCode + ".png"); 129 | if (!file.exists()) { 130 | String color = colors[Math.abs(name.hashCode() % len)]; 131 | // 最后一个字符 132 | String lastChar = name.substring(name.length() - 1).toUpperCase(); 133 | file = new NameAvatarBuilder(color).name(lastChar, name).build(); 134 | } 135 | return file; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/IMCallbackController.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | import cn.wildfirechat.pojos.*; 4 | import com.google.gson.Gson; 5 | import org.springframework.web.bind.annotation.PostMapping; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | /* 10 | IM对应事件发生时,会回调到配置地址。需要注意IM服务单线程进行回调,如果接收方处理太慢会导致推送线程被阻塞,导致延迟发生,甚至导致IM系统异常。 11 | 建议异步处理快速返回,这里收到后转到异步线程处理,并且立即返回。另外两个服务器的ping值不能太大。 12 | */ 13 | @RestController() 14 | public class IMCallbackController { 15 | /* 16 | 用户在线状态回调 17 | */ 18 | @PostMapping(value = "/im_event/user/online") 19 | public Object onUserOnlineEvent(@RequestBody UserOnlineStatus event) { 20 | System.out.println("User:" + event.userId + " on device:" + event.clientId + " online status:" + event.status); 21 | return "ok"; 22 | } 23 | 24 | /* 25 | 用户关系变更回调 26 | */ 27 | @PostMapping(value = "/im_event/user/relation") 28 | public Object onUserRelationUpdated(@RequestBody RelationUpdateEvent event) { 29 | System.out.println("User relation updated:" + event.userId); 30 | return "ok"; 31 | } 32 | 33 | /* 34 | 用户信息更新回调 35 | */ 36 | @PostMapping(value = "/im_event/user/info") 37 | public Object onUserInfoUpdated(@RequestBody InputOutputUserInfo event) { 38 | System.out.println("User info updated:" + event.getUserId()); 39 | return "ok"; 40 | } 41 | 42 | /* 43 | 发送消息回调 44 | */ 45 | @PostMapping(value = "/im_event/message") 46 | public Object onMessage(@RequestBody OutputMessageData event) { 47 | System.out.println("message:" +event.getMessageId()); 48 | return "ok"; 49 | } 50 | 51 | /* 52 | 发送消息回调 53 | */ 54 | @PostMapping(value = "/im_event/recall_message") 55 | public Object onRecallMessage(@RequestBody OutputRecallMessageData event) { 56 | System.out.println("recall message:" +event.getUserId()); 57 | return "ok"; 58 | } 59 | 60 | /* 61 | 物联网消息回调 62 | */ 63 | @PostMapping(value = "/im_event/things/message") 64 | public Object onThingsMessage(@RequestBody OutputMessageData event) { 65 | System.out.println("message:" + event.getMessageId()); 66 | return "ok"; 67 | } 68 | 69 | /* 70 | 消息已读回调 71 | */ 72 | @PostMapping(value = "/im_event/message_read") 73 | public Object onMessageRead(@RequestBody OutputReadData event) { 74 | System.out.println("message:" +event.user); 75 | return "ok"; 76 | } 77 | 78 | /* 79 | 群组信息更新回调 80 | */ 81 | @PostMapping(value = "/im_event/group/info") 82 | public Object onGroupInfoUpdated(@RequestBody GroupUpdateEvent event) { 83 | System.out.println("group info updated:" + event.type); 84 | return "ok"; 85 | } 86 | 87 | /* 88 | 群组成员更新回调 89 | */ 90 | @PostMapping(value = "/im_event/group/member") 91 | public Object onGroupMemberUpdated(@RequestBody GroupMemberUpdateEvent event) { 92 | System.out.println("group member updated:" + event.type); 93 | return "ok"; 94 | } 95 | 96 | /* 97 | 频道信息更新回调 98 | */ 99 | @PostMapping(value = "/im_event/channel/info") 100 | public Object onChannelInfoUpdated(@RequestBody ChannelUpdateEvent event) { 101 | System.out.println("channel info updated:" + event.type); 102 | return "ok"; 103 | } 104 | 105 | /* 106 | 聊天室信息更新回调 107 | */ 108 | @PostMapping(value = "/im_event/chatroom/info") 109 | public Object onChatroomInfoUpdated(@RequestBody ChatroomUpdateEvent event) { 110 | System.out.println("chatroom info updated:" + event.type); 111 | return "ok"; 112 | } 113 | 114 | /* 115 | 聊天室成员更新回调 116 | */ 117 | @PostMapping(value = "/im_event/chatroom/member") 118 | public Object onChatroomMemberUpdated(@RequestBody ChatroomMemberUpdateEvent event) { 119 | System.out.println("chatroom member updated:" + event.type); 120 | return "ok"; 121 | } 122 | 123 | /* 124 | 消息审查示例。 125 | 126 | 如果允许发送,返回状态码为200,内容为空;如果替换内容发送,返回状态码200,内容为替换过的payload内容。如果不允许发送,返回状态码403。 127 | 注意如果没有替换内容运行原消息发送,要返回空内容,不要返回原消息!!! 128 | */ 129 | @PostMapping(value = "/message/censor") 130 | public Object censorMessage(@RequestBody OutputMessageData event) { 131 | System.out.println("message:" +event.getMessageId()); 132 | if(event.getPayload().getSearchableContent() != null && event.getPayload().getSearchableContent().contains("testkongbufenzi")) { 133 | throw new ForbiddenException(); 134 | } 135 | if(event.getPayload().getSearchableContent() != null && event.getPayload().getSearchableContent().contains("testzhaopian")) { 136 | event.getPayload().setSearchableContent(event.getPayload().getSearchableContent().replace("zhaopian", "照片")); 137 | return new Gson().toJson(event.getPayload()); 138 | } 139 | return ""; 140 | } 141 | 142 | @PostMapping(value = "/im_event/conference/create") 143 | public Object onConferenceCreated(@RequestBody ConferenceCreateEvent event) { 144 | System.out.println("conference created:" + event); 145 | return "ok"; 146 | } 147 | 148 | @PostMapping(value = "/im_event/conference/destroy") 149 | public Object onConferenceDestroyed(@RequestBody ConferenceDestroyEvent event) { 150 | System.out.println("conference destroyed:" + event); 151 | return "ok"; 152 | } 153 | 154 | @PostMapping(value = "/im_event/conference/member_join") 155 | public Object onConferenceMemberJoined(@RequestBody ConferenceJoinEvent event) { 156 | System.out.println("conference member joined:" + event); 157 | return "ok"; 158 | } 159 | 160 | @PostMapping(value = "/im_event/conference/member_leave") 161 | public Object onConferenceMemberLeaved(@RequestBody ConferenceLeaveEvent event) { 162 | System.out.println("conference member leaved:" + event); 163 | return "ok"; 164 | } 165 | 166 | @PostMapping(value = "/im_event/conference/member_publish") 167 | public Object onConferenceMemberPublished(@RequestBody ConferencePublishEvent event) { 168 | System.out.println("conference member published:" + event); 169 | return "ok"; 170 | } 171 | 172 | @PostMapping(value = "/im_event/conference/member_unpublish") 173 | public Object onConferenceMemberUnpublished(@RequestBody ConferenceUnpublishEvent event) { 174 | System.out.println("conference member unpublished:" + event); 175 | return "ok"; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/sms/SmsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.sms; 2 | 3 | import cn.wildfirechat.app.RestResult; 4 | import com.aliyuncs.CommonRequest; 5 | import com.aliyuncs.CommonResponse; 6 | import com.aliyuncs.DefaultAcsClient; 7 | import com.aliyuncs.IAcsClient; 8 | import com.aliyuncs.exceptions.ClientException; 9 | import com.aliyuncs.exceptions.ServerException; 10 | import com.aliyuncs.http.MethodType; 11 | import com.aliyuncs.profile.DefaultProfile; 12 | import com.google.gson.Gson; 13 | import com.tencentcloudapi.common.Credential; 14 | import com.tencentcloudapi.common.exception.TencentCloudSDKException; 15 | import com.tencentcloudapi.common.profile.ClientProfile; 16 | import com.tencentcloudapi.common.profile.HttpProfile; 17 | import com.tencentcloudapi.sms.v20210111.SmsClient; 18 | import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest; 19 | import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.beans.factory.annotation.Value; 24 | import org.springframework.stereotype.Service; 25 | 26 | import java.io.IOException; 27 | 28 | @Service 29 | public class SmsServiceImpl implements SmsService { 30 | private static final Logger LOG = LoggerFactory.getLogger(SmsServiceImpl.class); 31 | 32 | 33 | private static class AliyunCommonResponse { 34 | String Message; 35 | String Code; 36 | } 37 | 38 | @Value("${sms.verdor}") 39 | private int smsVerdor; 40 | 41 | @Autowired 42 | private TencentSMSConfig mTencentSMSConfig; 43 | 44 | @Autowired 45 | private AliyunSMSConfig aliyunSMSConfig; 46 | 47 | @Override 48 | public RestResult.RestCode sendCode(String mobile, String code) { 49 | if (smsVerdor == 1) { 50 | return sendTencentCode(mobile, code); 51 | } else if(smsVerdor == 2) { 52 | return sendAliyunCode(mobile, code); 53 | } else { 54 | return RestResult.RestCode.ERROR_SERVER_NOT_IMPLEMENT; 55 | } 56 | } 57 | 58 | private RestResult.RestCode sendTencentCode(String mobile, String code) { 59 | try { 60 | /* 必要步骤: 61 | * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。 62 | * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。 63 | * 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人, 64 | * 以免泄露密钥对危及你的财产安全。 65 | * CAM密匙查询: https://console.cloud.tencent.com/cam/capi*/ 66 | Credential cred = new Credential(mTencentSMSConfig.secretId, mTencentSMSConfig.secretKey); 67 | 68 | // 实例化一个http选项,可选,没有特殊需求可以跳过 69 | HttpProfile httpProfile = new HttpProfile(); 70 | // 设置代理 71 | // httpProfile.setProxyHost("真实代理ip"); 72 | // httpProfile.setProxyPort(真实代理端口); 73 | /* SDK默认使用POST方法。 74 | * 如果你一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */ 75 | httpProfile.setReqMethod("POST"); 76 | /* SDK有默认的超时时间,非必要请不要进行调整 77 | * 如有需要请在代码中查阅以获取最新的默认值 */ 78 | httpProfile.setConnTimeout(60); 79 | /* SDK会自动指定域名。通常是不需要特地指定域名的,但是如果你访问的是金融区的服务 80 | * 则必须手动指定域名,例如sms的上海金融区域名: sms.ap-shanghai-fsi.tencentcloudapi.com */ 81 | httpProfile.setEndpoint("sms.tencentcloudapi.com"); 82 | 83 | /* 非必要步骤: 84 | * 实例化一个客户端配置对象,可以指定超时时间等配置 */ 85 | ClientProfile clientProfile = new ClientProfile(); 86 | /* SDK默认用TC3-HMAC-SHA256进行签名 87 | * 非必要请不要修改这个字段 */ 88 | clientProfile.setSignMethod("HmacSHA256"); 89 | clientProfile.setHttpProfile(httpProfile); 90 | /* 实例化要请求产品(以sms为例)的client对象 91 | * 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,或者引用预设的常量 */ 92 | SmsClient client = new SmsClient(cred, "ap-guangzhou",clientProfile); 93 | /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 94 | * 你可以直接查询SDK源码确定接口有哪些属性可以设置 95 | * 属性可能是基本类型,也可能引用了另一个数据结构 96 | * 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */ 97 | SendSmsRequest req = new SendSmsRequest(); 98 | 99 | /* 填充请求参数,这里request对象的成员变量即对应接口的入参 100 | * 你可以通过官网接口文档或跳转到request对象的定义处查看请求参数的定义 101 | * 基本类型的设置: 102 | * 帮助链接: 103 | * 短信控制台: https://console.cloud.tencent.com/smsv2 104 | * sms helper: https://cloud.tencent.com/document/product/382/3773 */ 105 | 106 | /* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */ 107 | req.setSmsSdkAppId(mTencentSMSConfig.appId); 108 | 109 | /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看 */ 110 | req.setSignName(mTencentSMSConfig.sign); 111 | 112 | /* 国际/港澳台短信 SenderId: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */ 113 | String senderid = ""; 114 | req.setSenderId(senderid); 115 | 116 | /* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 */ 117 | String sessionContext = "xxx"; 118 | req.setSessionContext(sessionContext); 119 | 120 | /* 短信号码扩展号: 默认未开通,如需开通请联系 [sms helper] */ 121 | String extendCode = ""; 122 | req.setExtendCode(extendCode); 123 | 124 | /* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */ 125 | req.setTemplateId(mTencentSMSConfig.templateId); 126 | 127 | /* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号] 128 | * 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 */ 129 | String[] phoneNumberSet = {mobile}; 130 | req.setPhoneNumberSet(phoneNumberSet); 131 | 132 | /* 模板参数: 若无模板参数,则设置为空 */ 133 | String[] templateParamSet = {code}; 134 | req.setTemplateParamSet(templateParamSet); 135 | 136 | /* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的 137 | * 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */ 138 | SendSmsResponse res = client.SendSms(req); 139 | 140 | // 输出json格式的字符串回包 141 | System.out.println(SendSmsResponse.toJsonString(res)); 142 | 143 | // 也可以取出单个值,你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义 144 | System.out.println(res.getRequestId()); 145 | return RestResult.RestCode.SUCCESS; 146 | } catch (TencentCloudSDKException e) { 147 | e.printStackTrace(); 148 | } 149 | return RestResult.RestCode.ERROR_SERVER_ERROR; 150 | } 151 | 152 | private RestResult.RestCode sendAliyunCode(String mobile, String code) { 153 | DefaultProfile profile = DefaultProfile.getProfile("default", aliyunSMSConfig.getAccessKeyId(), aliyunSMSConfig.getAccessSecret()); 154 | IAcsClient client = new DefaultAcsClient(profile); 155 | 156 | String templateparam = "{\"code\":\"" + code + "\"}"; 157 | CommonRequest request = new CommonRequest(); 158 | request.setMethod(MethodType.POST); 159 | request.setDomain("dysmsapi.aliyuncs.com"); 160 | request.setVersion("2017-05-25"); 161 | request.setAction("SendSms"); 162 | request.putQueryParameter("PhoneNumbers", mobile); 163 | request.putQueryParameter("SignName", aliyunSMSConfig.getSignName()); 164 | request.putQueryParameter("TemplateCode", aliyunSMSConfig.getTemplateCode()); 165 | request.putQueryParameter("TemplateParam", templateparam); 166 | try { 167 | CommonResponse response = client.getCommonResponse(request); 168 | System.out.println(response.getData()); 169 | if (response.getData() != null) { 170 | AliyunCommonResponse aliyunCommonResponse = new Gson().fromJson(response.getData(), AliyunCommonResponse.class); 171 | if (aliyunCommonResponse != null) { 172 | if (aliyunCommonResponse.Code.equalsIgnoreCase("OK")) { 173 | return RestResult.RestCode.SUCCESS; 174 | } else { 175 | System.out.println("Send aliyun sms failure with message:" + aliyunCommonResponse.Message); 176 | } 177 | } 178 | } 179 | } catch (ServerException e) { 180 | e.printStackTrace(); 181 | } catch (ClientException e) { 182 | e.printStackTrace(); 183 | } 184 | 185 | 186 | return RestResult.RestCode.ERROR_SERVER_ERROR; 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/avatar/GroupAvatarUtil.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.avatar; 2 | 3 | 4 | import javax.imageio.ImageIO; 5 | import java.awt.*; 6 | import java.awt.geom.AffineTransform; 7 | import java.awt.image.AffineTransformOp; 8 | import java.awt.image.BufferedImage; 9 | import java.io.DataInputStream; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.net.HttpURLConnection; 13 | import java.net.URISyntaxException; 14 | import java.net.URL; 15 | import java.net.URLConnection; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | /** 20 | * @author: fangdaji 21 | * @date: 2019/3/23 15:59 22 | * @description: 23 | */ 24 | public class GroupAvatarUtil { 25 | public static void getCombinationOfHead(List paths, File targetFile) 26 | throws IOException, URISyntaxException { 27 | 28 | List bufferedImages = new ArrayList(); 29 | // 压缩图片所有的图片生成尺寸同意的 为 50x50 30 | 31 | int imageSize = 33; 32 | if (paths.size() <= 4) { 33 | imageSize = 50; 34 | } 35 | 36 | for (int i = 0; i < paths.size(); i++) { 37 | bufferedImages.add(resize2(paths.get(i), imageSize, imageSize, true)); 38 | } 39 | 40 | int width = 112; // 这是画板的宽高 41 | 42 | int height = 112; // 这是画板的高度 43 | 44 | // BufferedImage.TYPE_INT_RGB可以自己定义可查看API 45 | 46 | BufferedImage outImage = new BufferedImage(width, height, 47 | BufferedImage.TYPE_INT_RGB); 48 | 49 | // 生成画布 50 | Graphics g = outImage.getGraphics(); 51 | 52 | Graphics2D g2d = (Graphics2D) g; 53 | 54 | // 设置背景色 55 | g2d.setBackground(new Color(231, 231, 231)); 56 | //g2d.setBackground(new Color(231, 0, 4)); 57 | 58 | // 通过使用当前绘图表面的背景色进行填充来清除指定的矩形。 59 | g2d.clearRect(0, 0, width, height); 60 | 61 | // 开始拼凑 根据图片的数量判断该生成那种样式的组合头像目前为4中 62 | int j = 1; 63 | int k = 1; 64 | for (int i = 1; i <= bufferedImages.size(); i++) { 65 | if (bufferedImages.size() == 9) { 66 | if (i <= 3) { 67 | g2d.drawImage(bufferedImages.get(i - 1), 33 * i + 3 * i - 33, 4, null); 68 | } else if (i <= 6) { 69 | g2d.drawImage(bufferedImages.get(i - 1), 33 * j + 3 * j - 33, 41, null); 70 | j++; 71 | } else { 72 | g2d.drawImage(bufferedImages.get(i - 1), 33 * k + 3 * k - 33, 77, null); 73 | k++; 74 | } 75 | } else if (bufferedImages.size() == 8) { 76 | if (i <= 2) { 77 | g2d.drawImage(bufferedImages.get(i - 1), 33 * i + 4 * i - 18, 4, null); 78 | } else if (i <= 5) { 79 | g2d.drawImage(bufferedImages.get(i - 1), 33 * j + 3 * j - 33, 41, null); 80 | j++; 81 | } else { 82 | g2d.drawImage(bufferedImages.get(i - 1), 33 * k + 3 * k - 33, 77, null); 83 | k++; 84 | } 85 | } else if (bufferedImages.size() == 7) { 86 | if (i <= 1) { 87 | g2d.drawImage(bufferedImages.get(i - 1), 39, 4, null); 88 | } else if (i <= 4) { 89 | g2d.drawImage(bufferedImages.get(i - 1), 33 * j + 3 * j - 33, 41, null); 90 | j++; 91 | } else { 92 | g2d.drawImage(bufferedImages.get(i - 1), 33 * k + 3 * k - 33, 77, null); 93 | k++; 94 | } 95 | } else if (bufferedImages.size() == 6) { 96 | if (i <= 3) { 97 | g2d.drawImage(bufferedImages.get(i - 1), 33 * i + 3 * i - 33, 15, null); 98 | } else { 99 | g2d.drawImage(bufferedImages.get(i - 1), 33 * j + 3 * j - 33, 58, null); 100 | j++; 101 | } 102 | } else if (bufferedImages.size() == 5) { 103 | if (i <= 2) { 104 | g2d.drawImage(bufferedImages.get(i - 1), 33 * i + 4 * i - 18, 15, null); 105 | } else { 106 | g2d.drawImage(bufferedImages.get(i - 1), 33 * j + 3 * j - 33, 58, null); 107 | j++; 108 | } 109 | } else if (bufferedImages.size() == 4) { 110 | if (i <= 2) { 111 | g2d.drawImage(bufferedImages.get(i - 1), 50 * i + 4 * i - 50, 4, null); 112 | } else { 113 | g2d.drawImage(bufferedImages.get(i - 1), 50 * j + 4 * j - 50, 58, null); 114 | j++; 115 | } 116 | } else if (bufferedImages.size() == 3) { 117 | if (i <= 1) { 118 | g2d.drawImage(bufferedImages.get(i - 1), 31, 4, null); 119 | } else { 120 | g2d.drawImage(bufferedImages.get(i - 1), 50 * j + 4 * j - 50, 58, null); 121 | j++; 122 | } 123 | 124 | } else if (bufferedImages.size() == 2) { 125 | 126 | g2d.drawImage(bufferedImages.get(i - 1), 50 * i + 4 * i - 50, 127 | 31, null); 128 | 129 | } else if (bufferedImages.size() == 1) { 130 | 131 | g2d.drawImage(bufferedImages.get(i - 1), 31, 31, null); 132 | 133 | } 134 | 135 | // 需要改变颜色的话在这里绘上颜色。可能会用到AlphaComposite类 136 | } 137 | 138 | 139 | String format = "png"; 140 | ImageIO.write(outImage, format, targetFile); 141 | } 142 | 143 | /** 144 | * 图片缩放 145 | * 146 | * @param filePath 图片路径 147 | * @param height 高度 148 | * @param width 宽度 149 | * @param bb 比例不对时是否需要补白 150 | */ 151 | private static BufferedImage resize2(URL filePath, int height, int width, 152 | boolean bb) { 153 | DataInputStream dis = null; 154 | try { 155 | URLConnection urlConnection = filePath.openConnection(); 156 | urlConnection.setConnectTimeout(5 * 1000); 157 | urlConnection.setReadTimeout(5 * 1000); 158 | dis = new DataInputStream(urlConnection.getInputStream()); 159 | 160 | double ratio = 0; // 缩放比例 161 | //File f = new File(dis); 162 | BufferedImage bi = ImageIO.read(dis); 163 | Image itemp = bi.getScaledInstance(width, height, 164 | Image.SCALE_SMOOTH); 165 | // 计算比例 166 | if ((bi.getHeight() > height) || (bi.getWidth() > width)) { 167 | if (bi.getHeight() > bi.getWidth()) { 168 | ratio = (new Integer(height)).doubleValue() 169 | / bi.getHeight(); 170 | } else { 171 | ratio = (new Integer(width)).doubleValue() / bi.getWidth(); 172 | } 173 | AffineTransformOp op = new AffineTransformOp( 174 | AffineTransform.getScaleInstance(ratio, ratio), null); 175 | itemp = op.filter(bi, null); 176 | } 177 | if (bb) { 178 | // copyimg(filePath, "D:\\img"); 179 | BufferedImage image = new BufferedImage(width, height, 180 | BufferedImage.TYPE_INT_RGB); 181 | Graphics2D g = image.createGraphics(); 182 | g.setColor(Color.white); 183 | g.fillRect(0, 0, width, height); 184 | if (width == itemp.getWidth(null)) { 185 | g.drawImage(itemp, 0, (height - itemp.getHeight(null)) / 2, 186 | itemp.getWidth(null), itemp.getHeight(null), 187 | Color.white, null); 188 | } else { 189 | g.drawImage(itemp, (width - itemp.getWidth(null)) / 2, 0, 190 | itemp.getWidth(null), itemp.getHeight(null), 191 | Color.white, null); 192 | } 193 | g.dispose(); 194 | itemp = image; 195 | } 196 | return (BufferedImage) itemp; 197 | } catch (IOException e) { 198 | e.printStackTrace(); 199 | } finally { 200 | if (dis != null) { 201 | try { 202 | dis.close(); 203 | } catch (IOException e) { 204 | throw new RuntimeException(e); 205 | } 206 | } 207 | 208 | } 209 | return null; 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/shiro/AuthDataSource.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app.shiro; 2 | 3 | import cn.wildfirechat.app.RestResult; 4 | import cn.wildfirechat.app.jpa.PCSession; 5 | import cn.wildfirechat.app.jpa.PCSessionRepository; 6 | import cn.wildfirechat.app.jpa.Record; 7 | import cn.wildfirechat.app.jpa.RecordRepository; 8 | import cn.wildfirechat.app.pojo.SessionOutput; 9 | import cn.wildfirechat.app.tools.Utils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.util.StringUtils; 16 | 17 | import java.util.Optional; 18 | import java.util.UUID; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.function.Supplier; 21 | 22 | import static cn.wildfirechat.app.RestResult.RestCode.*; 23 | import static cn.wildfirechat.app.jpa.PCSession.PCSessionStatus.*; 24 | 25 | @Service 26 | public class AuthDataSource { 27 | private static final Logger LOG = LoggerFactory.getLogger(AuthDataSource.class); 28 | @Value("${sms.super_code}") 29 | private String superCode; 30 | 31 | @Autowired 32 | private PCSessionRepository pcSessionRepository; 33 | 34 | @Autowired 35 | private RecordRepository recordRepository; 36 | 37 | public RestResult.RestCode insertRecord(String mobile, String code) { 38 | if (!Utils.isMobile(mobile)) { 39 | LOG.error("Not valid mobile {}", mobile); 40 | return RestResult.RestCode.ERROR_INVALID_MOBILE; 41 | } 42 | 43 | Record record = recordRepository.findById(mobile).orElseGet(() -> new Record(code, mobile)); 44 | 45 | if (System.currentTimeMillis() - record.getTimestamp() < 60 * 1000) { 46 | LOG.error("Send code over frequency. timestamp {}, now {}", record.getTimestamp(), System.currentTimeMillis()); 47 | return RestResult.RestCode.ERROR_SEND_SMS_OVER_FREQUENCY; 48 | } 49 | 50 | if (!record.increaseAndCheck()) { 51 | LOG.error("Count check failure, already send {} messages today", record.getRequestCount()); 52 | RestResult.RestCode c = RestResult.RestCode.ERROR_SEND_SMS_OVER_FREQUENCY; 53 | c.msg = "发送给用户 " + mobile + " 超出频率限制"; 54 | return c; 55 | } 56 | 57 | record.setCode(code); 58 | record.setTimestamp(System.currentTimeMillis()); 59 | recordRepository.save(record); 60 | return RestResult.RestCode.SUCCESS; 61 | } 62 | 63 | public void clearRecode(String mobile) { 64 | try { 65 | recordRepository.deleteById(mobile); 66 | } catch (Exception e) { 67 | } 68 | } 69 | 70 | public RestResult.RestCode verifyCode(String mobile, String code) { 71 | if (StringUtils.isEmpty(superCode) || !code.equals(superCode)) { 72 | Optional recordOptional = recordRepository.findById(mobile); 73 | if (!recordOptional.isPresent()) { 74 | LOG.error("code not exist"); 75 | return RestResult.RestCode.ERROR_CODE_INCORRECT; 76 | } 77 | if(!recordOptional.get().getCode().equals(code)) { 78 | LOG.error("code not matched"); 79 | return RestResult.RestCode.ERROR_CODE_INCORRECT; 80 | } 81 | 82 | if (System.currentTimeMillis() - recordOptional.get().getTimestamp() > 5 * 60 * 1000) { 83 | LOG.error("Code expired. timestamp {}, now {}", recordOptional.get().getTimestamp(), System.currentTimeMillis()); 84 | return RestResult.RestCode.ERROR_CODE_EXPIRED; 85 | } 86 | } 87 | return RestResult.RestCode.SUCCESS; 88 | } 89 | 90 | public PCSession createSession(String userId, String clientId, String token, int platform) { 91 | PCSession session = new PCSession(); 92 | session.setConfirmedUserId(userId); 93 | session.setStatus(StringUtils.isEmpty(userId) ? Session_Created : Session_Scanned); 94 | session.setClientId(clientId); 95 | session.setCreateDt(System.currentTimeMillis()); 96 | session.setPlatform(platform); 97 | session.setDuration(300 * 1000); //300 seconds 98 | 99 | if (StringUtils.isEmpty(token)) { 100 | token = UUID.randomUUID().toString(); 101 | } 102 | 103 | session.setToken(token); 104 | 105 | pcSessionRepository.save(session); 106 | return session; 107 | } 108 | 109 | public PCSession getSession(String token, boolean clear) { 110 | Optional session = pcSessionRepository.findById(token); 111 | if (clear) { 112 | pcSessionRepository.deleteById(token); 113 | } 114 | return session.orElse(null); 115 | } 116 | 117 | public void saveSession(PCSession session) { 118 | pcSessionRepository.save(session); 119 | } 120 | 121 | public RestResult scanPc(String userId, String token) { 122 | Optional session = pcSessionRepository.findById(token); 123 | if (session.isPresent()) { 124 | SessionOutput output = session.get().toOutput(); 125 | LOG.info("user {} scan pc, session {} expired time left {}", userId, token, output.getExpired()); 126 | if (output.getExpired() > 0) { 127 | session.get().setStatus(Session_Scanned); 128 | session.get().setConfirmedUserId(userId); 129 | output.setStatus(Session_Scanned); 130 | output.setUserId(userId); 131 | pcSessionRepository.save(session.get()); 132 | return RestResult.ok(output); 133 | } else { 134 | return RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED); 135 | } 136 | } else { 137 | LOG.info("user {} scan pc, session {} not exist!", userId, token); 138 | return RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED); 139 | } 140 | } 141 | 142 | public RestResult confirmPc(String userId, String token) { 143 | Optional session = pcSessionRepository.findById(token); 144 | if (session.isPresent()) { 145 | SessionOutput output = session.get().toOutput(); 146 | LOG.info("user {} confirm pc, session {} expired time left {}", userId, token, output.getExpired()); 147 | if (output.getExpired() > 0) { 148 | session.get().setStatus(Session_Verified); 149 | output.setStatus(Session_Verified); 150 | session.get().setConfirmedUserId(userId); 151 | pcSessionRepository.save(session.get()); 152 | return RestResult.ok(output); 153 | } else { 154 | return RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED); 155 | } 156 | } else { 157 | LOG.error("user {} scan pc, session {} not exist!", userId, token); 158 | return RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED); 159 | } 160 | } 161 | 162 | public RestResult cancelPc(String token) { 163 | LOG.error("session {} canceled", token); 164 | Optional session = pcSessionRepository.findById(token); 165 | if (session.isPresent()) { 166 | session.get().setStatus(Session_Canceled); 167 | pcSessionRepository.save(session.get()); 168 | } 169 | 170 | return RestResult.ok(null); 171 | } 172 | 173 | public RestResult.RestCode checkPcSession(String token) { 174 | Optional session = pcSessionRepository.findById(token); 175 | if (session.isPresent()) { 176 | if (session.get().getStatus() == Session_Verified) { 177 | //使用用户id获取token 178 | return SUCCESS; 179 | } else { 180 | if (session.get().getStatus() == Session_Created) { 181 | return ERROR_SESSION_NOT_SCANED; 182 | } else if (session.get().getStatus() == Session_Canceled) { 183 | return ERROR_SESSION_CANCELED; 184 | } else { 185 | return ERROR_SESSION_NOT_VERIFIED; 186 | } 187 | } 188 | } else { 189 | return RestResult.RestCode.ERROR_SESSION_EXPIRED; 190 | } 191 | } 192 | 193 | public String getUserId(String token, boolean clear) { 194 | Optional session = pcSessionRepository.findById(token); 195 | if (clear) { 196 | pcSessionRepository.deleteById(token); 197 | } 198 | 199 | if (session.isPresent()) { 200 | return session.get().getConfirmedUserId(); 201 | } 202 | 203 | return null; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/app/AppController.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.app; 2 | 3 | import cn.wildfirechat.app.jpa.FavoriteItem; 4 | import cn.wildfirechat.app.pojo.*; 5 | import cn.wildfirechat.pojos.InputCreateDevice; 6 | import org.apache.shiro.SecurityUtils; 7 | import org.apache.shiro.subject.Subject; 8 | import org.h2.util.StringUtils; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.*; 15 | import org.springframework.web.context.request.async.DeferredResult; 16 | import org.springframework.web.multipart.MultipartFile; 17 | 18 | import javax.servlet.http.HttpServletResponse; 19 | import java.io.IOException; 20 | import java.util.concurrent.CompletableFuture; 21 | import java.util.concurrent.Executors; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | @RestController 25 | public class AppController { 26 | private static final Logger LOG = LoggerFactory.getLogger(AppController.class); 27 | @Autowired 28 | private Service mService; 29 | 30 | @GetMapping() 31 | public Object health() { 32 | return "Ok"; 33 | } 34 | 35 | /* 36 | 移动端登录 37 | */ 38 | @PostMapping(value = "/send_code", produces = "application/json;charset=UTF-8") 39 | public Object sendLoginCode(@RequestBody SendCodeRequest request) { 40 | return mService.sendLoginCode(request.getMobile()); 41 | } 42 | 43 | @PostMapping(value = "/login", produces = "application/json;charset=UTF-8") 44 | public Object loginWithMobileCode(@RequestBody PhoneCodeLoginRequest request, HttpServletResponse response) { 45 | return mService.loginWithMobileCode(response, request.getMobile(), request.getCode(), request.getClientId(), request.getPlatform() == null ? 0 : request.getPlatform()); 46 | } 47 | 48 | @PostMapping(value = "/login_pwd", produces = "application/json;charset=UTF-8") 49 | public Object loginWithPassword(@RequestBody UserPasswordLoginRequest request, HttpServletResponse response) { 50 | return mService.loginWithPassword(response, request.getMobile(), request.getPassword(), request.getClientId(), request.getPlatform() == null ? 0 : request.getPlatform()); 51 | } 52 | 53 | @PostMapping(value = "/change_pwd", produces = "application/json;charset=UTF-8") 54 | public Object changePassword(@RequestBody ChangePasswordRequest request) { 55 | return mService.changePassword(request.getOldPassword(), request.getNewPassword()); 56 | } 57 | 58 | @PostMapping(value = "/send_reset_code", produces = "application/json;charset=UTF-8") 59 | public Object sendResetCode(@RequestBody SendCodeRequest request) { 60 | return mService.sendResetCode(request.getMobile()); 61 | } 62 | 63 | @PostMapping(value = "/reset_pwd", produces = "application/json;charset=UTF-8") 64 | public Object resetPassword(@RequestBody ResetPasswordRequest request) { 65 | return mService.resetPassword(request.getMobile(), request.getResetCode(), request.getNewPassword()); 66 | } 67 | 68 | @PostMapping(value = "/send_destroy_code", produces = "application/json;charset=UTF-8") 69 | public Object sendDestroyCode() { 70 | return mService.sendDestroyCode(); 71 | } 72 | 73 | @PostMapping(value = "/destroy", produces = "application/json;charset=UTF-8") 74 | public Object destroy(@RequestBody DestroyRequest code, HttpServletResponse response) { 75 | return mService.destroy(response, code.getCode()); 76 | } 77 | 78 | /* PC扫码操作 79 | 1, PC -> App 创建会话 80 | 2, PC -> App 轮询调用session_login进行登陆,如果已经扫码确认返回token,否则返回错误码9(已经扫码还没确认)或者10(还没有被扫码) 81 | */ 82 | @CrossOrigin 83 | @PostMapping(value = "/pc_session", produces = "application/json;charset=UTF-8") 84 | public Object createPcSession(@RequestBody CreateSessionRequest request) { 85 | return mService.createPcSession(request); 86 | } 87 | 88 | @CrossOrigin 89 | @PostMapping(value = "/session_login/{token}", produces = "application/json;charset=UTF-8") 90 | public Object loginWithSession(@PathVariable("token") String token) { 91 | LOG.info("receive login with session key {}", token); 92 | RestResult timeoutResult = RestResult.error(RestResult.RestCode.ERROR_SESSION_EXPIRED); 93 | ResponseEntity timeoutResponseEntity = new ResponseEntity<>(timeoutResult, HttpStatus.OK); 94 | int timeoutSecond = 50; 95 | DeferredResult deferredResult = new DeferredResult<>(timeoutSecond * 1000L, timeoutResponseEntity); 96 | CompletableFuture.runAsync(() -> { 97 | try { 98 | int i = 0; 99 | while (i < timeoutSecond) { 100 | RestResult restResult = mService.loginWithSession(token); 101 | if (restResult.getCode() == RestResult.RestCode.ERROR_SESSION_NOT_VERIFIED.code && restResult.getResult() != null) { 102 | deferredResult.setResult(new ResponseEntity(restResult, HttpStatus.OK)); 103 | break; 104 | } else if (restResult.getCode() == RestResult.RestCode.SUCCESS.code 105 | || restResult.getCode() == RestResult.RestCode.ERROR_SESSION_EXPIRED.code 106 | || restResult.getCode() == RestResult.RestCode.ERROR_SERVER_ERROR.code 107 | || restResult.getCode() == RestResult.RestCode.ERROR_SESSION_CANCELED.code 108 | || restResult.getCode() == RestResult.RestCode.ERROR_CODE_INCORRECT.code) { 109 | ResponseEntity.BodyBuilder builder =ResponseEntity.ok(); 110 | if(restResult.getCode() == RestResult.RestCode.SUCCESS.code){ 111 | Subject subject = SecurityUtils.getSubject(); 112 | Object sessionId = subject.getSession().getId(); 113 | builder.header("authToken", sessionId.toString()); 114 | } 115 | deferredResult.setResult(builder.body(restResult)); 116 | break; 117 | } else { 118 | TimeUnit.SECONDS.sleep(1); 119 | } 120 | i ++; 121 | } 122 | } catch (Exception ex) { 123 | ex.printStackTrace(); 124 | deferredResult.setResult(new ResponseEntity(RestResult.error(RestResult.RestCode.ERROR_SERVER_ERROR), HttpStatus.OK)); 125 | } 126 | }, Executors.newCachedThreadPool()); 127 | return deferredResult; 128 | } 129 | 130 | /* 手机扫码操作 131 | 1,扫码,调用/scan_pc接口。 132 | 2,调用/confirm_pc 接口进行确认 133 | */ 134 | @PostMapping(value = "/scan_pc/{token}", produces = "application/json;charset=UTF-8") 135 | public Object scanPc(@PathVariable("token") String token) { 136 | return mService.scanPc(token); 137 | } 138 | 139 | @PostMapping(value = "/confirm_pc", produces = "application/json;charset=UTF-8") 140 | public Object confirmPc(@RequestBody ConfirmSessionRequest request) { 141 | return mService.confirmPc(request); 142 | } 143 | @PostMapping(value = "/cancel_pc", produces = "application/json;charset=UTF-8") 144 | public Object cancelPc(@RequestBody CancelSessionRequest request) { 145 | return mService.cancelPc(request); 146 | } 147 | 148 | /* 149 | 修改野火账户 150 | */ 151 | @CrossOrigin 152 | @PostMapping(value = "/change_name", produces = "application/json;charset=UTF-8") 153 | public Object changeName(@RequestBody ChangeNameRequest request) { 154 | if (StringUtils.isNullOrEmpty(request.getNewName())) { 155 | return RestResult.error(RestResult.RestCode.ERROR_INVALID_PARAMETER); 156 | } 157 | return mService.changeName(request.getNewName()); 158 | } 159 | 160 | 161 | /* 162 | 群公告相关接口 163 | */ 164 | @CrossOrigin 165 | @PostMapping(value = "/put_group_announcement", produces = "application/json;charset=UTF-8") 166 | public Object putGroupAnnouncement(@RequestBody GroupAnnouncementPojo request) { 167 | return mService.putGroupAnnouncement(request); 168 | } 169 | 170 | @CrossOrigin 171 | @PostMapping(value = "/get_group_announcement", produces = "application/json;charset=UTF-8") 172 | public Object getGroupAnnouncement(@RequestBody GroupIdPojo request) { 173 | return mService.getGroupAnnouncement(request.groupId); 174 | } 175 | 176 | /* 177 | 客户端上传协议栈日志 178 | */ 179 | @PostMapping(value = "/logs/{userId}/upload") 180 | public Object uploadFiles(@RequestParam("file") MultipartFile file, @PathVariable("userId") String userId) throws IOException { 181 | return mService.saveUserLogs(userId, file); 182 | } 183 | 184 | /* 185 | 投诉和建议 186 | */ 187 | @CrossOrigin 188 | @PostMapping(value = "/complain", produces = "application/json;charset=UTF-8") 189 | public Object complain(@RequestBody ComplainRequest request) { 190 | return mService.complain(request.text); 191 | } 192 | 193 | /* 194 | 物联网相关接口 195 | */ 196 | @PostMapping(value = "/things/add_device") 197 | public Object addDevice(@RequestBody InputCreateDevice createDevice) { 198 | return mService.addDevice(createDevice); 199 | } 200 | 201 | @PostMapping(value = "/things/list_device") 202 | public Object getDeviceList() { 203 | return mService.getDeviceList(); 204 | } 205 | 206 | @PostMapping(value = "/things/del_device") 207 | public Object delDevice(@RequestBody InputCreateDevice createDevice) { 208 | return mService.delDevice(createDevice); 209 | } 210 | 211 | /* 212 | 发送消息 213 | */ 214 | @PostMapping(value = "/messages/send") 215 | public Object sendUserMessage(@RequestBody SendMessageRequest sendMessageRequest) { 216 | return mService.sendUserMessage(sendMessageRequest); 217 | } 218 | 219 | /* 220 | iOS设备Share extension分享图片文件等使用 221 | */ 222 | @PostMapping(value = "/media/upload/{media_type}") 223 | public Object uploadMedia(@RequestParam("file") MultipartFile file, @PathVariable("media_type") int mediaType) throws IOException { 224 | return mService.uploadMedia(mediaType, file); 225 | } 226 | 227 | @CrossOrigin 228 | @PostMapping(value = "/fav/add", produces = "application/json;charset=UTF-8") 229 | public Object putFavoriteItem(@RequestBody FavoriteItem request) { 230 | return mService.putFavoriteItem(request); 231 | } 232 | 233 | @CrossOrigin 234 | @PostMapping(value = "/fav/del/{fav_id}", produces = "application/json;charset=UTF-8") 235 | public Object removeFavoriteItem(@PathVariable("fav_id") int favId) { 236 | return mService.removeFavoriteItems(favId); 237 | } 238 | 239 | @CrossOrigin 240 | @PostMapping(value = "/fav/list", produces = "application/json;charset=UTF-8") 241 | public Object getFavoriteItems(@RequestBody LoadFavoriteRequest request) { 242 | return mService.getFavoriteItems(request.id, request.count); 243 | } 244 | 245 | @CrossOrigin 246 | @PostMapping(value = "/group/members_for_portrait", produces = "application/json;charset=UTF-8") 247 | public Object getGroupMembersForPortrait(@RequestBody GroupIdPojo groupIdPojo) { 248 | return mService.getGroupMembersForPortrait(groupIdPojo.groupId); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | cn.wildfirechat 7 | app 8 | 0.71 9 | jar 10 | 11 | app 12 | Demo project for Wildfire chat app server 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.2.10.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 2.17.2 26 | 1.4.2 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-test 43 | test 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-data-rest 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-data-jpa 53 | 54 | 55 | com.h2database 56 | h2 57 | 58 | 59 | 60 | mysql 61 | mysql-connector-java 62 | 8.0.28 63 | 64 | 65 | 66 | com.google.code.gson 67 | gson 68 | 2.8.9 69 | 70 | 71 | 72 | com.google.protobuf 73 | protobuf-java 74 | 2.5.0 75 | 76 | 77 | 78 | org.apache.logging.log4j 79 | log4j-slf4j-impl 80 | ${log4j2.version} 81 | 82 | 83 | org.apache.logging.log4j 84 | log4j-api 85 | ${log4j2.version} 86 | 87 | 88 | org.apache.logging.log4j 89 | log4j-core 90 | ${log4j2.version} 91 | 92 | 93 | org.apache.logging.log4j 94 | log4j-to-slf4j 95 | ${log4j2.version} 96 | 97 | 98 | 99 | commons-io 100 | commons-io 101 | 2.7 102 | 103 | 104 | 105 | com.googlecode.json-simple 106 | json-simple 107 | 1.1.1 108 | 109 | 110 | 111 | org.slf4j 112 | slf4j-api 113 | 1.7.5 114 | 115 | 116 | 117 | org.slf4j 118 | slf4j-log4j12 119 | 1.7.5 120 | 121 | 122 | 123 | 124 | commons-httpclient 125 | commons-httpclient 126 | 3.1 127 | 128 | 129 | 130 | uk.org.lidalia 131 | slf4j-test 132 | 1.0.0-jdk6 133 | test 134 | 135 | 136 | 137 | org.mockito 138 | mockito-all 139 | 1.9.5 140 | jar 141 | test 142 | 143 | 144 | 145 | com.tencentcloudapi 146 | tencentcloud-sdk-java-sms 147 | 3.1.410 148 | 149 | 150 | 151 | cn.wildfirechat 152 | sdk 153 | ${wfc.sdk.version} 154 | system 155 | ${project.basedir}/src/lib/sdk-${wfc.sdk.version}.jar 156 | 157 | 158 | 159 | cn.wildfirechat 160 | common 161 | ${wfc.sdk.version} 162 | system 163 | ${project.basedir}/src/lib/common-${wfc.sdk.version}.jar 164 | 165 | 166 | 167 | 168 | com.aliyun 169 | aliyun-java-sdk-core 170 | 4.1.0 171 | 172 | 173 | 174 | com.qiniu 175 | qiniu-java-sdk 176 | 7.3.0 177 | 178 | 179 | 180 | com.aliyun.oss 181 | aliyun-sdk-oss 182 | 3.10.2 183 | 184 | 185 | 186 | io.minio 187 | minio 188 | 7.0.2 189 | 190 | 191 | 192 | com.qcloud 193 | cos_api 194 | 5.6.28 195 | 196 | 197 | 198 | com.google.guava 199 | guava 200 | 25.1-jre 201 | 202 | 203 | 204 | org.apache.shiro 205 | shiro-spring 206 | 1.7.1 207 | 208 | 209 | 210 | ws.schild 211 | jave-core 212 | 2.7.3 213 | 214 | 215 | 216 | ws.schild 217 | jave-nativebin-linux64 218 | 2.7.3 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | org.springframework.boot 243 | spring-boot-starter-mail 244 | 245 | 246 | 247 | 248 | 249 | 250 | org.springframework.boot 251 | spring-boot-maven-plugin 252 | 253 | true 254 | 255 | 256 | 257 | 258 | jdeb 259 | org.vafer 260 | 1.8 261 | 262 | 263 | package 264 | 265 | jdeb 266 | 267 | 268 | ${project.basedir}/deb/control 269 | false 270 | ${project.build.directory}/app-server-${project.version}.deb 271 | 272 | 273 | ${project.build.directory}/${project.name}-${project.version}.jar 274 | file 275 | 276 | perm 277 | /opt/app-server 278 | 279 | 280 | 281 | ${project.basedir}/config 282 | directory 283 | 284 | perm 285 | /opt/app-server/config 286 | 287 | 288 | 289 | ${project.basedir}/systemd/app-server.service 290 | file 291 | 292 | perm 293 | /usr/lib/systemd/system 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | --------------------------------------------------------------------------------